platform-assistant/AGENTS.md
Sisyphus 68f6fc5c16 feat(ui): 前端 UI 组件化重构 + 发布表单增强
【新组件】
- ErrorBoundary: 全局错误边界,渲染崩溃时显示重试/刷新按钮
- ui/Toast: 全局 Toast 通知系统 (success/error/warning/info),Context Provider 模式
- ui/Modal: 统一弹窗 (ESC关闭 + loading遮罩 + 自定义footer)
- ui/Skeleton: 骨架屏 (SkeletonTable / SkeletonCard)
- ui/Lightbox: 图片放大灯箱

【页面迁移】
- InquiryPage: useToast + Modal + SkeletonTable 替换手写代码,新增 pubForm
  发布前预填联系人/电话/地址 + 发布时传 body 给后端
- LeadDetailPage: 迁移到 Toast + Modal
- ProductPage: 迁移到 Toast + Modal + SkeletonTable
- AdminPage: 迁移到 useToast
- ComparisonPage: 迁移到 Toast + SkeletonTable
- ImagePage: 迁移到 Toast + SkeletonCard

【基础设施】
- globals.css: 新增 fadeIn/slideUp 动画
- page.tsx: SPA 根接入 ToastProvider + ErrorBoundary
- tailwind.config.ts: 新增 animate keyframes

【后端】
- integration.service.ts: MCP session 管理 + charset 修复
- lead.controller.ts/lead.service.ts: 发布端点增强 + 物料提取服务注入
2026-07-16 15:46:37 +08:00

27 KiB
Raw Permalink Blame History

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)

开发命令

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/招标公告/<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, 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 才够(否则全被 <think> 吃掉)。物料提取正文用 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 输出容易混入 <think>、让我分析、**加粗** 等解释文字
  • 关键词匹配已能覆盖新采集数据,老数据人工 review 更可靠

后续流程

  • 新采集数据:自动走关键词匹配(15 个初始品类 + 已入库的 ~25 个细分类)
  • AdminPage → 品类管理:用户手动 review AI 新建的细分类(合并到已有大类、删除不合理的)
  • 空品类历史线索:用户从详情面板手动指定,或忽略(过期数据)
  • 新增采集场景需要新关键词:编辑 ProductCategory.keywords 字段(AdminPage 或直接 SQL)

数据维护脚本

脚本 用途
scripts/backfill_category.js 批量回填空品类(已停止使用,仅供参考)
scripts/clean_category.js 清理 AI 输出脏值(<think>、**加粗**、 - 描述、解释行)
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/招标公告/<date>/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 实现

// 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<integrationId, sessionId>
  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:

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)
  • 不在后端代码中硬编码脚本路径以外的文件路径