- W26: NestJS API + Next.js Web + Prisma + SQLite scaffold - W26: Lead 模型 + Crawler + 询价发布助手 (线索池 + 一键发布) - W29.x: MCP 集成 (Integration 模型 + connectMcp + publish_rfq 5 工具端到端跑通) - W30: 3 字段补充 (notes / quotationType / quotationDemand) - W31 P1: 自动填 enterpriseId / userId (Prisma companyUserId 字段 + get_app_identity tool + lead.service.ts publishArgs) - W31: detail 详情页 + admin UI 集成管理 (OpenAppListView + AuditLogListView) 关联: ~/Documents/02-平台开发/产品开发/F002-W31-交接文档.md GIT-AUDIT-REPORT-2026-06-24.md
230 lines
13 KiB
Markdown
230 lines
13 KiB
Markdown
# AGENTS.md
|
||
|
||
语言:中文。所有对话、注释、文档、提交信息均使用中文。
|
||
|
||
## 项目结构
|
||
|
||
```
|
||
/
|
||
├── 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)
|
||
- 不在后端代码中硬编码脚本路径以外的文件路径
|