- ProductCategory 表/CRUD/AI 分类/关键词反哺完整流程 - LLM 配置 (MiniMax-M2.7, env MINIMAX_API_KEY) - 重要决策:停止用 AI 回填历史空品类线索 (91% 已分配,剩余 68 条 + AI 新建 25 个细分类 交人工 review)
17 KiB
17 KiB
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) | 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)。导航通过onNavigateprop 传递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(envMINIMAX_MODEL可覆盖) - env:
MINIMAX_API_KEY写入apps/api/.env(生产已配) - 注意:M2.7 是推理模型,max_tokens 要 ≥500 才够(否则全被 吃掉)
- 生产
.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 格式不同)。其他网站只需:
- 在
scripts/tender-fetcher/放脚本(如fetch-xxx.py),输出~/desktop/招标公告/<date>/xxx_downloaded.json - 前端"采集器配置"新建记录,
type填bid_xxx,sourceName填来源显示名,scriptPath填脚本文件名 - 无需改代码,
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 已开
- 本地重部署流程:
npm run build重新编译 api/webrsync同步 dist/.next + 源码到/opt/pingtai/pm2 restart pingtai-api pingtai-web(注意pm2 restart默认不更新环境变量,要--update-env)- Prisma 变更后必须先
npx prisma migrate deploy && npx prisma generate(在apps/api/下)
- 常见操作:
pm2 list/pm2 logs/pm2 restart pingtai-apitail -f /var/log/pingtai/api.out-1.logsqlite3 /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_KEYin.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 对接配置(嵌入系统配置页面) |
使用流程
- 前端「运营管理 → 系统配置」页面顶部"MCP 对接配置"区域
- 点击「新增对接」,填入:名称、MCP Server 地址、APIKey、客户编码
- 点击「连接」验证 MCP server 可达并获取工具列表
- 发布询价时,自动通过 MCP 调用
publish_rfqtool
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-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)
- 不在后端代码中硬编码脚本路径以外的文件路径