# 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 版 `product_network_mcp`) | `ssh devbox` + PM2, **不在 Mac 本地** | | **8095** | devbox MCP server (Java 版 `industry-mcp-server` Docker) | `ssh devbox` + `docker ps` 看 `industry-mcp-server`, **不在 Mac 本地** | 注意:**3002 / 8095 都是 devbox 上的 MCP server 端口, 不是平台助手 web 端口**。 打开浏览器访问平台助手用 `http://localhost:3001/` (不是 3002/8095)。两套 MCP server 后端数据库不同, appId/appSecret 互不通用。 > ⚠️ 2026-07-15 实测: 平台助手本地 `next dev` 偶尔会 fallback 到 3000 端口(被 `admin_new` Vite 项目抢 3001 时),以实际启动后终端打印的端口为准。`AdminPage.tsx:1000` 默认填的 endpoint 仍是 `http://100.111.84.16:3002/mcp`(Node 版)。 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/招标公告//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` 切换 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, **ProductCategory**(品类字典,AI 关键词反哺用) ## 品类方案 C(关键词优先 + AI 反哺,2026-07-01) ### 数据模型 `ProductCategory` 表: - `name`:品类名(2-6 中文字,唯一) - `keywords`:JSON 数组,关键字列表 - `isAIGenerated`:AI 自动创建标记(人工 review 后可改 false) ### 分类逻辑 ``` 1. 采集器入库 / 手动回填时,对每条线索标题做: 关键词匹配(ProductCategory.keywords contains 标题)→ 命中直接写 ↓ 未命中 MiniMax API 分类 → 写品类 + 反写关键词到 ProductCategory 2. 同类标题下次直接走关键词,不再调 AI ``` ### 关键代码路径 | 文件 | 说明 | |------|------| | `apps/api/prisma/schema.prisma` | `ProductCategory` model | | `apps/api/src/product-category/` | CRUD + 关键词匹配 service | | `apps/api/src/product-category/category-inference.service.ts` | AI 推断 + 反写关键词 | | `apps/api/src/crawler/crawler.service.ts` | 采集入库用 `batchInfer()` | | `apps/api/src/crawler/notes-extractor.service.ts` | PDF/HTML 提取要点也走 AI | | `apps/web/src/components/InquiryPage.tsx` | 品类筛选项动态从 `/api/categories` 加载 | ### LLM 配置(DeepSeek → MiniMax 迁移) - endpoint:`https://api.minimaxi.com/v1/chat/completions` - model:`MiniMax-M2.7`(env `MINIMAX_MODEL` 可覆盖) - env:`MINIMAX_API_KEY` 写入 `apps/api/.env`(生产已配)。本地开发 `.env` 也需要配,否则物料提取/category-inference 走兜底 - **注意**:M2.7 是推理模型,max_tokens 要 ≥500 才够(否则全被 `` 吃掉)。物料提取正文用 2000,标题提取用 800 - 生产 `.env` 同时保留 `MINIMAX_API_KEY` 和 `MINIMAX_MODEL` 两个变量 ### 历史回填策略(重要:AI 不可全信) 2026-07-01 用 MiniMax 回填了 507 条历史空品类线索,结果: - ✅ 命中:693/761(91%)已分配品类 - ⚠️ AI 自动新建 ~25 个细分类,需要人工 review 合并/删除 - ❌ 68 条空品类线索(重要数据手动指定,非重要忽略) **决策(用户)**:停止继续调 AI 回填剩余数据。原因: - 单条 API 推理模型耗时大(500 tokens 给思考留空间) - AI 输出容易混入 ``、`让我分析`、`**加粗**` 等解释文字 - 关键词匹配已能覆盖新采集数据,老数据人工 review 更可靠 ### 后续流程 - **新采集数据**:自动走关键词匹配(15 个初始品类 + 已入库的 ~25 个细分类) - **AdminPage → 品类管理**:用户手动 review AI 新建的细分类(合并到已有大类、删除不合理的) - **空品类历史线索**:用户从详情面板手动指定,或忽略(过期数据) - **新增采集场景需要新关键词**:编辑 `ProductCategory.keywords` 字段(AdminPage 或直接 SQL) ### 数据维护脚本 | 脚本 | 用途 | |------|------| | `scripts/backfill_category.js` | 批量回填空品类(已停止使用,仅供参考) | | `scripts/clean_category.js` | 清理 AI 输出脏值(``、`**加粗**`、` - 描述`、解释行) | | `scripts/deploy.sh` | 部署时自动同步上述脚本到生产 | ## 采集器关键事实 - 调度基于 `@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/招标公告//xxx_downloaded.json` 2. 前端"采集器配置"新建记录,`type` 填 `bid_xxx`,`sourceName` 填来源显示名,`scriptPath` 填脚本文件名 3. 无需改代码,`runGenericScript()` 自动推断脚本和 JSON 文件路径 - `CrawlerConfig` 新增 `sourceName`(线索来源显示名)和 `scriptPath`(脚本文件名,可选,默认从 type 推断) ## 生产部署(腾讯云 172.81.241.109 / direv.cloud) > ⚠️ 注意:服务器 IP 以 `scripts/deploy.sh` 里的 `SSH_HOST` 为准(当前 172.81.241.109)。 > 历史曾用 124.222.182.242,已失效(SSH 22 端口超时)。如遇 IP 变更,改 deploy.sh 即可。 - **一键部署**:`bash scripts/deploy.sh`(build + rsync dist/.next/源码 + pm2 restart,密码内置于脚本) - **SSH**:`sshpass -p 'Jubianshuzi@2026' ssh ubuntu@172.81.241.109`(需本地装 sshpass) - **路径**:`/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/` 下) - ⚠️ **Web 构建注意**:本地 build 的 `.next/` 若直接 rsync 到生产,可能因 `node_modules` 版本不一致导致 chunk 加载失败。遇此情况,在生产服务器上执行 `cd /opt/pingtai/apps/web && npx next build` 重建。 - **常见操作**: - `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 | 通用化采集器调度 | | `MaterialExtractorService` | class | apps/api/src/lead/material-extractor.service.ts | **W37 新增** 发布时实时提取物料清单 (items),三级降级链 | | `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, POST 方法) ↓ x-app-id + x-app-secret (多租户 APIKey, 替代旧的 x-api-key) renren-mcp-server ├─ Node.js 版 (devbox:3002, product_network_mcp) └─ Java 版 (devbox:8095, industry-mcp-server Docker 容器) ↓ 直调 Java 业务方法 ↓ product_network 7 个 Java 服务 ``` 注: MCP server 端口 **3002(Node)和 8095(Java Docker)都是 devbox 开发机上的端口**, 不是平台助手本地端口 (3001/4000)。两套 MCP server 后端数据库不同, appId/appSecret 互不通用, 申请时确认走哪一套。 ### MCP 端点要求(重要!) 平台助手消费 MCP 时, 端点必须满足: - **协议**: Streamable HTTP (`@modelcontextprotocol/sdk` 的 `StreamableHTTPClientTransport`) - **方法**: **POST** (不是 GET). 如果 MCP server 实现要求 GET, 需要改 server 而不是 client - **认证头**: 多租户 APIKey → `x-app-id` + `x-app-secret`, 由 devbox B2B 运营后台"集成管理 → 应用列表"申请 - **路径示例**: `http://host:port/mcp` (注意末尾 `/mcp`) 前端 "MCP 对接配置 → 新增对接" 弹窗的 MCP 地址字段下方有提示文案提醒这些要求。 ### 设计原则 - **MCP 主通道**:17 个 tool(询价 5 + 商品 4 + 比价 4 + 推理 4) - **REST 兼容层**:5 个端点(健康/兑换/订阅/订单/撤销) - **APIKey 认证**:Header `x-app-id` + `x-app-secret`(多租户, 替代旧的 `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)+ **session 管理 + charset 修复** | | `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 + **可选 overrides 参数** | | `apps/api/src/lead/lead.controller.ts` | `POST /api/leads/:id/publish` 新增可选 body | | `apps/api/src/lead/material-extractor.service.ts` | **W37 新增** 物料清单提取(发布时实时调 MiniMax) | | `apps/api/src/lead/lead.module.ts` | 注册 `MaterialExtractorService` | | `apps/web/src/components/InquiryPage.tsx` | **发布前确认弹窗**(对接文档 §5.2) | | `apps/web/src/components/AdminPage.tsx` → `ConfigTab` | MCP 对接配置(嵌入系统配置页面)+ 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-app-id': appId, 'x-app-secret': appSecret }, }, // 自定义 fetch: 自动管理 mcp-session-id (MCP Streamable HTTP 协议要求) fetch: this.buildSessionAwareFetch(id), } ) await client.connect(transport) const tools = await client.listTools() await client.callTool({ name: 'publish_rfq', arguments: { title, ... } }) ``` ### MCP Session 管理(2026-07-15 实现) `integration.service.ts` 的 `buildSessionAwareFetch(integrationId)` 方法实现三层 session 管理: 1. **提取**:自定义 fetch wrapper 从 initialize 响应头 `mcp-session-id` 提取并缓存到 `mcpSessionIds: Map` 2. **注入**:后续所有请求自动在 header 中带 `mcp-session-id` 3. **清理**:disconnect / 重连时自动清空旧 session 双保险设计(SDK 1.27.0 内置 session 管理 + 自定义 wrapper fetch 兜底): | 路径 | 谁管理 session | |---|---| | connect() initialization | wrapper fetch 从响应头提取 | | listTools() / callTool() | wrapper fetch 注入 `mcp-session-id` header | | executeTool() 自动重连 | `clearSession(id)` 后再调用 `connect()` | | disconnect() | 调用 `clearSession(id)` | | 进程关闭 (onModuleDestroy) | `mcpSessionIds.clear()` | ### Content-Type charset 修复(2026-07-15) SDK 默认设 `content-type: application/json` 但不带 `charset=utf-8`。Java Jackson 解析器缺 charset 时可能不以 UTF-8 解码,导致多字节中文的字节计数与 Content-Length 不匹配,报 JSON 解析错误。 **修复**:在 `buildSessionAwareFetch` wrapper 中检测 Content-Type,自动补上 `charset=utf-8`: ```typescript const ct = headers.get('content-type') if (ct && !ct.includes('charset') && ct.includes('application/json')) { headers.set('content-type', 'application/json; charset=utf-8') } ``` ### 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) ### publish_rfq 字段映射(对接文档 4.1+4.2 节) `lead.service.ts` → `publishToDirev(id, overrides?)` 组装 `publishArgs` 后调 `executeTool('publish_rfq', args)`。 `overrides` 可选参数:`{ contactPhone?, contactName?, deliveryAddress? }`,由前端弹窗传入。 | publishArgs 字段 | lead 来源 / 默认值 | 文档要求 | 类型 | 备注 | |---|---|---|---|---| | title | `lead.title` | 询价标题 | string | 限 50 字,超长报错 | | items | MaterialExtractor 实时提取 | 物料清单行 | array | 可选,最多 20 行 | | deliveryAddress | `overrides?.deliveryAddress` → `lead.regionDetail` → `lead.region` | 收货地址 | string | 弹窗可编辑覆盖 | | deliveryDate | `deadline + 30 天` | 期望交货日期 | string YYYY-MM-DD | — | | validDays | `7` | 报价有效期 | integer | **W37 新增** | | contactName | `overrides?.contactName \|\| lead.contact \|\| '待补充'` | 联系人 | string | 弹窗可编辑 | | contactPhone | `overrides?.contactPhone \|\| lead.phone \|\| '待补充'` | 联系手机 | string | **必填,弹窗补全** | | inquiryParty | 硬编码 `郑州巨变网络科技有限公司` | 询价主体 | string | 企业配置 | | freightFlag | `1` | 运费承担 | integer | 1=含运费(曾传字符串"含运费"报错,已修复) | | procurementType | `1` | 采购类型 | integer | 1=公开采购(曾传字符串"定量采购"报错,已修复) | | quotationDemand | `1` | 报价要求 | integer | **W37 新增**,1=需要报价 | | invoiceType | `2` | 开票要求 | integer | 2=增值税专票(曾传字符串报错,已修复) | | paymentMethod | **不传** | 付款方式 | — | 后端用企业默认配置,传了"23"会报"企业字典找不到" | | userId / enterpriseId | `Integration.companyUserId` | 企业身份 | string | MCP connect 时获取 | ### 发布前确认弹窗(对接文档 §5.2,2026-07-16 实现) `InquiryPage.tsx` 中实现,用户勾选线索后点"一键发布"弹出: **弹窗布局**(三个区域): | 区域 | 数据来源 | 可编辑 | |---|---|---| | 询价信息 | 线索标题/品类/数量/单位 | ❌ 只读 | | 收货信息 | 联系人、联系手机、收货地址 | ✅ 用户可编辑 | | 业务默认值 | 企业硬编码字段值清单 | ❌ 只读 | **编辑字段**: | 字段 | 类型 | 验证 | 为空时 | |---|---|---|---| | contactName | 文本输入框 | 无 | 传 `'待补充'` | | contactPhone | 电话输入框 | 标红提示"⚠ 必填" | 传 `'待补充'` | | deliveryAddress | 文本输入框 | 标红提示"⚠ 必填" | 传线索地区数据 | **数据流**: ``` 前端弹窗 → DO_Publish() → POST /api/leads/:id/publish { contactName, contactPhone, deliveryAddress } → lead.service.ts publishToDirev(id, overrides) → 覆盖组装 publishArgs → executeTool('publish_rfq', publishArgs) → MCP server → 产业平台询价广场 ``` ### publish 端点变化(2026-07-16) `lead.controller.ts`: | 端点 | 方法 | Body | 改动 | |---|---|---|---| | `POST /api/leads/:id/publish` | POST | `{ contactPhone?, contactName?, deliveryAddress? }` | 新增可选 body 参数 | | `POST /api/leads/batch-publish` | POST | `{ ids: string[] }` | 未改(循环调 publish) | ### 当前已知卡点(对接文档 §8 + 2026-07-16 实测) | 卡点 | 状态 | 说明 | |---|---|---| | `code:10021 "未获取到用户信息"` | ⏳ 产业平台修复中 | MCP server 用户上下文问题,创建 APIKey 时已关联 `companyUserId`,后端应直接使用 | | `paymentMethod` 传 `"23"` 报"企业字典找不到" | ✅ 已规避 | 已修改:不传 paymentMethod,后端用默认值 | | JSON 截断 / Content-Length 不匹配 | ✅ 已修复 | 加 `charset=utf-8` 后解决 | | 字段值类型不匹配(字符串 vs 整数) | ✅ 已修复 | `freightFlag`, `procurementType`, `invoiceType` 改整数,`paymentMethod` 不传 | | `tb_inquiry.title` varchar(50) 超长 | ✅ 已加前置校验 | 发布前 `publishToDirev()` 检查 title 长度,超 50 字提前报错 | ### 数据质量现状(2026-07-16 生产数据统计) 815 条线索中,以下字段**全军覆没**(采集器只抓了列表页元数据): | 字段 | 填写率 | 说明 | |---|---|---| | deadline | 702/815 (86%) | 有 | | region | 696/815 (85%) | 有 | | regionDetail | 542/815 (66%) | 有 | | contact | 0/815 (0%) | **全缺** — 采集器未提取 | | phone | 0/815 (0%) | **全缺** | | quantity | 0/815 (0%) | **全缺** | | unit | 0/815 (0%) | **全缺** | | notes | 0/815 (0%) | **全缺** | **原因**:三个采集脚本(中电建/fetch-v1.2.js、新矿/fetch-xkgroup.py、中核/fetch-cnncecp.py)都只解析列表页,不访问详情页正文提取联系人/电话/数量/单位。这些字段需要新增"详情页内容提取"步骤,或依赖发布前弹窗让用户手动补全。 **当前应对**:发布前弹窗让用户手动填写 contactPhone 和 deliveryAddress。如需自动化提取,需在 crawler.service.ts 入库后增加类似 `notes-extractor` 的 AI 提取步骤(待实现)。 ## ANTI-PATTERNS - 前端不加路由库,保持 SPA useState 模式 - 不提交 secrets 到 git(JWT_SECRET 已在 .env.production,但 .gitignore 未排除) - 不删除旧采集器 state 文件(中电建用 `systemId_招采公告` 作 key) - 不依赖 Postgres/Redis(docker-compose.yml 是历史遗留,实际只用 SQLite) - 不在后端代码中硬编码脚本路径以外的文件路径