Quellcode durchsuchen

change(chat): 企业微信二维码改到「参考资料之前」

用户指定:二维码放在参考资料之前(原先放在整条回答最末尾)。
顺序变为 正文 → 卡片/综合说明 → 二维码 → 参考资料。

- flushContent 里把二维码块的 push 移到 refLinks 之前
- 新增 3 项顺序断言(构造带 source 的一轮,断言二维码下标 < 参考资料下标、
  二维码在正文之后、参考资料仍在最末尾)——适配层脚本 19 → 22/22
- api-chat.md 的 cipa 表、feature_list、README 同步

验证:build 通过;validate-harness 通过(21 项功能,in_progress 1 项)。

Co-Authored-By: Claude Code <noreply@anthropic.com>
gongtianxiao vor 13 Stunden
Ursprung
Commit
ca1a82e3ba

+ 1 - 1
README.md

@@ -279,7 +279,7 @@
   原先名称列 `max-width: 462px` + 部门列写死 `110px`,整行只有约 520px 宽、右侧空一大块,
   部门写全称时还被挤成三行;改为名称列占满剩余空间、部门列可伸展(下限仍 110px)
 - **CIPA / 出海类问答展示企业微信二维码**:后端在 `result.data.cipa` 给出布尔值,
-  `true`(问的是 CIPA / 海外服务平台 / 出海服务)时在回答**最末尾**追加一张二维码图片
+  `true`(问的是 CIPA / 海外服务平台 / 出海服务)时在**参考资料之前**追加一张二维码图片
   (`src/assets/qiyeweixin.png`,点击可放大)
   - 走适配层翻译成既有的 `<image-scope>` 内容标记,**渲染层零改动**(该标记本来就有对应行类型)
   - 复制按钮的剥离规则同步补上 `<image-scope>`,避免把图片地址复制进剪贴板

+ 1 - 1
harness/docs/reference/api-chat.md

@@ -169,7 +169,7 @@ data: {"thread_id":"window-20260914-001","request_id":"a78b17a3b38241fa83c8ee91f
 
 | 取值 | 含义 | 前端行为 |
 |---|---|---|
-| `true` | 问的是 **CIPA / 海外服务平台 / 出海服务** | 在回答**最末尾**追加一张企业微信二维码(`<image-scope>` 块,点击可放大) |
+| `true` | 问的是 **CIPA / 海外服务平台 / 出海服务** | 在**参考资料之前**追加一张企业微信二维码(`<image-scope>` 块,点击可放大) |
 | `false` / 缺失 | 其他情况 | 不展示 |
 
 - 前端按 **`=== true` 严格判定**,非布尔值一律当 false

+ 4 - 4
harness/feature_list.json

@@ -329,15 +329,15 @@
       "priority": 8,
       "area": "chat",
       "title": "CIPA / 出海类问答展示企业微信二维码(result.cipa)",
-      "user_visible_behavior": "当用户问的是 CIPA / 海外服务平台 / 出海服务时,回答的最末尾会多出一张企业微信二维码(点击可放大查看);其他问题不受影响。",
+      "user_visible_behavior": "当用户问的是 CIPA / 海外服务平台 / 出海服务时,回答里会多出一张企业微信二维码(位置在**参考资料之前**,点击可放大);其他问题不受影响。",
       "status": "in_progress",
       "verification": [
         "npm run build 通过;图片进产物(dist/assets/qiyeweixin-<hash>.png),标记字符串在 bundle 里",
-        "harness/tools/verify-company-info-passthrough.mjs —— 用**假 SSE 流喂真实协调器**,新增 9 项断言:cipa=true → 追加 `<image-scope>` 块且含非空地址、块在**内容最末尾**、正文不被污染;cipa=false / 字段缺失 / 非布尔值(\"true\" 字符串)→ 都不加;**跨轮不串**(第一轮 true、第二轮缺省 → 第二轮没有)",
+        "harness/tools/verify-company-info-passthrough.mjs —— 用**假 SSE 流喂真实协调器**,新增 12 项断言:cipa=true → 追加 `<image-scope>` 块且含非空地址、**位置在参考资料之前**(断言了顺序:正文 → 二维码 → 参考资料)、参考资料仍在最末尾、正文不被污染;cipa=false / 字段缺失 / 非布尔值(\"true\" 字符串)→ 都不加;**跨轮不串**(第一轮 true、第二轮缺省 → 第二轮没有)",
         "回归:verify-coordinator-multi-instance 11/11、verify-policy-card-fields 8/8、verify-chat-task-utils 33/33(三者都依赖本次重新打包的 _coordinator.mjs)"
       ],
-      "evidence": "协调器层 9 项新断言通过 + 三个依赖脚本回归全绿 + 构建通过(图片已进产物)。⚠️ **真实后端尚未下发该字段**:2026-09-20 实测两份 CIPA/出海相关问答,result.data 的键仍是 response/company_info/recommendation/errors(无 cipa)。前端对缺字段安全(不显示),**待后端部署后即可生效**;届时需在浏览器确认一次。",
-      "notes": "实现(协议适配层,渲染层零改动):\n- 协调器 `case 'result'` 捕获 `cipa === true`(严格判定)→ `pendingCipa`;`resetTurnContent()` 每轮清空;\n  `flushContent()` 里在**参考资料之后**追加 `<image-scope>\n{url}\n</image-scope>`\n- 图片走 Vite 资源导入 `import cipaQrcodeUrl from '@/assets/qiyeweixin.png'`(构建后是带 hash 的产物地址),\n  没有新增组件:渲染层本来就认 `<image-scope>` 块 → `ImageContent` 行类型(支持点击放大)\n- 复制按钮的剥离规则补了 `<image-scope>`,避免把图片地址复制进剪贴板\n\n⚠️ **打包注意**:协调器现在 import 了 .png,node 侧的 esbuild 必须带 `--loader:.png=dataurl`\n(否则直接报错)——命令已写进 `harness/tools/README.md` 与 `_entry-coordinator.ts` 头部注释。\n\n⚠️ 待办:①后端部署该字段后,在浏览器问一句 CIPA/出海类问题确认二维码出现、其他问题不出现\n②确认后再把本条转 passing。"
+      "evidence": "协调器层 12 项新断言通过(含顺序断言) + 三个依赖脚本回归全绿 + 构建通过(图片已进产物)。⚠️ **真实后端尚未下发该字段**:2026-09-20 实测两份 CIPA/出海相关问答,result.data 的键仍是 response/company_info/recommendation/errors(无 cipa)。前端对缺字段安全(不显示),**待后端部署后即可生效**;届时需在浏览器确认一次。",
+      "notes": "实现(协议适配层,渲染层零改动):\n- 协调器 `case 'result'` 捕获 `cipa === true`(严格判定)→ `pendingCipa`;`resetTurnContent()` 每轮清空;\n  `flushContent()` 里在**参考资料之前**追加 `<image-scope>\n{url}\n</image-scope>`(位置由用户 2026-09-20 指定;断言了 正文 → 二维码 → 参考资料 的顺序)\n- 图片走 Vite 资源导入 `import cipaQrcodeUrl from '@/assets/qiyeweixin.png'`(构建后是带 hash 的产物地址),\n  没有新增组件:渲染层本来就认 `<image-scope>` 块 → `ImageContent` 行类型(支持点击放大)\n- 复制按钮的剥离规则补了 `<image-scope>`,避免把图片地址复制进剪贴板\n\n⚠️ **打包注意**:协调器现在 import 了 .png,node 侧的 esbuild 必须带 `--loader:.png=dataurl`\n(否则直接报错)——命令已写进 `harness/tools/README.md` 与 `_entry-coordinator.ts` 头部注释。\n\n⚠️ 待办:①后端部署该字段后,在浏览器问一句 CIPA/出海类问题确认二维码出现、其他问题不出现\n②确认后再把本条转 passing。\n\n📌 **2026-09-20 位置调整**:最初放在整条回答最末尾,用户要求改到**参考资料之前**(一行顺序调整 + 一条顺序断言)。"
     }
   ]
 }

+ 9 - 0
harness/progress.md

@@ -2879,3 +2879,12 @@
   - ⚠️ 图片放在 `src/assets/`(随构建产物发布);若将来要换图,替换文件即可(文件名不用改)
   - ⚠️ 二维码出现在**回答最末尾**(参考资料之后)——位置是我选的,若用户希望贴在别处(如正文下方)说一声
 - **下一步最佳动作**:等后端部署 `cipa` → 浏览器实测 → 转 passing
+
+### 续:二维码位置调整(用户指定)
+
+用户要求「**二维码放在参考资料之前**」(原先放在整条回答最末尾)。
+
+- 代码:`flushContent()` 里把二维码块的 push 移到 refLinks **之前**(顺序变为 正文 → 卡片/综合说明 → **二维码** → 参考资料)
+- 断言:新增 3 项**顺序断言**(构造一轮带 source 的问答,断言 `二维码下标 < 参考资料下标`、二维码在正文之后、参考资料仍在最末尾)——
+  适配层脚本 19 → **22/22 通过**
+- 文档同步:`api-chat.md` 的 cipa 表、`feature_list.json`、根 `README.md` 的 20260920 节

+ 17 - 1
harness/tools/verify-company-info-passthrough.mjs

@@ -162,9 +162,25 @@ console.log('\n【5】result.cipa → 企业微信二维码块(<image-scope>
   const text = seen_message_text(shown);
   check('cipa=true → 追加 <image-scope> 块', /<image-scope>[\s\S]*?<\/image-scope>/.test(text), text.slice(-120));
   check('块里有非空的图片地址', /<image-scope>\s*\S[^\n]*\s*<\/image-scope>/.test(text));
-  check('图片块在整条内容的**最末尾**(正文之后)', text.trimEnd().endsWith('</image-scope>'));
   check('正文本身没被污染(正文仍在最前面)', text.trimStart().startsWith('这是回答正文。'), text.slice(0, 40));
 
+  // 位置:正文 → 二维码 → 参考资料(用户 2026-09-20 指定「二维码放在参考资料之前」)
+  const withSources = await runTurn([
+    ...BASE_EVENTS,
+    // 参考资料块要求来源**有链接**(见 buildRefLinksContent 的 filter)
+    ['source', { id: 's1', title: '测试政策', type: 'policy', source: { url: 'https://example.com/p1' } }],
+    ['result', { response: '这是回答正文。', cipa: true }],
+    ['done', { status: 'completed' }],
+  ]);
+  const ordered = seen_message_text(withSources);
+  const qrAt = ordered.indexOf('<image-scope>');
+  const refAt = ordered.indexOf('<ref_links');
+  const bodyAt = ordered.indexOf('这是回答正文。');
+  check('这一轮确实有参考资料块(否则位置断言无意义)', refAt > -1, ordered.slice(-160));
+  check('二维码在**参考资料之前**', qrAt > -1 && refAt > -1 && qrAt < refAt, { qrAt, refAt });
+  check('二维码在正文之后', bodyAt > -1 && qrAt > bodyAt, { bodyAt, qrAt });
+  check('参考资料仍排在整条内容的最末尾', ordered.trimEnd().endsWith('</ref_links>'));
+
   const off = await runTurn([
     ...BASE_EVENTS,
     ['result', { response: '这是回答正文。', cipa: false }],

+ 10 - 8
src/components/api-chat-coordinator.ts

@@ -12,8 +12,9 @@
  * - item                 → <!-- POLICY_TABLE {json} POLICY_TABLE -->(原政策卡片 + 原详情面板)
  * - source               → <ref_links>[…]</ref_links>(原“参考资料”)
  * - interrupt            → <question-cards>{…}</question-cards>(原选项卡)
- * - result.cipa(布尔)  → <image-scope>…</image-scope>(企业微信二维码,追加在最后;
- *                          仅当后端判定问的是 CIPA / 海外服务平台 / 出海服务时为 true)
+ * - result.cipa(布尔)  → <image-scope>…</image-scope>(企业微信二维码,
+ *                          位置在**参考资料之前**;仅当后端判定问的是 CIPA /
+ *                          海外服务平台 / 出海服务时为 true)
  * - answer / summary     → 纯文本
  *
  * 正文与卡片在 done 时按原顺序一次性输出:新接口本身不逐字流式返回,
@@ -1102,17 +1103,18 @@ export class ApiChatCoordinator {
       parts.push(buildQuestionCardsContent(this.pendingInterrupt, this.answerText));
     }
 
-    // 参考资料放在最后:它是引用来源,应该排在正文与卡片之后
+    // 企业微信二维码:后端标记为 CIPA / 出海类问题时追加。
+    // 位置在**参考资料之前**(用户 2026-09-20 指定)——正文/卡片之后、参考资料之前
+    if (this.pendingCipa) {
+      parts.push(buildCipaQrcodeContent());
+    }
+
+    // 参考资料放在最后:它是引用来源,应该排在正文、卡片与二维码之后
     const refLinks = buildRefLinksContent(this.pendingSources);
     if (refLinks) {
       parts.push(refLinks);
     }
 
-    // 企业微信二维码:后端标记为 CIPA / 出海类问题时追加(放在整条回答的最末尾)
-    if (this.pendingCipa) {
-      parts.push(buildCipaQrcodeContent());
-    }
-
     const content = parts.filter((part) => String(part || '').trim().length > 0).join('\n\n');
     if (!content) {
       return;