platform-assistant/AGENTS.md
Sisyphus 8d8945f92b docs: 更新 AGENTS.md、gitignore(排除上传文件)、开发文档
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-06-26 16:25:24 +08:00

272 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# AGENTS.md
语言:中文。所有对话、注释、文档、提交信息均使用中文。
## 🤝 协作流程标准(避免理解不一致造成返工)
**所有具体问题的修改,必须按这个流程执行:**
```
1. 用户描述需求(业务场景、看到的问题、截图、日志)
↓
2. 协作者反问 7 个技术维度(场景/触发/重要程度/数据/边界/样式/部署)
↓
3. 对齐范围:用户确认 + 协作者列技术方案
↓
4. 实施 + 验证
↓
5. 用户看效果,反馈或继续下一项
```
### 协作者必须问的 7 个技术维度
| 维度 | 问什么 |
|------|--------|
| **场景** | 哪个页面/哪个模块?用户路径是什么? |
| **触发条件** | 什么时候出现?手动还是自动?频率? |
| **重要程度** | 阻塞核心流程 / 体验优化 / 锦上添花? |
| **数据** | 影响多少数据?要不要数据迁移? |
| **边界** | 空数据/异常情况怎么处理? |
| **样式** | 改动大吗?要不要出截图对比? |
| **生产部署** | 改完要不要立即部署?还是攒一起? |
### 原则
- **不擅自开干**:未对齐范围前不做实现
- **不预设方案**:用户描述 ≠ 协作者的解决方案,先问清楚
- **不省略验证**:改完必须跑端到端测试,不只是"代码看起来对"
- **不静默改大动作**:涉及数据库结构、生产配置、依赖升级等要明确告知
### 例外(不需要问的简单场景)
- 拼写错误、明显笔误
- 单文件 typo 修复
- 用户明确说"按你判断做"
- 紧急 bug 修复(事后补问)
## 项目结构
```
/
├── apps/
│ ├── web/ # Next.js 14 App Router + Tailwind CSS 3
│ └── api/ # NestJS 10 + Prisma ORM + SQLite
├── packages/
│ └── shared/ # 共享 TS 类型(编译型,无运行时依赖)
└── scripts/
├── parse_products.py # 解析 Excel/CSV → JSON
├── tender-fetcher/ # 招标公告抓取脚本
│ ├── fetch-v1.2.js # 中电建
│ ├── fetch-xkgroup.py # 新矿集团
│ ├── fetch-cnncecp.py # 中核集团
│ └── lead_evaluation_report.py
└── direv-publish/ # direv 平台自动发布脚本(10 个 .py)
```
## 开发命令
```bash
npm run setup # 首次:npm install + db:init
npm run dev # 同时启动前端(3001)和后端(4000)
npm run dev:api # 仅后端(nest start --watch,用 dist/ 编译产物)
npm run dev:web # 仅前端
npm run db:init # prisma generate → migrate → seed
npm run db:reset # 删 db 文件后 db:init
npm run build # nest build + next build
npm run lint # eslint + next lint
npm run typecheck # tsc --noEmit 所有包(无 root tsconfig,各包独立)
npm run dev:daemon # nohup 后台启动(PID 写入 .dev-api.pid / .dev-web.pid)
npm run dev:stop # kill PID 文件中记录的进程
```
## 端口速查 (本地开发)
| 端口 | 服务 | 启动方式 |
|---|---|---|
| **3001** | 平台助手 web (Next.js) | `npm run dev:web` 或 `npm run dev` |
| **4000** | 平台助手 API (NestJS) | `npm run dev:api` 或 `npm run dev` |
| **3002** | devbox MCP server (Node.js) | `ssh devbox` + PM2, **不在 Mac 本地** |
注意:**3002 是 devbox 上的 MCP server 端口, 不是平台助手 web 端口**。
打开浏览器访问平台助手用 `http://localhost:3001/` (不是 3002)。
AdminPage.tsx / schema.prisma 里出现的 `100.111.84.16:3002/mcp` 是 MCP server 地址 (填到 ConfigTab 的"endpoint" 字段), 不是 web 入口。
## 后端陷阱
- **`nest start --watch` 用 `dist/` 编译产物**:Prisma schema 变更后必须 `npm run build -w apps/api` 重建 dist,再重启。单纯 `prisma generate` 不够
- **端口 4000 残留进程**:`dev:stop` 只杀 PID 文件中记录的进程。如果有多余进程占端口,需手动 `lsof -ti:4000 | xargs kill -9`
- **Prisma 变更流程**:改 schema → `npm run db:init`(含 generate + migrate + seed)
- SQLite:`apps/api/prisma/dev.db`;种子数据(seed.ts):15 电缆型号 × 5 供应商 = 75 Product + 管理用户;`crawlerConfig` 保留 3 条(中电建 + 新矿集团 + 中核集团,中核默认 paused)
- `CrawlerService`(新增):`bid_powerchina` 类型 → `execSync('node fetch-v1.2.js')`;`bid_xkgroup` 类型 → `execSync('python3 fetch-xkgroup.py')`。脚本路径硬编码为 `scripts/tender-fetcher/`,结果从 `~/Desktop/招标公告/<date>/downloaded.json` 读取,upsert 到 Lead 表
- Lead 表 `noticeId` 从 `summary` 中提取时曾因 `substr` 字符/字节偏移混淆导致截断错误(`+8` 应为 `+6`)。提取公告 ID 统一用 `substr(summary, instr(summary, '公告ID:') + 6, 32)`
- Lead 表 `@@unique([source, noticeId])`:去重键,`CrawlerService.run()` 用 `upsert` 写入,重复则更新 summary/detailUrl
- Lead 表 `detailUrl`:独立字段,前端显示为可点击链接。脚本保存 `id` 后重构 URL(中电建用 `r.id` 而非 `systemId`;新矿需补 `&userId=guest&accountId=2066845847806639104` 认证参数)
- 后端无测试框架
## 前端陷阱
- **SPA 无路由**:`page.tsx` 用 `useState<Page>` 切换 14 个组件(含 LoginPage + Sidebar + Providers)。导航通过 `onNavigate` prop 传递 `page` 值,参数通过 props
- **API base**:`src/lib/apiBase.ts` 暴露 `API_BASE`,构建期由 `NEXT_PUBLIC_API_URL` 注入。本地开发不设环境变量时回退到 `http://localhost:4000`,生产构建为 `/api`(Nginx 反代到 4000)
- JWT 存 localStorage key `auth`,AuthProvider 启动时验证
- 3000 端口被占用,`next dev` 自动 fallback 到 3001
- 路径别名 `@/` → `src/`
- Prettier: noSemi, singleQuote, trailingComma: all, printWidth: 100
- Tailwind 设计 Token:`secondary(#0051d5)`, `border-subtle(#E2E8F0)`, `surface-container` 系
## 前端数据对接状态
| 页面 | 状态 |
|------|------|
| ComparisonPage | ✅ GET `api/comparison`,POST upload/suggestion |
| ImagePage | ✅ GET `api/images`,POST generate/parse(MiniMax 生图) |
| ProductPage | ✅ GET/POST/PATCH/DELETE `api/products` + POST upload-image |
| OverviewPage | ✅ Promise.all([GET `api/leads/stats`, GET `api/leads`]) |
| InquiryPage | ✅ GET `api/leads`,POST `api/leads/{id}/publish` |
| LeadDetailPage | ✅ GET `api/leads/{id}`,POST to-rfq/to-collab/publish/follow-up |
| RuleCenterPage | 含"采集器配置"Tab:GET/POST/PUT `api/crawlers`,POST run/pause/resume |
| ReportCenterPage | 纯 mock |
| SystemMonitorPage | 纯 mock |
| AdminPage | 已实现,5 个子区块(tenants/users/stats/configs/plans),configs 含 MCP 对接配置 |
| LoginPage | POST `api/auth/login` |
## 演示账号
`admin` / `admin123`(username + password,**不是 email**;AGENTS.md 旧版写的 `admin@platform.com` 不对,后端 LoginDto 用 username 字段)
## 数据库模型
User, Lead(含 AI 评分/状态流转/跟进日志), Rule/RuleVersion, Report, AuditLog, CrawlerConfig(含 keywords/categories + cron schedule), Inquiry, Product(含 supplier/imageUrl), GeneratedImage
## 采集器关键事实
- 调度基于 `@nestjs/schedule` + `SchedulerRegistry`,`onModuleInit` 时从 DB 加载 `active` 配置注册 cron
- pause/resume/update 联动调度器的注册与注销
- `CrawlerConfig` 的 `schedule` 字段存储 cron 表达式,前端可选:每4小时、每6小时、每天早8点、每天晚7点、手动
- 运行采集器有即时反馈 Modal(请求完自动更新状态,也可"后台运行"关闭弹窗)
- 脚本路径相对于运行时工作目录,实际为 `scripts/tender-fetcher/`
- **通用化新增采集器**:PowerChina 保持特殊处理(JSON 格式不同)。其他网站只需:
1. 在 `scripts/tender-fetcher/` 放脚本(如 `fetch-xxx.py`),输出 `~/desktop/招标公告/<date>/xxx_downloaded.json`
2. 前端"采集器配置"新建记录,`type` 填 `bid_xxx`,`sourceName` 填来源显示名,`scriptPath` 填脚本文件名
3. 无需改代码,`runGenericScript()` 自动推断脚本和 JSON 文件路径
- `CrawlerConfig` 新增 `sourceName`(线索来源显示名)和 `scriptPath`(脚本文件名,可选,默认从 type 推断)
## 生产部署(腾讯云 124.222.182.242 / direv.cloud)
- **路径**:`/opt/pingtai/`(代码) + `/data/pingtai/`(持久化数据,dev.db / uploads / library / 招标公告)
- **进程**:`pm2` 托管 `pingtai-api`(NestJS 4000)+ `pingtai-web`(Next.js 3001),systemd 自启(`/etc/systemd/system/pm2-ubuntu.service`)
- **反代**:Nginx 80/443 → 3001;`/api/*` → 4000;`/uploads/*` → `/data/pingtai/uploads/` 静态直出
- **HTTPS**:Let's Encrypt(certbot),自动续期 timer 已开
- **本地重部署流程**:
1. `npm run build` 重新编译 api/web
2. `rsync` 同步 dist/.next + 源码到 `/opt/pingtai/`
3. `pm2 restart pingtai-api pingtai-web`(注意 `pm2 restart` 默认不更新环境变量,要 `--update-env`)
4. Prisma 变更后必须先 `npx prisma migrate deploy && npx prisma generate`(在 `apps/api/` 下)
- **常见操作**:
- `pm2 list` / `pm2 logs` / `pm2 restart pingtai-api`
- `tail -f /var/log/pingtai/api.out-1.log`
- `sqlite3 /data/pingtai/dev.db` 直接查 DB
- 备份:`tar czf pingtai-backup-$(date +%Y%m%d).tgz /data/pingtai/`
- **历史包袱**:服务器上旧的 5月22日 Postgres/Redis 部署已备份到 `/home/ubuntu/platform-assistant.bak-*`,旧 nginx conf 在 `/etc/nginx/conf.d/platform.conf.bak-*`,如需恢复可回退
- **采集器脚本**:`scripts/tender-fetcher/` 写出的 `~/desktop/招标公告/` 通过 symlink `~/desktop -> /data/pingtai` 落到持久化卷;symlink 必须指向 `/data/pingtai`(不是 `/data/pingtai/招标公告`),否则会双层嵌套
## CODE MAP
| Symbol | Type | Location | Role |
|--------|------|----------|------|
| `AppModule` | module | apps/api/src/app.module.ts | 根模块,导入 13 个子模块 |
| `bootstrap()` | function | apps/api/src/main.ts | NestJS 应用启动入口 |
| `page.tsx` | page | apps/web/src/app/page.tsx | SPA 根,useState 切换 14 页面 |
| `AuthProvider` | context | apps/web/src/components/AuthProvider.tsx | JWT 认证上下文 |
| `CrawlerService.run()` | method | apps/api/src/crawler/crawler.service.ts | 采集器入口,execSync 调度脚本 |
| `ImageService` | class | apps/api/src/image/image.service.ts | 三阶段生图(图库→Bing→AI) |
| `runGenericScript()` | method | apps/api/src/crawler/crawler.service.ts | 通用化采集器调度 |
| `search_image.py` | script | scripts/search_image.py | Bing 搜索 top N 候选 |
| `process_image.py` | script | scripts/process_image.py | 去水印+裁剪+白底合成 |
## product_network B2B 平台对接(2026-06-16 新增,MCP 主通道)
### 对接架构
```
平台助手 NestJS (4000)
↓ IntegrationService.callTool()
↓ MCP 协议 (Streamable HTTP)
↓ x-app-id + x-app-secret (多租户 APIKey, 替代旧的 x-api-key)
renren-mcp-server (Node.js, devbox:3002)
↓ 直调 Java 业务方法
↓
product_network 7 个 Java 服务
```
注: MCP server 端口 3002 是 **devbox 开发机** 上的端口, 不是平台助手本地端口 (3001/4000)。
### 设计原则
- **MCP 主通道**:17 个 tool(询价 5 + 商品 4 + 比价 4 + 推理 4)
- **REST 兼容层**:5 个端点(健康/兑换/订阅/订单/撤销)
- **APIKey 认证**:Header `x-api-key`,AES-256-GCM 加密存储(密钥 `ENCRYPT_KEY` in `.env`,32 bytes hex)
- **公司账号代理**:每客户 1 个 `sys_user` (user_type=99)
### 关键代码路径
| 文件 | 说明 |
|------|------|
| `apps/api/prisma/schema.prisma` | `Integration` 表(MCP 连接配置) |
| `apps/api/src/integration/integration.service.ts` | MCP client 管理(connect/listTools/callTool) |
| `apps/api/src/integration/integration.controller.ts` | REST API(CRUD + test + connect) |
| `apps/api/src/integration/integration.module.ts` | NestJS 模块 |
| `apps/api/src/lead/lead.service.ts` | `publishToDirev()` 调 MCP `publish_rfq` tool |
| `apps/web/src/components/AdminPage.tsx` → `ConfigTab` | MCP 对接配置(嵌入系统配置页面) |
### 使用流程
1. 前端「运营管理 → 系统配置」页面顶部"MCP 对接配置"区域
2. 点击「新增对接」,填入:名称、MCP Server 地址、APIKey、客户编码
3. 点击「连接」验证 MCP server 可达并获取工具列表
4. 发布询价时,自动通过 MCP 调用 `publish_rfq` tool
### MCP Client 实现
```typescript
// integration.service.ts 核心逻辑
const client = new Client({ name: 'platform-assistant', version: '1.0.0' })
const transport = new StreamableHTTPClientTransport(
new URL(endpoint),
{ requestInit: { headers: { 'x-api-key': apiKey } } }
)
await client.connect(transport)
const tools = await client.listTools()
await client.callTool({ name: 'publish_rfq', arguments: { title, ... } })
```
### API 端点
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/api/integrations` | 列表 |
| POST | `/api/integrations` | 创建 |
| PUT | `/api/integrations/:id` | 更新 |
| DELETE | `/api/integrations/:id` | 删除 |
| POST | `/api/integrations/:id/connect` | 连接 MCP |
| POST | `/api/integrations/:id/disconnect` | 断开 |
| POST | `/api/integrations/test` | 测试连接 |
| POST | `/api/integrations/:id/call-tool` | 调用工具 |
### 注意事项
- MCP server 地址格式:`http://host:port/mcp`(Streamable HTTP transport)
- APIKey 通过 Header `x-api-key` 传递
- 客户编码通过 Header `x-customer-code` 传递(可选)
- **APIKey 加密**:AES-256-GCM,存 DB 为 base64 密文(iv12+tag16+ciphertext),读 API 时自动解密。旧明文数据读取时 fallback 不加密(兼容)
- 连接状态存储在数据库,服务重启后需重新连接
- 发布失败降级为本地模式(生成假 inquiryNo)
## ANTI-PATTERNS
- 前端不加路由库,保持 SPA useState 模式
- 不提交 secrets 到 git(JWT_SECRET 已在 .env.production,但 .gitignore 未排除)
- 不删除旧采集器 state 文件(中电建用 `systemId_招采公告` 作 key)
- 不依赖 Postgres/Redis(docker-compose.yml 是历史遗留,实际只用 SQLite)
- 不在后端代码中硬编码脚本路径以外的文件路径