platform-assistant/docs/devbox-requirements/MCP-publish_rfq-字段补充需求.md
Sisyphus afac18e843 feat(F002): W26 + W29.x + W30 + W31 平台助手全部改动首次 commit
- 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
2026-06-24 11:07:11 +08:00

360 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 需求:F002 MCP publish_rfq 第 2 轮字段补充(基于老 direv-publish 脚本对比)
**提出方**:平台助手(nest-api)
**接收方**:devbox 产品网络(renren-supplier-server + mcp-server)
**优先级**:P1
**时间**:2026-06-22
## 一、背景
W29.7 已加 11 个商务字段,但跟老 GUI 流程(`direv-publish` skill)对比,仍缺关键信息,导致发布的询价前端展示质量低。
参考老 GUI 脚本:
- `/Users/zou/Downloads/巨变自动招标公告发布/direv-publish/batch_publish_v4.py`(带备注 + PDF 上传的最新版)
- `/Users/zou/Downloads/巨变自动招标公告发布/direv-publish/extract_notes.py`(PDF → 6 条要点提取)
- `/Users/zou/Downloads/巨变自动招标公告发布/direv-publish/SKILL.md`(15 字段表单映射)
## 二、老 GUI 表单 vs 当前 MCP 字段对比
### 2.1 老 GUI 必填字段(SKILL.md §表单字段映射)
| 序号 | direv 表单字段 | 来源 | 当前 MCP 状态 |
|------|--------------|------|--------------|
| 1 | 询价单标题 | `lead.title` | ✅ title |
| 2 | 询价主体 | 固定:郑州巨变网络科技有限公司 | ✅ inquiryParty |
| 3 | 询价截止日期 | `lead.deadline` | ✅ deadline |
| 4 | 采购类型 | 固定:定量采购 | ✅ procurementType |
| 5 | 采购联系人 | 固定:郑钊 | ⚠️ 平台助手未传 |
| 6 | 联系方式 | 固定:15532027557 | ⚠️ 平台助手未传 |
| 7 | 报价要求 | 固定:报价包含运费 | ✅ freightFlag |
| 8 | 付款方式 | 固定:货到付款 | ✅ paymentMethod |
| 9 | 要求交货日期 | `lead.delivery` | ✅ deliveryDate |
| 10 | 开票要求 | 固定:增值税专票 | ✅ invoiceType |
| 11 | 收货地址 | `lead.region` / `regionDetail` | ✅ shippingAddress |
| 12 | 商品名称 | `lead.category` | ❌ 完全缺 |
| 13 | 规格型号 | `lead.spec`(可选) | ❌ 完全缺 |
| 14 | 单位 | `lead.unit` | ❌ 完全缺 |
| 15 | 数量 | `lead.quantity` | ✅ qty |
| 16 | 报价方式 | 固定:公开报价 | ⚠️ hardcode quotationType=1(接口未暴露) |
| 17 | **备注** | **DeepSeek 提取的 6 条要点** | ❌ **完全缺,最关键** |
| 18 | **PDF 上传** | **公告 PDF** | ❌ 完全缺 |
| 提交 | 发起询价 | — | ✅ |
### 2.2 关键缺漏(按影响优先级)
#### ❌ 缺漏 1:**备注(note 字段)** —— 最严重
- **老 GUI**:从 PDF/HTML 用 DeepSeek 提取 6 条要点(项目背景/付款/交付/包装/特殊要求/保证金),填到 tb_inquiry.note
- **当前 MCP**:description 字段接的是 `lead.summary`,但 `lead.summary` 实际只是 `"来源: 电建\n公告ID: xxx\n链接: xxx"`(元数据,不是商务要点)
- **结果**:tb_inquiry.note 里只是 "来源: 电建...",前端招采广场显示无商务信息
- **影响**:买家看不到付款条件、交付要求、保证金等关键信息,无法判断是否投标
#### ❌ 缺漏 2:商品明细(category/spec/unit)
- **老 GUI**:商品名称 + 规格型号 + 单位 + 数量 是询价单的核心,单独一行展示
- **当前 MCP**:只有 qty(数量),category/spec/unit 都没字段
- **tb_inquiry 表**:没有 category/spec/unit 字段(这些在 tb_inquiry_detail 表,单询价单 - 多商品模型)
- **影响**:发布的询价没有商品分类信息,买家不知道买的什么
#### ⚠️ 缺漏 3:enterpriseId / userId 平台助手未传
- **当前**:publishToDirev 函数 hardcode 不传 enterpriseId,导致 paymentMethod 字典查询不到
- **结果**:tb_inquiry.payment_method = NULL(虽然 schema 加了字段但实际没填)
- **影响**:前端询价详情看不到付款方式("货到付款" 等)
#### ⚠️ 缺漏 4:contactName/contactPhone 平台助手偶尔不传
- **当前**:publishToDirev 函数只在 lead.contact/lead.phone 有值时才传
- **问题**:老 GUI 固定传"郑钊/15532027557",MCP 经常 NULL
- **影响**:前端询价详情看不到联系人
#### ❌ 缺漏 5:PDF 上传
- **老 GUI**:电建公告 PDF 上传到备注区
- **当前 MCP**:attachments 只支持 URL 列表,不支持二进制上传
- **影响**:买家看不到原 PDF 公告(但 description 里有原始链接,可点)
### 2.3 tb_inquiry 表字段映射(devbox 已有)
| 表字段 | 老 GUI | 当前 MCP | 备注 |
|--------|--------|----------|------|
| title | ✓ | ✓ title | |
| note | ✓ 备注 | ❌ 没暴露 | **5000 字容量,老 GUI 填 6 条要点** |
| inquiry_party | ✓ 固定 | ✓ inquiryParty | |
| contact_name | ✓ 固定 | ⚠️ 平台助手偶尔不传 | |
| contact_phone | ✓ 固定 | ⚠️ 平台助手偶尔不传 | |
| shipping_address | ✓ region | ✓ shippingAddress | |
| inquiry_valid_date | ✓ deadline | ✓ deadline | |
| deliver_date | ✓ delivery (auto+30) | ✓ deliveryDate | |
| purchase_quantity | ✓ quantity | ✓ qty | |
| urls | ✓ PDF/HTML URL | ✓ attachments | |
| image_names | ✓ 封面图 | ❌ MCP 不支持图片生成 | |
| procurement_type | ✓ 定量 | ✓ procurementType | |
| freight_flag | ✓ 含运费 | ✓ freightFlag | |
| invoice_type | ✓ 增值税专票 | ✓ invoiceType | |
| payment_method | ✓ 货到付款 | ⚠️ 平台助手未传 enterpriseId 字典查不到 | |
| quotation_demand | ✓ 允许全部 | ❌ 没暴露(hardcode 2) | |
| quotation_type | ✓ 公开报价 | ❌ 没暴露(hardcode 1) | |
| appr_status | — 免审核 | ✅ 默认 2 (W29.8) | |
| name / mobile | — | ❌ 不在本轮范围 | 老需求文档 §五 |
| province_id / city_id / area_id | ✓ 地址编码 | ❌ MCP 完全没字段 | 老 GUI 用前端 JS 选 |
## 三、需求
### 3.1 【P0】MCP `publish_rfq` schema 加 2 字段
在 `product_network_mcp/src/tools/inquiry.ts` 的 `publish_rfq` zod schema 里增加:
```typescript
{
// ===== W29.7 已有字段 =====
// ... (保持不变)
// ===== W29.10 新增字段 =====
/** 备注:从公告 PDF/HTML 用 LLM 提取的 6 条商务要点
* 例: "- 项目背景: ...\n- 付款周期: ...\n- 交付要求: ...\n- 包装要求: ...\n- 特殊技术要求: ...\n- 保证金: ..." */
notes: z.string().optional().describe("备注 (LLM 提取的 6 条商务要点摘要, 填到 tb_inquiry.note)"),
/** 询价类型 (1=公开/2=邀请), 老 GUI 固定传 1 公开报价 */
quotationType: z.number().int().min(1).max(2).optional().describe("报价方式: 1=公开报价 2=邀请报价 (默认 1)"),
/** 报价要求 (1=必须全部/2=允许全部), 老 GUI 固定传 2 */
quotationDemand: z.number().int().min(1).max(2).optional().describe("报价要求: 1=必须全部报价 2=允许全部报价 (默认 2)"),
/** 询价单位 (id), 来自 sys_user 表里 type=99 的公司账号 */
userId: z.number().optional().describe("代发用户 ID (F002 公司账号对应 sys_user.id, 可选)"),
// 已有但平台助手没传 — 强提醒 platform-assistant 必传
}
```
### 3.2 【P0】Java 后端 `InquiryServiceImpl.publish()` 升级
`renren-supplier-server/src/main/java/io/renren/service/impl/InquiryServiceImpl.java`
**新增字段映射**:
```java
// 备注 (W29.10) - 优先级高于 description
String notes = (String) params.get("notes");
if (StrUtil.isNotBlank(notes)) {
inquiryEntity.setNote(notes);
} else {
inquiryEntity.setNote((String) params.get("description"));
}
// 报价方式 (1=公开/2=邀请)
Object quotationTypeObj = params.get("quotationType");
if (quotationTypeObj != null) {
inquiryEntity.setQuotationType(Integer.valueOf(quotationTypeObj.toString()));
}
// 报价要求 (1=必须全部/2=允许全部)
Object quotationDemandObj = params.get("quotationDemand");
if (quotationDemandObj != null) {
inquiryEntity.setQuotationDemand(quotationDemandObj.toString());
} else {
inquiryEntity.setQuotationDemand("2"); // 默认允许全部
}
// userId - 从 customerCode 关联的 sys_user 表查 (type=99 公司账号)
Long userId = null;
Object userIdObj = params.get("userId");
if (userIdObj != null) {
userId = Long.valueOf(userIdObj.toString());
}
if (userId == null && inquiryEntity.getInquiryEnterpriseId() != null) {
// 自动查 sys_user 表, 取 enterprise_id 对应的公司账号 (type=99)
userId = findCompanyUserId(inquiryEntity.getInquiryEnterpriseId());
}
if (userId != null) {
inquiryEntity.setUserId(userId);
}
```
**新增辅助方法 `findCompanyUserId(enterpriseId)`**:
```java
/**
* 根据企业 ID 查询公司账号用户 ID (sys_user 表 type=99)
* 这是 F002 公司账号代发模式
*/
private Long findCompanyUserId(Long enterpriseId) {
try {
List<SysUserEntity> users = sysUserService.list(
Wrappers.<SysUserEntity>lambdaQuery()
.eq(SysUserEntity::getEnterpriseId, enterpriseId)
.eq(SysUserEntity::getUserType, 99) // 99=公司账号
.last("LIMIT 1")
);
if (!users.isEmpty()) {
return users.get(0).getId();
}
} catch (Exception e) {
// log
}
return null;
}
```
### 3.3 【P1】平台助手 Lead 模型加 `notes` 字段(LLM 提取结果)
`apps/api/prisma/schema.prisma`:
```prisma
model Lead {
// ... 现有字段
/// 备注:DeepSeek 从公告 PDF/HTML 提取的 6 条商务要点
/// 格式: "- 项目背景: ...\n- 付款周期: ...\n- 交付要求: ...\n- 包装要求: ...\n- 特殊技术要求: ...\n- 保证金: ..."
notes String?
}
```
执行 `npx prisma db push` 迁移(SQLite,加列无破坏)。
### 3.4 【P1】平台助手 `crawler.service.ts` 抓取时 LLM 提取
在抓取公告保存到 Lead 时,如果附件是 PDF 或 HTML,用 DeepSeek 提取商务要点填到 `lead.notes`:
```typescript
// 现有抓取逻辑 (line 130-148)
const lead = await this.prisma.lead.create({
data: {
title: r.title,
summary: `来源: ${r.source}\n公告ID: ${r.noticeId}\n链接: ${r.detailUrl}`,
// ===== 新增 =====
notes: await this.extractBusinessNotes(r.detailUrl, r.source),
// ===== 现有其他字段 =====
...
}
}
/**
* 提取商务要点(参考 direv-publish/extract_notes.py)
*/
private async extractBusinessNotes(detailUrl: string, source: string): Promise<string | null> {
try {
const content = await this.downloadAndParse(detailUrl, source);
if (!content || content.length < 200) return null;
// 调用 DeepSeek (复用 auth-profiles.json 配置)
const deepseekKey = this.getLLMKey('deepseek');
if (!deepseekKey) return null;
const prompt = `你是招标公告信息提取助手。请从以下公告内容提取关键商业要求,整理成简明要点。
排除项(不要出现):公司简介、联系人姓名/电话/邮箱、地址信息、招标人联系方式
保留项(有则提取):项目背景、付款周期/方式/条件、交付周期/时间、包装要求、快递/运输费用、特殊技术要求、资质要求、保证金要求、其他特殊要求
输出格式:纯要点,每条一行以 "- " 开头,最多 6 条,每条 20 字以内。不要标题、分类头、解释。
公告内容:
${content.slice(0, 4000)}`;
const res = await axios.post('https://api.deepseek.com/v1/chat/completions', {
model: 'deepseek-chat',
messages: [
{ role: 'system', content: '你是招标公告信息提取助手。' },
{ role: 'user', content: prompt }
],
temperature: 0.1,
max_tokens: 500
}, {
headers: { 'Authorization': `Bearer ${deepseekKey}` }
});
return res.data.choices?.[0]?.message?.content?.trim() || null;
} catch (e) {
this.logger.warn(`提取商务要点失败: ${e.message}`);
return null;
}
}
```
### 3.5 【P1】平台助手 `publishToDirev()` 改:传 `notes` + `enterpriseId`
`apps/api/src/lead/lead.service.ts` 第 215-290 行:
```typescript
// 改 1: 优先用 lead.notes (W29.10 新字段)
publishArgs.notes = lead.notes || lead.summary || lead.title || '';
// 改 2: 必传 enterpriseId (MCP 才能查字典)
if (lead.organization) {
// lead.organization 可能存的是企业 ID 或名称
// 如果是 ID 直接用, 是名称需要查表
publishArgs.enterpriseId = await this.getEnterpriseIdByName(lead.organization);
}
// 改 3: 必传 userId (用默认公司账号)
publishArgs.userId = await this.getDefaultCompanyUserId(publishArgs.enterpriseId);
```
### 3.6 【P2】前端 `AdminPage.tsx` 表单字段补全
如果 ConfigTab MCP 对接配置需要让用户填写 enterpriseId/userId,补到表单默认值。但 W29.7 已经加了这些字段,前端也支持输入,主要是平台助手 publish 链路默认要传。
## 四、验收标准
升级后,平台助手发布一条电建/新矿询价,在 B2B 平台 PC 招采广场应能看到以下字段都正确填充:
| 字段 | 期望 | 当前 |
|------|------|------|
| ✅ 标题 | 项目名 | OK |
| ✅ 询价主体 | 郑州巨变网络科技有限公司 | OK |
| ✅ 询价截止日期 | YYYY-MM-DD | OK |
| ✅ 要求交货日期 | YYYY-MM-DD | OK |
| ✅ 收货地址 | 详细地址 | OK |
| ✅ 联系人/电话 | 郑钊 / 15532027557 | **部分 OK** |
| ✅ 付款方式 | 货到付款 (dict id) | **缺失(无 enterpriseId)** |
| ✅ 发票类型 | 增值税专票 | OK |
| ✅ 运费 | 含运费 | OK |
| 🆕 **备注** | **6 条商务要点摘要** | **缺失(只有元数据)** |
| 🆕 **商品分类/规格/单位** | **品类 / 规格 / 件** | **完全缺失** |
| ✅ 询价状态 | 长期有效 / 进行中 | OK (W29.8) |
## 五、不在本轮范围
- ❌ PDF 二进制文件上传(老 GUI 用 Playwright 上传,MCP 协议层支持有限,保留 attachments URL)
- ❌ 商品封面图 AI 生成(MiniMax image-01,MCP 无对应机制)
- ❌ 多商品(tb_inquiry_detail 表 1:N,老 GUI 单条 publish 也只发 1 商品)
- ❌ 省市区编码(province_id/city_id/area_id,老 GUI 用前端 JS 选,区域单独字段)
- ❌ 询价人姓名/电话(name/mobile,老需求文档 §五 不在范围)
## 六、测试用例
升级后请用以下 payload 测试:
```json
{
"title": "九峰山抽水蓄能电站工字钢槽钢镀锌扁铁采购",
"notes": "- 项目背景: 九峰山抽水蓄能电站\n- 付款: 货到付款\n- 交付: 30 天\n- 包装: 标准捆扎\n- 资质: 钢材生产许可证\n- 保证金: 5%",
"qty": 335,
"deadline": "2026-05-26",
"deliveryDate": "2026-06-25",
"shippingAddress": "河南省新乡市辉县市九峰山抽水蓄能电站",
"contactName": "郑钊",
"contactPhone": "15532027557",
"freightFlag": "含运费",
"paymentMethod": "货到付款",
"invoiceType": "增值税专票",
"procurementType": "定量采购",
"inquiryParty": "郑州巨变网络科技有限公司",
"userId": 1067246875800000001,
"enterpriseId": 23,
"enterpriseName": "郑州巨变网络科技有限公司",
"attachments": ["https://example.com/notice.pdf"],
"quotationType": 1,
"quotationDemand": 2
}
```
调 `list_my_rfqs` 验证上述字段(特别是 `notes` / `paymentMethod` / `quotationType` / `quotationDemand`)都被持久化。
## 七、devbox 已就绪(待平台助手升级)
- ✅ `notes` 字段 MCP schema 已加(待 §3.1)
- ✅ `quotationType` / `quotationDemand` 字段 MCP schema 已加
- ✅ `userId` 字段已存在
- ✅ `enterpriseId` 自动查字典(待 §3.2 findCompanyUserId 实现)
平台助手端需要做的:
1. 部署文档(此文档)给 MCP server 维护者
2. 按 §3.3-3.5 改 Lead 模型 + crawler.service + publishToDirev
3. LLM 提取商务要点(参考 §3.4)