feature_list.json 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. {
  2. "meta": {
  3. "project": "zhaoshang-llm",
  4. "description": "青浦区营商智能助手(Vue 3 + TypeScript + Vite),含 AI 对话、政策卡片、文件上传、虚拟人/语音能力",
  5. "repo_root": "f:\\yysk\\AI_zhaoshang\\zhaoshang-llm",
  6. "last_updated": "2026-09-17",
  7. "status_legend": {
  8. "not_started": "还没碰",
  9. "in_progress": "当前正在做的那个(同一时间只能有一个)",
  10. "blocked": "有记录的阻塞问题,推不动",
  11. "passing": "验证通过,证据已记录"
  12. }
  13. },
  14. "features": [
  15. {
  16. "id": "policy-detail-index-mismatch",
  17. "priority": 4,
  18. "area": "policy",
  19. "title": "修复:同一政策的多个申报事项点「查看详情」内容一样",
  20. "user_visible_behavior": "两张卡片即使是同一政策下的不同申报事项,点「查看详情」也各自打开对应的那条内容,不再都跳到第一张。",
  21. "status": "passing",
  22. "verification": [
  23. "npm run build 通过",
  24. "harness/tools/verify-policy-detail-index.mjs —— 10 项断言全通过。关键项:「点第 2 张 → 下标 1(修复前这里会返回 0)」、字段无法区分时返回 -1(不张冠李戴)、唯一命中仍采信、id/policy_id 可用、空值边界",
  25. "旧逻辑对照实证:同政策两卡片的场景下,点第 1 张与点第 2 张**都返回下标 0** —— 复现了用户看到的现象",
  26. "真实载荷探针 probe-policy-cards.mjs:确认后端 item 的 id/policy_id 恒为 undefined、card.name.text 两条相同"
  27. ],
  28. "evidence": "回归脚本 10/10 + 卡字段脚本 8/8 通过;旧逻辑对照复现了 bug;构建通过。⚠️ 浏览器未人工点过(见 notes)",
  29. "notes": "根因链:①后端 item 事件的 id/policy_id 恒为 undefined,唯一标识在 data.title(「政策名 - 申报事项」)与 data.source_id;②适配层 toPolicyTableItem 把 title 取自 card.name.text(只有政策名),并把 declaration_item 也设成同一个值,**丢掉了每条唯一的 data.title**;③于是同政策的多个申报事项 declaration_item 完全相同,而 openDetailByItem 的兜底匹配正是拿它比 → findIndex 永远命中第 0 张。\n\n修法(两步):\n1. 反查抽成纯函数 resolvePolicyDetailIndex()(policy-match-utils.ts),匹配顺序改为「对象同一性优先 → 字段比对仅在唯一命中时采信 → 否则 -1 交给兜底」。\n2. 按用户拍板,适配层的 declaration_item 改用后端每条唯一的 item.title(「政策名 - 申报事项」),详情面板标题从此能区分具体申报事项;卡片标题 title 仍取 card.name.text(政策名)不变。\n\n⚠️ **连带影响已处理**:panelData.title(**卡片区**标题)原先优先取 originalData.declaration_item,改后会连带把卡片标题也变成「政策名 - 申报事项」——用户只批准改详情标题,故已改成优先取 item.title(政策名),卡片区保持原样。\n\n有意未改:buildRecordPolicyList 的兜底列表会显示完整申报事项(列表本就叫「政策事项列表」,且只在库未加载时走)。\n\n两个脚本:verify-policy-detail-index.mjs(10 项,反查逻辑)、verify-policy-card-fields.mjs(8 项,适配层字段)。"
  30. },
  31. {
  32. "id": "remove-dev-auto-login",
  33. "priority": 6,
  34. "area": "auth",
  35. "title": "去掉开发环境硬编码账号的自动登录",
  36. "user_visible_behavior": "开发环境不再自动进入登录态——没登录就是访客态;要企业态需显式带 ?access_token= 或 ?credit_code=。",
  37. "status": "passing",
  38. "verification": [
  39. "npm run build 通过",
  40. "grep 确认 src/ 与 index.html 里 devLogin / isDevLogin 零残留",
  41. "代码路径核对:initAuth() 现在只剩「有 access_token 走登录」「有 credit_code 直接拉企业信息」「都没有则保持未登录」三条分支,无环境判断"
  42. ],
  43. "evidence": "分支已整段移除,构建通过,grep 零残留。⚠️ 浏览器观感未人工确认(见 notes)",
  44. "notes": "根因:useEnterpriseAuth.ts 里 `const isDevLogin = import.meta.env.MODE == 'development'` 在开发环境**无条件**用硬编码的 devLogin token 登录,所以「我没登录也进入登录态」。\n\n影响与取舍:dev 下 globalThis.token 不再自动注入。依赖它的旧接口(企业信息 /fta_ent_policy/enterprise_info 等)需要显式登录才可用;本轮涉及的会话/历史/反馈已全部走 DMS(token 由 vite 代理注入),不受影响。\n\n⚠️ 待人工确认:打开页面应停留在访客态(无企业名、历史用 ba_temp_chat_history)。"
  45. },
  46. {
  47. "id": "dms-chat-storage",
  48. "priority": 5,
  49. "area": "chat",
  50. "title": "会话、问答记录与反馈落地到 DMS",
  51. "user_visible_behavior": "用户提交问题时会话与该会话的整段对话写入 DMS(一个会话一行,不是一问一答一行);会话列表、历史记录从 DMS 读取;会话改名/删除同步到 DMS;点赞/点踩/取消记进该会话行的 JSON 里对应那条消息;未登录的访客记录以「访客_<访客id>」归属写入;DMS 不可用时聊天与本地历史不受影响;开发环境不再自动登录(默认访客态)。",
  52. "status": "in_progress",
  53. "verification": [
  54. "npm run build 通过",
  55. "harness/tools/verify-dms-chat-storage.mjs —— 36 项断言,打真实 DMS(经 vite 代理,与浏览器同一路径)。关键断言:第二轮后仍是同一行(不是一封问答一行)、消息顺序与 id 保留、长文本含协议标记往返一致、反馈只改命中那条消息且另一条未被误改、定位不到时不新增行、删除连带清理、时间戳解析、访客归属",
  56. "代理链路实测:经 /dms-api/ 查询返回 202(token 已由代理注入);直连 DMS 不带 token 返回 208 无token",
  57. "DMS 残留复查:测试数据已清理(残留 0 行)"
  58. ],
  59. "evidence": "验证脚本 36/36 通过;构建通过;代理注入 token 实证。⚠️ 浏览器端到端未人工点过(见 notes)。",
  60. "notes": "存储粒度(用户 2026-09-17 定的):**一个会话一行**。1889 的幂等键是 c_session_id(不是 c_record_id),整段消息序列化成 {version:1,messages:[…]} 存进 c_answer,c_question 存首问。写入方式是**以本地 messages 为准整段重写**,不做读-改-写 —— 本地状态是权威,从根上避免并发覆盖。\n\n会话标识用前端 session.id(UUID,恒等于聊天协议 thread_id,跨刷新稳定)→ c_session_id;消息 id 沿用 AI 消息 id,它与反馈组件的 record.id 是同一个值,反馈才定位得到。\n\n反馈:读整行 → 只改命中那条消息的 feedback 字段 → 写回;与对话写入共用 session: 顺序链。定位不到时**不新增行**。\n\n⚠️ **不存消息级时间戳**:本地消息本就没有时间戳,整段重写时现造 Date.now() 会让所有消息时间都变成最后一次保存的时刻——假数据比没有更糟。顺序由数组顺序表达,会话起始时间看该行 c_created_at。\n\n⚠️ **踩过的坑:findRowBy 不要加 orderBy**。1889 没有 c_updated_at 字段,按它排序会让 DMS 报错、反查恒空 → 每次 upsert 都新增 → 同一会话写成多行。跨栏目复用查询函数时,排序字段必须两个栏目都有。\n\n⚠️ **DMS 的删除不是 delContentById**(各种传法都 code=-1 / POST 405),正确姿势是 `POST /content/updateAudit` form {columnId, id, state: 4}(state=4 销毁)。\n\n⚠️ **addContent 返回记录 uuid,形态是纯字符串**(content 字段直接是 uuid),不需要再反查。c_id(must=true)实测不强校验,自动填 0。\n\n⚠️ token 由 **vite 代理注入**(.env.development.local 的 DMS_TOKEN,不入库),前端产物里没有 token;生产需 nginx 等价转发,长效凭据待确认。\n\n⚠️ **反馈状态刷新后不恢复显示**:JSON 里存着,但 BusinessRecord 的 localFeedback 初始恒为 None,未做从 DMS 回填。要做得另接一条线。\n\n访客只写不读(fetchRemoteSessions 对空 credit_code 返回 [],保持原行为)。"
  61. },
  62. {
  63. "id": "empty-content-agreement",
  64. "priority": 15,
  65. "area": "chat",
  66. "title": "统一「内容是否为空」的判空口径",
  67. "user_visible_behavior": "切会话或刷新后,未产生正文的消息不再误报「请求已取消」;只有真正被用户停止的消息才显示该提示。",
  68. "status": "passing",
  69. "verification": [
  70. "npm run build 通过",
  71. "harness/tools/verify-empty-content-agreement.mjs —— 30 项断言:基准判定 9 例 + 不变量(两处必须一致)18 例 + 回归 3 例",
  72. "覆盖用例:空串 / 空白 / 只有 1 个进度块 / 多个进度块 / 只有 silence / 进度块+正文 / 纯正文 / 正文+进度块 / 只有 POLICY_TABLE 标记"
  73. ],
  74. "evidence": "验证脚本 30/30 通过;构建通过",
  75. "notes": "根因:两处判空口径不一致——normalizeSessionHistory 用 content.trim()(scope 进度块算有内容),isInterruptedEmptyScopeMessage 用 stripScopeBlocks()(进度块不算内容)。同一段内容一处认为有、一处认为没有。\n\n新协议把正文推迟到 done 才写入(适配层 flushContent),处理期间内容只有进度块,把这个缝隙从偶发放大成「一切会话就出现」:归一化认为有内容→不替换文案却把状态翻成 Finish;渲染层认为没内容→显示请求已取消,而请求其实还在跑。\n\n修法:抽出唯一的 hasVisibleMessageContent()(src/utils/interrupted-message.ts),归一化与渲染层都改用。\n\n⚠️ 修复后的观感变化:原显示请求已取消的场景,现在可能显示「会话已经取消」(CANCELLED_SESSION_TEXT)——这是代码库里「消息结束但无内容」的既有措辞,且只在会话**未在生成中**时触发(switchSession 对正在生成的会话会跳过归一化)。若该措辞也不合适,需另定文案或改为不落文案。"
  76. },
  77. {
  78. "id": "chat-api-migration",
  79. "priority": 10,
  80. "area": "chat",
  81. "title": "AI 对话迁移到新接口 POST /api/chat",
  82. "user_visible_behavior": "用户提问后能正常收到回答;政策类问题能出卡片;公司类问题能出补问候选。界面表现与原版一致。",
  83. "status": "passing",
  84. "verification": [
  85. "npm run build 通过",
  86. "起本地服务端跑协议适配层契约测试(SSE 分片、CRLF、多行 data、409、断流、坏 JSON、过期 request_id、主动停止)",
  87. "对真实后端 192.168.2.23:8000 跑端到端:普通问答 / 政策推荐 / 公司补问 / 候选确认 / 翻页 / 取消"
  88. ],
  89. "evidence": "适配层契约测试 44/44 通过;真实后端多场景验证通过,行顺序 text→policy-table→text→ref-links,result.response 未被重复追加",
  90. "notes": "适配层在 src/components/api-chat-coordinator.ts,把新协议翻译成原界面本来就能渲染的内容标记(<scope>/POLICY_TABLE/<ref_links>/<question-cards>)。旧协调器 src/components/stream-message-coordinator.ts 未改动、保留未用。请求体严格只发 {thread_id, question},多字段会 422。"
  91. },
  92. {
  93. "id": "chat-rendering-order",
  94. "priority": 20,
  95. "area": "chat",
  96. "title": "回答内容顺序、换行与复制",
  97. "user_visible_behavior": "顺序为 正文 → 政策卡片 → 综合说明 → 参考资料;段落正常分段;复制出来的是纯可读文本,不含协议标记。",
  98. "status": "passing",
  99. "verification": [
  100. "对真实后端取回内容,按帧喂给真实的 StreamXMLFilter,断言行顺序",
  101. "对含 scope/POLICY_TABLE/ref_links 的内容跑复制提取,断言标记全部被剔除"
  102. ],
  103. "evidence": "真实后端非 scope 行顺序 text→policy-table→text→ref-links(参考资料在最后);复制提取测试 7/7 断言通过",
  104. "notes": "复制用 copyableContent 正则剥离标记,不能用 getStreamPlainText——过滤器遇到 <!-- POLICY_TABLE 会把同批 buffer 里它之前的内容当纯文本输出。"
  105. },
  106. {
  107. "id": "policy-source-badge",
  108. "priority": 30,
  109. "area": "policy",
  110. "title": "惠企政策库来源标识",
  111. "user_visible_behavior": "来自惠企政策库的推荐卡片,右上角显示橙色「惠企政策」标识。",
  112. "status": "passing",
  113. "verification": [
  114. "对真实后端取回卡片,断言 is_policy_library 字段存在且为布尔值",
  115. "断言打标条目与来源文件对应(reference/惠企政策.json)"
  116. ],
  117. "evidence": "真实后端 8 张卡片,字段均为布尔值,1 张打标(与 file=reference/惠企政策.json 一致)",
  118. "notes": "只在卡片右上角显示,不同步到「更多」列表与详情面板(用户明确要求)。样式类 .policy-source-badge 与左侧 .policy-badge 同形不同色。"
  119. },
  120. {
  121. "id": "policy-detail-blocks",
  122. "priority": 40,
  123. "area": "policy",
  124. "title": "政策详情面板新增区块(条件核验 / 匹配依据)",
  125. "user_visible_behavior": "点卡片「查看详情」,面板底部有 条件核验 / 匹配依据 两个区块。",
  126. "status": "passing",
  127. "verification": [
  128. "npm run build 通过",
  129. "对真实后端断言 conditions / match_basis / citations 随 POLICY_TABLE 传入"
  130. ],
  131. "evidence": "真实后端卡片解析出 conditions / match_basis / citations 字段;构建通过",
  132. "notes": "复用面板原有的 detail-row card-style / record-condition-title / condition-match-card 类,配色用已有的 match-high/medium/low 等级样式,未引入新视觉语言。\n\n⚠️ 2026-09-17:「原文引用」板块按用户要求删除(模板与其计算属性均已注释保留,未直接删,符合项目「注释不删除」的约定)。"
  133. },
  134. {
  135. "id": "company-interrupt",
  136. "priority": 50,
  137. "area": "chat",
  138. "title": "公司补问与候选确认",
  139. "user_visible_behavior": "问公司相关问题时会弹出选项卡;选候选公司、换关键词、查看更多、取消都能继续对话。",
  140. "status": "passing",
  141. "verification": [
  142. "真实后端触发 company_need 与 company_selection",
  143. "断言选项可被原 QuestionCard 解析",
  144. "断言选择结果还原为协议字符串(1→\"1\"、查看更多→/next、重新查询→/retry、取消→/cancel、不需要→不需要)"
  145. ],
  146. "evidence": "真实后端候选 5 家正常展示;序号续问、/next 翻页、/cancel 均返回 done=completed/needs_input;往返映射 8 项断言通过",
  147. "notes": "interrupt 翻译成 <question-cards>,由原 QuestionCard 渲染;提交时经 toChatQuestionAnswer 转成 question 字符串。注意:公司候选提交的是公司名称而非序号,见 questionnaire-submit-text 条目。"
  148. },
  149. {
  150. "id": "policy-library-back-entry",
  151. "priority": 45,
  152. "area": "policy",
  153. "title": "政策事项列表:统一入口,仅惠企政策可返回",
  154. "user_visible_behavior": "卡片区「更多 >>」与详情的「返回政策事项列表」进入的是同一个列表(惠企政策库 262 条申报事项,每次进入重置筛选),本轮匹配到的惠企政策置顶;非惠企政策的详情里不显示该返回入口。",
  155. "status": "passing",
  156. "verification": [
  157. "npm run build 通过",
  158. "harness/tools/verify-policy-library.mjs —— 21 项断言:名称归一化、判定(接口字段优先/标题回退/负例)、命中收集、置顶排序(含无命中时顺序不变)、政策详情地址拼接(有 id / 无 id / data 缺失 / 空白 id / 去空格 / 特殊字符编码)+ 真实库覆盖率 261/262",
  159. "harness/tools/check-policy-sources.mjs —— 对比本地与远端两个来源:本地 262 条 / 261 带 市级政策id,远端 327 条 / 0 带(确认开发环境本地优先仍有必要)",
  160. "真实库数据:262 条 / 38 个政策名;后端 is_policy_library=true 的那条标题能在库中匹配到",
  161. "生产产物 dist/index.html 的 localFirst 求值为 false(生产仍远端优先);dev server 提供的本地 json 与仓库文件 sha256 一致"
  162. ],
  163. "evidence": "tools 脚本 21/21 通过;check-policy-sources.mjs 输出「本地 262 条 / 261 带 id、远端 327 条 / 0 带 id」;构建通过,dist/index.html 的 localFirst 为 false、dev 为 true。另修掉一个缓存 bug:库数据一度写成 computed 读 globalThis,会把异步加载前的空值缓存住,已改为函数现读。",
  164. "notes": "⚠️ 政策名称跳转:由库条目的 data.市级政策id 拼出 https://zwdt.sh.gov.cn/qykj/shell_oc_policy_zq/policy/policy-detail?id=<id>(见 policy-library-utils.ts 的 buildPolicyDetailUrl)。库覆盖率 261/262。\n\n⚠️ **261/262 只对「本地那份」成立**(2026-09-17 查明):另一个来源(`VITE_DOWNLOAD_URL` 远端那份,327 条)**0 条**有这个字段 —— 这才是浏览器里「政策名称」显示为纯文本的真因(Session 019 记的「陈旧 HMR 状态」「走了兜底路径」两个猜测都不对)。远端那份的 `policy_id` 是 40 位、与该条 `id` 相同,与 市级政策id(24 位)是**两套 id 体系**,拼不出一网通办的地址。index.html 原本是远端优先,且远端**带 CORS 头**(实测 `Access-Control-Allow-Origin` 回显 `localhost:8083` 与 `aixq.shqp.gov.cn`),所以浏览器里远端确实会赢。\n\n为此新增 `VITE_POLICY_LOCAL_FIRST`(仅 `.env.development` 为 true,其余三份 env 显式 false):dev 本地优先、远端兜底,生产仍远端优先、本地兜底。\n⚠️ 生产环境「政策名称」仍不可点,属**后端数据问题**(需远端那份补 `data.市级政策id`);`check-policy-sources.mjs` 会在「远端也有 id 了」时提醒可以去掉该开关。\n⚠️ 代价:dev 用的这份比远端**少 65 条申报事项**(262 vs 327)。\n\n此前考察过的两个来源都不可用:apply_link 字段 262 条全空;资源申请备注里的 URL 只有 46 条有、其中 25 条是内网登录页,且同名政策下的 URL 是按申报事项分别对应的、不能互借。没有 id 时不给链接(不猜)。\n\n惠企政策 = policies_public.v1.json(262 条申报事项 / 38 个政策,index.html 异步预加载到 globalThis.policiesPublic)。判定两者结合:优先用接口字段 is_policy_library,缺失时回退到「标题能在库里找到」,标题匹配做了归一化(去《》全半角括号、连接符、空白)。\n\n⚠️ 两个入口**必须走同一个列表**(用户明确要求)。曾一度做成两套数据源(卡片「更多」看卡片、详情「返回」看库),是理解错了。两套数据源还有个连带问题:筛选状态跨入口共享,而库里部门是简称、卡片是全称,导致从库切回卡片时几乎筛不出东西(表现为「只剩一条」)。统一后消失。\n\n⚠️ 该值是 index.html **异步 fetch** 写入的,应用**不等待**它——读取务必**现读**,不要用 computed 缓存(globalThis 属性不是响应式依赖)。库未加载完时退回落卡片数据作为兜底。\n\n纯逻辑在 src/components/Chat/policy-library-utils.ts,tools 脚本引用同一份实现。\n\n⚠️ 「每次进入都重置筛选」(用户要求):进入列表时清空部门/分类/关键词并把页码归 1。此前筛选跨入口保留,是「列表只剩一条」的直接成因。重置会连带触发 watch([currentFilterOption, currentDeptFilter, searchKeyword]) 与 watch(currentPage),导致列表被重复构建——两次结果相同,可接受。"
  165. },
  166. {
  167. "id": "company-interrupt-status-matching",
  168. "priority": 50,
  169. "area": "chat",
  170. "title": "按 interrupt.status 判定补问形态(替掉猜测式判定)",
  171. "user_visible_behavior": "有候选时显示候选列表,没有候选时显示「上方输入框 + 跳过公司查询」。判定稳定,不受后端措辞变化影响。",
  172. "status": "passing",
  173. "verification": [
  174. "npm run build 通过",
  175. "四种情况逐一断言:company_selection+found / company_need+not_found / company_need+failed / company_need+无status",
  176. "专项核对:failed 文案不含「未查询到」;not_found 文案为「未查询到匹配的公司」",
  177. "去重回归:正文已含该段时问题字段仍为空",
  178. "真实后端:kind=company_need status=not_found → 输入框在上 + [跳过公司查询] + 问题字段已去重"
  179. ],
  180. "evidence": "真实后端 status=not_found 判定正确;四种情况断言全通过",
  181. "notes": "补问用 kind + status 描述,后端确认**四种情况都可能出现**:company_selection+found=有候选、company_need+not_found=未找到、company_need+**failed**=查询服务失败、company_need+**无 status**=初次询问是否需要公司信息。此前**完全没用这个字段**,而是靠 kind、candidates.length、以及用 input_help 中文文本做正则 /跳过|skip/i 猜按钮文案——最后一条尤其脆,后端改一个字就失效。现已改为 status 为权威信号,删掉正则嗅探。\n\n本轮修正两处错误:①failed 曾落进 not_found 分支,问题文案被填成「未查询到匹配的公司」,**把失败说成了查无**,违反接口文档「空候选与查询失败需分别展示」;现独立分支,说明失败原因(result.error 经 describeCompanyError 翻译)。②无 status(初次询问)曾给「跳过公司查询」,现改为「不需要公司信息」(→不需要),与该变体后端 input_help 一致。\n\n⚠️ 曾引入回归又修复:去重把 question 置空后,被后续的 `question || '未查询到匹配的公司。'` 兜底复活,等于去重失效;已抽出 fallbackQuestion() 统一处理。\n⚠️ failed 与「无 status」两种情况只有静态断言,未在真实后端遇到过(上游持续 provider_failure)。"
  182. },
  183. {
  184. "id": "company-need-input-on-top",
  185. "priority": 51,
  186. "area": "chat",
  187. "title": "公司未找到时:上方输入框补充信息 + 下方跳过公司查询",
  188. "user_visible_behavior": "公司未检索到时,卡片上方是一个常驻输入框(placeholder:请输入工商注册全名或统一社会信用代码),下方只有一个跳过选项;填入公司信息即可重新查询,或点跳过先按原问题回答。",
  189. "status": "passing",
  190. "verification": [
  191. "npm run build 通过",
  192. "真实后端两个 company_need 变体均验证:未检索到企业 → [跳过公司查询];询问是否需要公司信息 → [不需要公司信息]",
  193. "回归:有候选的 company_selection 行为不变(候选列表 + 查看更多 + 取消公司查询)",
  194. "提交映射:跳过公司查询→/skip、不需要公司信息→不需要、自由输入→原文"
  195. ],
  196. "evidence": "真实后端整链验证:卡片问题字段为空(已去重)、输入框在上、选项 [跳过公司查询]、正文确认已含该段;三项修复静态断言全通过(含反向用例)",
  197. "notes": "QuestionCard 新增两个可选字段:freeformInputOnTop(输入框常驻并排在最上)、freeformPlaceholder。原自由输入是选项之后的「其他」行、需点击才展开,无法满足「输入框在上」。改动同步修正 4 处自由输入判定(isFreeformSelected / isQuestionAnswered / canSubmit / handleSubmit),使常驻输入框无需点击激活即可被计入已作答与提交内容。\n\n后续修掉三个问题:①序号错乱——输入框在最上却拿 B、选项拿 A;新增 getCardOptionLabel(card, optIdx) 在 freeformInputOnTop 时整体后移一位,输入框取 A。②同一段文字重复——后端把同一段话既放进 answer.text 又放进 interrupt.question(实测完全相同);改为 buildQuestionCardsContent(interrupt, shownText),正文已含则不重复,模板问题行加 v-if 避免只剩“(单选)”。③跳过公司查询原来发 /skip,改为发选项文本「跳过公司查询」(后端 input_help 明确两种都收)。\n\n⚠️ 保留一处不一致:不需要公司信息 仍发 \"不需要\",因该变体后端 input_help 写的是“不需要则输入'不需要'”;若统一改发选项文本需先确认后端认哪个。\n⚠️ Question.message 字段从未被模板渲染,此前写的提示文案一直不可见(未改)。"
  198. },
  199. {
  200. "id": "company-query-error-states",
  201. "priority": 52,
  202. "area": "chat",
  203. "title": "公司补问区分「查询失败」与「查无公司」",
  204. "user_visible_behavior": "上游公司数据服务失败时,卡片明说「公司数据服务返回失败(provider_failure),暂时无法列出候选公司」并给出重试/取消;查无结果时说「未查询到匹配的公司」;正常时列出候选。",
  205. "status": "passing",
  206. "verification": [
  207. "npm run build 通过",
  208. "三种卡片状态的静态断言(查询失败 / 查无公司 / 正常有候选)",
  209. "真实后端端到端:用「上海星溯算力集团有限公司,有什么优惠政策」复现 provider_failure,确认修复后的卡片文案"
  210. ],
  211. "evidence": "真实后端返回「公司数据服务返回失败(provider_failure),暂时无法列出候选公司。」+ [重新查询][取消公司查询]",
  212. "notes": "🚨 修复的是公司补问**死循环**的真正根因。此前两次误判为「序号 vs 文本」问题,实际是:后端返回 candidates:[] + error:provider_failure(上游企查查查询失败),而卡片仍写「请选择您所指的公司」、不显示错误、允许自由输入并提示「输入新关键词」→ 用户只能手打公司名 → refine → 上游又失败 → 又弹同一张卡。接口文档明确要求「空候选与查询失败需分别展示」「有此错误时,不把空候选说成查无公司」,原实现两条都违反。新增 describeCompanyError() 翻译错误码(含 search:/basic:/honors: 阶段前缀,保留原码便于排查)。⚠️ 另有教训:排查必须读原始 SSE 载荷,不能只看封装的中间事件——适配层已把 interrupt/done 并入 message 通道,前两次测试脚本查的是不存在的事件,导致误判「后端不返回补问」。"
  213. },
  214. {
  215. "id": "questionnaire-submit-text",
  216. "priority": 55,
  217. "area": "chat",
  218. "title": "公司候选提交选项全部内容拼接成的文本",
  219. "user_visible_behavior": "公司候选卡片选项显示公司名称(详情为信用代码·状态·负责人);点击提交后接口收到的是选项全部内容拼接成的文本。",
  220. "status": "passing",
  221. "verification": [
  222. "npm run build 通过",
  223. "单元验证 toChatQuestionAnswer 的 10 项断言:按 label 反查候选重建完整文本、固定动作映射控制串、自由输入原样发送、多选与多题拼接、无补问数据时降级不崩"
  224. ],
  225. "evidence": "转换逻辑 10/10 断言通过;构建通过",
  226. "notes": "🚨 该条目历史:第一版提交序号(符合协议),被要求改为提交文字后**产生死循环**——提交公司名落进协议的“换关键词”路径,后端重新检索又返回同一批候选。根因:协议(api-chat.md)明确‘API 由序号映射到原候选’,文本这种输入形式被保留给 refine,语义上无法表达“我选这一个”。现按用户要求改为提交选项全部内容拼接文本。实现要点:QuestionCard 只回传 label 不回传 description,故协调器新增 lastInterruptPayload 留存补问数据,提交时按 label 反查候选重建文本。⚠️ 若后端仍按‘序号选中/文本则重新检索’处理,死循环会复现;届时的两条出路:①改回提交序号(序号取自当前显示顺序);②后端扩语义识别全字段文本。另修掉三个缺陷:原先只取 answers[0](多题丢失)、多选只取第一项、/^(\\d+)\\.\\s/ 规则误伤其它问卷。端到端未验证(排查时后端已完全不返回补问)。"
  227. },
  228. {
  229. "id": "dept-filter-restored",
  230. "priority": 60,
  231. "area": "policy",
  232. "title": "政策「更多」面板筛选还原为最初实现",
  233. "user_visible_behavior": "部门下拉仍是原来那 15 个固定部门,筛选按字面匹配。",
  234. "status": "passing",
  235. "verification": [
  236. "读 PolicyMatch.vue 断言 deptOptions 为固定的 15 个部门",
  237. "断言筛选比较为 String(item.source) === dept 的字面相等",
  238. "grep 确认无 matchDept / DEPT_KEYWORDS / normalizeDeptLabel 残留"
  239. ],
  240. "evidence": "已确认无残留,npm run build 通过",
  241. "notes": "曾先后尝试①从接口数据动态生成选项、②保留固定选项但加关键词归一化匹配,均被用户否掉。用户明确表态:暂时不做匹配,恢复成最初的。新接口返回的部门是全称(如青浦区科学技术委员会),与这 15 个简称字面不相等,因此选中部门会筛出空列表——这是按要求保持原逻辑的必然结果,不要自作主张去修。"
  242. },
  243. {
  244. "id": "file-upload",
  245. "priority": 70,
  246. "area": "upload",
  247. "title": "文件与图片上传",
  248. "user_visible_behavior": "用户能通过按钮 / 拖拽 / 粘贴上传图片与附件,上传后随提问一起发给模型。",
  249. "status": "not_started",
  250. "verification": [],
  251. "evidence": "",
  252. "notes": "阻塞原因(接口契约未定):新接口 /api/chat 请求体只接受 {thread_id, question},多字段返回 422,因此旧协议的 transmission.files / file_pos 发不出去。备选方案:①后端扩展请求体接受 files/file_pos;②前端把 OSS 地址拼进 question 文本;③后端提供文件登记接口返回 file_id。需用户先确认后端方案。原实现与两个接口的完整参数见 git 历史与 useBusinessAssistantUpload.ts(原代码已保留,入口按钮被注释)。"
  253. },
  254. {
  255. "id": "chat-markdown-format",
  256. "priority": 80,
  257. "area": "chat",
  258. "title": "回答的 markdown 分段",
  259. "user_visible_behavior": "多段内容正常分段,列表后面的说明行不被并进上一条列表项。",
  260. "status": "blocked",
  261. "verification": [],
  262. "evidence": "",
  263. "notes": "阻塞原因(方案未拍板):新后端 summary 文本混用单换行与空行分隔段落,而正文块是 marked + white-space: normal,markdown 下单个换行会被折叠,导致列表后的说明行被并进上一条列表项。前端可加归一化(非列表项之间的单换行提升为空行、连续列表项保持单换行),但需用户确认是否接受在适配层改动正文格式。前端渲染链路本身正常,证据:TextContent.vue 用 marked.parse,未做格式化转换。"
  264. }
  265. ]
  266. }