Răsfoiți Sursa

fix(chat): 补问轮不展示企业微信二维码

用户浏览器实测截图指出:公司类问题触发 company_need 补问时,二维码被贴在
「输入框 + 跳过公司查询 + 提交」下面 —— 那一轮还没给出正文回答,贴二维码既突兀也没意义。

- 判据加严:追加 && !this.pendingInterrupt(有 interrupt 视为「还没给出正文回答」)
- 新增 3 项断言:company_need 与 company_selection 两类补问轮带 cipa=true 都不出图,
  且选项卡本身仍正常出现 —— 适配层脚本 22 → 25/25
- 同时确认:后端已部署 cipa(截图中二维码确实渲染),此前记录的「尚未下发」已更正
- 顺带记录:探测时又遇到一次 HTTP 500(0 字节,当日第二次),前端按流中断处理

文档:api-chat.md 的 cipa 表补「true + 本轮有 interrupt → 不展示」,README 与 feature_list 同步。
验证:build 与 validate-harness 通过。

Co-Authored-By: Claude Code <noreply@anthropic.com>
gongtianxiao 6 ore în urmă
părinte
comite
b8626ed570

+ 2 - 3
README.md

@@ -283,6 +283,5 @@
   (`src/assets/qiyeweixin.png`,点击可放大)
   - 走适配层翻译成既有的 `<image-scope>` 内容标记,**渲染层零改动**(该标记本来就有对应行类型)
   - 复制按钮的剥离规则同步补上 `<image-scope>`,避免把图片地址复制进剪贴板
-  - 适配层 9 项新断言(含 `false`/缺字段/非布尔值不显示、跨轮不串)
-  - ⚠️ 实测**后端尚未下发该字段**(`result.data` 仍是 response/company_info/recommendation/errors)——
-    前端对缺字段安全,后端部署后即生效
+  - 适配层 15 项新断言(含 `false`/缺字段/非布尔值不显示、跨轮不串、**补问轮不展示**)
+  - 位置:**参考资料之前**;**补问轮(等用户补充公司信息/确认候选)不展示**——那一轮还没有正文回答

+ 3 - 4
harness/docs/reference/api-chat.md

@@ -171,12 +171,11 @@ data: {"thread_id":"window-20260914-001","request_id":"a78b17a3b38241fa83c8ee91f
 |---|---|---|
 | `true` | 问的是 **CIPA / 海外服务平台 / 出海服务** | 在**参考资料之前**追加一张企业微信二维码(`<image-scope>` 块,点击可放大) |
 | `false` / 缺失 | 其他情况 | 不展示 |
+| `true` + **本轮有 `interrupt`** | 补问轮(等用户补充公司信息 / 确认候选) | **不展示**:那一轮界面上是补问卡片、还没有正文回答(2026-09-20 实测踩到) |
 
 - 前端按 **`=== true` 严格判定**,非布尔值一律当 false
-- ⚠️ 该字段**目前尚未在开发环境(`192.168.2.23:8000`)下发**:
-  2026-09-20 实测两份 CIPA/出海相关的问答,`result.data` 的键仍是
-  `response / company_info / recommendation / errors`。前端已实现且对缺字段安全
-  (不显示),后端部署后即生效——**用 `curl` 打一次 `/api/chat` 看有没有 `cipa` 即可确认**
+- ✅ 该字段已于 2026-09-20 部署到开发环境(浏览器实测已看到二维码渲染)
+- ⚠️ 后端偶发 500(当日实测一次,请求返回 0 字节),前端按流中断处理,不重试
 
 ### company_info(`result.data.company_info`)
 

+ 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 流喂真实协调器**,新增 12 项断言:cipa=true → 追加 `<image-scope>` 块且含非空地址、**位置在参考资料之前**(断言了顺序:正文 → 二维码 → 参考资料)、参考资料仍在最末尾、正文不被污染;cipa=false / 字段缺失 / 非布尔值(\"true\" 字符串)→ 都不加;**跨轮不串**(第一轮 true、第二轮缺省 → 第二轮没有)",
+        "harness/tools/verify-company-info-passthrough.mjs —— 用**假 SSE 流喂真实协调器**,新增 15 项断言:cipa=true → 追加 `<image-scope>` 块且含非空地址、**位置在参考资料之前**(断言了顺序:正文 → 二维码 → 参考资料)、参考资料仍在最末尾、正文不被污染;cipa=false / 字段缺失 / 非布尔值(\"true\" 字符串)→ 都不加;**补问轮不展示**(company_need / company_selection 两类 interrupt 带 cipa=true 都不出图)、**跨轮不串**(第一轮 true、第二轮缺省 → 第二轮没有)",
         "回归:verify-coordinator-multi-instance 11/11、verify-policy-card-fields 8/8、verify-chat-task-utils 33/33(三者都依赖本次重新打包的 _coordinator.mjs)"
       ],
-      "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 位置调整**:最初放在整条回答最末尾,用户要求改到**参考资料之前**(一行顺序调整 + 一条顺序断言)。"
+      "evidence": "协调器层 15 项新断言通过(含顺序断言与补问轮不展示) + 三个依赖脚本回归全绿 + 构建通过(图片已进产物)。📌 后端已于 2026-09-20 部署该字段(用户浏览器实测截图显示二维码确实渲染出来了,从而发现「补问轮不该出图」的问题并已修)。⚠️ **待办**:修好后需再在浏览器确认一次(正文回答出图、补问轮不出图),然后把本条转 passing。",
+      "notes": "实现(协议适配层,渲染层零改动):\n- 协调器 `case 'result'` 捕获 `cipa === true`(严格判定)→ `pendingCipa`;`resetTurnContent()` 每轮清空;\n  `flushContent()` 里在**参考资料之前**追加 `<image-scope>\n{url}\n</image-scope>`(位置由用户 2026-09-20 指定;断言了 正文 → 二维码 → 参考资料 的顺序);**补问轮不追加**(`pendingInterrupt` 非空时跳过)\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 位置调整**:最初放在整条回答最末尾,用户要求改到**参考资料之前**(一行顺序调整 + 一条顺序断言)。\n\n📌 **2026-09-20 补:补问轮不展示**(用户从浏览器截图指出)。场景:公司类问题触发 `company_need` 补问,界面上是「输入框 + 跳过公司查询 + 提交」,后端同时给了 `cipa=true`,结果二维码贴在补问卡片下面。判据为**本轮是否有 interrupt**:有则视为「还没给出正文回答」,不展示。"
     }
   ]
 }

+ 19 - 0
harness/progress.md

@@ -2888,3 +2888,22 @@
 - 断言:新增 3 项**顺序断言**(构造一轮带 source 的问答,断言 `二维码下标 < 参考资料下标`、二维码在正文之后、参考资料仍在最末尾)——
   适配层脚本 19 → **22/22 通过**
 - 文档同步:`api-chat.md` 的 cipa 表、`feature_list.json`、根 `README.md` 的 20260920 节
+
+### 续 2:补问轮不该出图(用户截图指出)
+
+用户发了浏览器实测截图:**公司类问题触发 `company_need` 补问**时,二维码被贴在
+「输入框 + 跳过公司查询 + 提交」下面 —— 那一轮界面还没给出正文回答,贴二维码既突兀也没意义。
+
+**两个信息**:
+1. ✅ **后端已经部署了 `cipa`**(截图里二维码确实渲染出来了)——此前记录的「尚未下发」已过期,已改
+2. 🐛 前端判据不严:原来只看 `cipa === true`,没看「本轮是不是补问轮」
+
+**修法**:追加条件 `&& !this.pendingInterrupt` —— 有 interrupt 就视为「还没给出正文回答」,不展示。
+**断言**:新增 3 项(`company_need` 与 `company_selection` 两类 interrupt 带 `cipa=true` 都不出图,
+且**选项卡本身仍正常出现**)——适配层脚本 22 → **25/25 通过**。
+
+**顺带观察**:探测真实后端时**又遇到一次 HTTP 500(0 字节)**(当日第二次,先前分类接口也 500 过一次)——
+前端按流中断处理、不重试;已记入 `api-chat.md` 的 cipa 节。
+
+文档同步:`api-chat.md` 的 cipa 表补了「true + 本轮有 interrupt → 不展示」一行、
+`feature_list.json` 与根 `README.md`。

+ 26 - 0
harness/tools/verify-company-info-passthrough.mjs

@@ -201,6 +201,32 @@ console.log('\n【5】result.cipa → 企业微信二维码块(<image-scope>
     ['done', { status: 'completed' }],
   ]);
   check('非布尔值一律当 false(严格 === true)', !seen_message_text(truthyString).includes('image-scope'));
+
+  // ⚠️ 补问轮不展示(用户 2026-09-20 从截图指出):
+  // 界面那时是「补问卡片」,还没给出正文回答,贴二维码既突兀也没意义
+  const INTERRUPT = {
+    kind: 'company_need',
+    status: 'not_found',
+    question: '本次未检索到与您提供的名称或代码匹配的企业。',
+    input_help: "请核对并补充工商注册全名或统一社会信用代码;也可以输入'跳过公司查询'。",
+  };
+  const withInterrupt = await runTurn([
+    ...BASE_EVENTS,
+    ['interrupt', INTERRUPT],
+    ['result', { response: 'x', cipa: true }],
+    ['done', { status: 'needs_input' }],
+  ]);
+  const interruptText = seen_message_text(withInterrupt);
+  check('补问轮:选项卡仍正常出现', interruptText.includes('<question-cards>'), interruptText.slice(0, 80));
+  check('**补问轮即使 cipa=true 也不展示二维码**', !interruptText.includes('image-scope'), interruptText.slice(-80));
+
+  const withCandidates = await runTurn([
+    ...BASE_EVENTS,
+    ['interrupt', { kind: 'company_selection', status: 'found', question: '请选择您所指的公司', result: { candidates: [{ Name: '甲公司', CreditCode: 'C1' }] } }],
+    ['result', { response: 'x', cipa: true }],
+    ['done', { status: 'needs_input' }],
+  ]);
+  check('候选确认轮同样不展示二维码', !seen_message_text(withCandidates).includes('image-scope'));
 }
 
 console.log('\n【6】cipa 跨轮不串(resetTurnContent 生效)');

+ 6 - 2
src/components/api-chat-coordinator.ts

@@ -1104,8 +1104,12 @@ export class ApiChatCoordinator {
     }
 
     // 企业微信二维码:后端标记为 CIPA / 出海类问题时追加。
-    // 位置在**参考资料之前**(用户 2026-09-20 指定)——正文/卡片之后、参考资料之前
-    if (this.pendingCipa) {
+    // 位置在**参考资料之前**(用户 2026-09-20 指定)——正文/卡片之后、参考资料之前。
+    //
+    // ⚠️ 本轮若是**补问轮**(interrupt:等用户补充公司信息 / 确认候选)则**不展示**:
+    // 那一轮界面上是补问卡片,还没给出正文回答,此时贴二维码既突兀也没意义
+    // (用户 2026-09-20 从截图里指出)。
+    if (this.pendingCipa && !this.pendingInterrupt) {
       parts.push(buildCipaQrcodeContent());
     }