- 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
13 KiB
13 KiB
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)
开发命令
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
采集器关键事实
- 调度基于
@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)
- 不在后端代码中硬编码脚本路径以外的文件路径