progress.md 190 KB

进度日志

通用的仓库内会话进度日志。文件名沿用课程历史约定,不绑定 Claude Code—— Codex、OpenHands 等 agent 同样可用,前提是仓库指令要求它在开工时读取、收尾时更新。 agent 不会自动维护这个文件。

当前已验证状态

  • 仓库根目录f:\yysk\AI_zhaoshang\zhaoshang-llm
  • 标准启动路径npm run dev → https://localhost:8083(dev server 为 HTTPS,带自签证书告警属正常)
  • 标准验证路径npm run build(生产构建,必过);可选 npm run test(jest)
  • 当前最高优先级未完成功能file-upload(文件与图片上传)
  • 当前 blocker:新接口 /api/chat 的请求体只接受 {thread_id, question},多余字段返回 422, 因此旧协议的 transmission.files / file_pos 发不出去。上传功能如何随请求提交的后端契约未定, 需先确认方案(后端扩展请求体 / 把 OSS 地址拼进 question / 后端提供文件登记接口)。

需要了解环境与约束

不在本文件重复,见:

想了解
架构、目录、核心系统、关键约束 docs/architecture.md
接口契约与字段释义 docs/reference/(先读该目录 README)
已知技术债与未决问题 docs/plans/tech-debt.md
质量现状与缺口 docs/quality.md
验证流程 sops/verification.md

还没写进 feature_list 的待办

  • 适配层映射了新字段 knowledge_type 的类型但未渲染,用户未要求展示
  • 惠企政策标识目前只在卡片右上角,用户明确要求不同步到「更多」列表与详情面板

会话记录

顺序:按时间正序(旧 → 新),与根 README.md 的日期节一致—— 最新的记录在最下面。加新记录请追加到文件末尾,不要插到开头。

Session 000(基线)

  • 日期:2026-09-15
  • 本轮目标:建立 harness 基线,把此前的接口迁移工作固化成可交接的仓库事实
  • 已完成
    • 建立 harness/ 文件夹(本套文件)并在根 CLAUDE.md 加入 Harness 工作流段落
    • 汇总先前的接口迁移成果到 feature_list.json(6 项 passing,1 项 blocked,1 项 not_started)
  • 运行过的验证
    • npm run build 通过
    • node -e "JSON.parse(...)" 校验 feature_list.json:JSON 合法、8 个功能、字段齐全、无重复 id、状态合法、in_progress 数量为 0
  • 已记录证据feature_list.json 中每项 passing 功能都带 verification 与 evidence 字段
  • 提交记录:无(仓库尚无任何提交;初始提交由用户自行完成)
  • 更新过的文件或工件:新建 harness/(README、feature_list.json、claude-progress.md、init.sh、 session-handoff.md、clean-state-checklist.md、evaluator-rubric.md);修改根 CLAUDE.md
  • 已知风险或未解决问题
    • file-uploadchat-markdown-format 两项受外部条件阻塞,见各自 notes
    • 部门筛选按用户要求还原为最初的固定列表 + 字面匹配,新接口部门是全称,选中会筛出空列表—— 这是刻意保持的状态,不要当成 bug 去修
    • 仓库尚无 git 提交,init.sh 的提交历史检查会降级跳过
  • 下一步最佳动作:确认 file-upload 的后端契约(三选一),然后把它切到 in_progress 开始实现

    Session 001

  • 日期:2026-09-15

  • 本轮目标:修问卷/补问提交序号的问题;建立接口参考文档目录

  • 已完成

    • toChatQuestionAnswer 改为提交选项文字内容:公司候选直接发公司名,选项不再带 N. 前缀; 顺带修掉三个缺陷(只取第一题、多选只取第一项、数字开头选项被静默改写)
    • 清掉 finishTurn 里从未使用的 donePayload 死参数(误导过一次排查)
    • 新建 docs/reference/:新协议契约 + legacy/API.md交接时代码快照,非规格)+ 索引 README
    • 删除 dos/(内容已迁入 docs/reference/
    • 重新定性 legacy/API.md:用户澄清它是"接手项目时根据源码生成的快照,后续接口会改", 据此在四处统一措辞(文档顶部横幅、索引表、CLAUDE.mdharness/README.md), 并删除原先"它是仓库内唯一依据"的错误说法——判据优先级明确为源码备份 > 生成文档
  • 运行过的验证

    • npm run build 通过(本次改动后又重跑)
    • toChatQuestionAnswer 单元断言 14/14 通过
    • 措辞一致性 grep:无"已停用的旧协议""唯一依据"等旧表述残留
  • 已记录证据:见 feature_list.jsonquestionnaire-submit-text

  • 提交记录:无(用户自行提交)

  • 更新过的文件或工件src/components/api-chat-coordinator.tsdocs/reference/*CLAUDE.mdharness/README.mdharness/feature_list.json、本文件

  • 已知风险或未解决问题

    • ⚠️ 公司候选提交公司名是对协议的刻意偏离(协议写的是提交序号 "1")。若后端按名称 触发 refine 重新检索,会出现"选一次又弹回同一批候选"的循环。 真实后端验证未完成——排查时后端已不再返回任何补问("上海青浦发展集团有限公司能享受哪些政策" 与"帮我查一下我公司的补贴政策"两种之前都能触发的问法,现在都返回"无补问")。 需在浏览器里走一遍公司候选流程确认。
    • ⚠️ 流程问题(自我记录):本轮的最后一次改动(legacy/API.md 重新定性)没有在改完后 立即记录,是用户追问"每次修改后有更新 harness 文档吗"才补上的,且当时记录里还留着被推翻的 旧措辞。这正是 harness 要防的失效模式——文件不会自动维护,每轮收尾必须即时更新,不要攒
  • 下一步最佳动作:在浏览器验证公司候选提交后的行为;若出现循环,与后端确认候选确认按什么取值

Session 002

  • 日期:2026-09-15
  • 本轮目标:修公司候选提交导致的问题
  • 已完成
    • 🚨 确认事故:上一轮改成提交公司名后,问卷死循环——提交后再次调用接口只传了 公司名,落进协议的"换关键词"路径,后端重新检索又返回同一批候选,用户再选再循环。 根因是我上一轮的改动方向错了:协议(api-chat.md)明确"API 由序号映射到原候选", 文本这种输入形式被保留给了 refine,语义上无法表达"我选这一个"。
    • 按用户要求改为提交选项全部内容拼接成的文本(公司名 + 信用代码 + 状态 + 负责人)。 实现要点:QuestionCard 提交时只回传 label、不回传 description,因此协调器新增 lastInterruptPayload 留存最近一次补问,toChatQuestionAnswer(answers, interrupt) 按 label 反查候选后重建完整文本。
  • 运行过的验证
    • npm run build 通过
    • toChatQuestionAnswer 单元断言 10/10 通过(含反查重建、固定动作、自由输入、多选多题、无补问降级)
  • 已记录证据:见 feature_list.jsonquestionnaire-submit-text
  • 提交记录:无(用户自行提交)
  • 更新过的文件或工件src/components/api-chat-coordinator.tssrc/components/business-assistant/useBusinessAssistantChat.tsdocs/reference/README.mdharness/feature_list.json、本文件
  • 已知风险或未解决问题
    • ⚠️ 死循环是否真的解除,未验证。 排查时后端已完全不返回补问—— "上海青浦发展集团有限公司能享受哪些政策""帮我查一下我公司的补贴政策" "我想申请青浦区的企业扶持补贴,帮我看看我的公司符不符合条件" 三种问法全部返回"无补问"。 因此本轮只完成了静态与单元验证,端到端未验证,必须由用户在浏览器确认。
    • ⚠️ 若浏览器里仍循环:只有两条路——①改回提交序号(协议做法,序号取自当前显示顺序); ②后端扩语义,能识别"全字段文本"等价于选中该候选。
    • 📌 规范修正(用户指出):引用接口约定时注意区分—— API.md初始项目的接口文档;当前用的是新接口,契约看 api-chat.md; 但两者都会滞后,真正的事实来源是跑着的后端。不要拿文档去否定实际观测。
  • 下一步最佳动作:在浏览器验证公司候选提交后是否还会弹回候选列表

Session 003

  • 日期:2026-09-15
  • 本轮目标:定位并修复公司补问的死循环
  • 已完成
    • 找到真正根因(与我此前两次猜测都不同):用用户给的测试问法 「上海星溯算力集团有限公司,有什么优惠政策」实测,后端返回 company_selection + candidates: [] + error: "provider_failure" ——上游公司数据服务(企查查)查询失败,不是"查无公司"。
    • 而前端卡片在这种情况下仍写「请选择您所指的公司」、不显示错误、且 允许自由输入并提示"输入新关键词" → 用户只能手打公司名 → 走 refine 路径 → 后端再查 → 上游又失败 → 又弹同一张卡 → 死循环
    • buildQuestionCardsContent,按接口文档要求区分三种情况 (文档原文:"空候选与查询失败需分别展示"、"有此错误时,不把空候选说成查无公司"): ①查询失败 → 说明失败原因 + 重试/取消;②查无公司 → 说明没搜到 + 建议换关键词; ③正常 → 列出候选。
    • 新增 describeCompanyError(),把错误码翻成人话(含 search:/basic:/honors: 阶段前缀, 保留原错误码便于排查)。
  • 运行过的验证
    • npm run build 通过
    • 三种卡片状态的静态断言(查询失败 / 查无公司 / 正常有候选)输出符合预期
    • 真实后端端到端复现并确认修复:同一问法现在返回 "公司数据服务返回失败(provider_failure),暂时无法列出候选公司。" + [重新查询][取消公司查询]
  • 已记录证据:见 feature_list.jsoncompany-query-error-states
  • 提交记录:无(用户自行提交)
  • 更新过的文件或工件src/components/api-chat-coordinator.tsharness/feature_list.json、本文件
  • 已知风险或未解决问题
    • ⚠️ "选项文本 vs 序号"这个疑点仍未验证:当前上游查询一直失败,拿不到有候选的 正常场景,因此无法验证"选中候选后提交文本"是否会被后端当作 refine 而循环。 上游恢复后必须补测:走一遍有候选的流程,选中一家,确认返回的是选定公司后的结果。 若那时出现循环,则只有两条路:①改回提交序号(序号取自当前显示顺序); ②后端扩语义。
    • 本轮同时证实:排查这类问题必须读原始 SSE 载荷,不能只看自己封装的中间事件—— 我前两次都因为测试脚本查了不存在的事件(interrupt/done 在适配层已并入 message 通道) 而误判"后端不返回补问"。
  • 下一步最佳动作:等上游恢复后补测有候选时的选中流程

Session 004

  • 日期:2026-09-16
  • 本轮目标:公司未找到时,补问卡片改为「上方输入框补充信息 + 下方跳过公司查询」
  • 已完成
    • 发现后端已改:公司未找到的场景现在走 company_need(不再是 company_selection), 且后端自己的文案就写着"请核对并补充工商注册全名或统一社会信用代码;也可以输入'跳过公司查询'"。 这与用户的诉求完全一致——先读实际载荷再动手,比照文档猜要可靠
    • QuestionCard 新增两个可选字段(不影响既有行为): freeformInputOnTop(输入框常驻并排在最上)、freeformPlaceholder; 并同步修正 4 处自由输入判定,使常驻输入框无需点击激活即可参与"已作答/可提交/提交内容"判断。
    • 适配层 company_need 分支改为:输入框在上 + 下方跳过选项。 跳过选项跟随后端自己的说法——提示里出现"跳过/skip"就用"跳过公司查询"(→/skip), 否则用"不需要公司信息"(→不需要),避免按钮与上方提示自相矛盾。
    • 顺带记录:Question.message 字段从未被模板渲染,此前写的提示一直是不可见的。
  • 运行过的验证
    • npm run build 通过
    • 真实后端两个变体均验证通过: 「上海星溯算力集团有限公司,有什么优惠政策」→ 输入框在上 + [跳过公司查询] 「我想申请青浦区的企业扶持补贴…」→ 输入框在上 + [不需要公司信息]
    • 回归:有候选(company_selection)时行为不变,仍为候选列表 + [查看更多][取消公司查询]
    • 提交映射:跳过公司查询→/skip、不需要公司信息→不需要、自由输入→原文
  • 已记录证据:见 feature_list.jsoncompany-need-input-on-top
  • 提交记录:无(用户自行提交)
  • 更新过的文件或工件src/components/Chat/QuestionCard.vuesrc/components/api-chat-coordinator.tsharness/feature_list.json、本文件
  • 已知风险或未解决问题
    • ⚠️ "选中候选后提交文本是否会被当作 refine 而循环"仍未验证:上游公司数据服务持续 失败(provider_failure),拿不到有候选的正常场景。上游恢复后必须补测。
    • Question.message 未被渲染——若需要显示提示文案,需另行改模板(本轮未改,避免扩大改动面)。
  • 下一步最佳动作:上游恢复后补测有候选时的选中流程

Session 005

  • 日期:2026-09-16
  • 本轮目标:修公司补问卡片的三个问题(用户看截图指出)
  • 已完成
    1. 序号错乱:输入框常驻在最上却拿 B、选项在下面却拿 A。 新增 getCardOptionLabel(card, optIdx)freeformInputOnTop 时选项序号整体后移一位, 输入框自己取 A。常规选项与两处下拉项共 3 处已替换。
    2. 同一段文字出现两次:后端把同一段话既放进 answer.text(渲染在消息正文)、 又放进 interrupt.question(实测两者字符串完全相同)。改为 buildQuestionCardsContent(interrupt, shownText),正文已含该段时卡片里不再重复; 模板侧问题行加 v-if="card.question",避免只剩一个"(单选)"。
    3. 跳过公司查询发的是 /skip:改为发选项文本「跳过公司查询」。 依据:该变体的后端 input_help 明确写着 输入"跳过公司查询"或 /skip 继续——两种都收。 同时移除映射表里对应的 token。
  • 运行过的验证
    • npm run build 通过
    • 三项修复的静态断言全部通过(含"正文不含该段时不被误删"的反向用例)
    • 真实后端整链验证:卡片问题字段为空(已去重)、输入框在上、选项 [跳过公司查询]、 正文确认已含该段
  • 已记录证据:见 feature_list.jsoncompany-need-input-on-top(已更新)
  • 提交记录:无(用户自行提交)
  • 更新过的文件或工件src/components/api-chat-coordinator.tssrc/components/Chat/QuestionCard.vueharness/feature_list.json、本文件
  • 已知风险或未解决问题
    • ⚠️ 保留了一处不一致,需用户确认不需要公司信息 仍然发 "不需要" 而不是选项文本。 理由是那个变体的后端 input_help 写的是"不需要则输入'不需要'",按它给的原字符串发最稳。 若统一改成发选项文本,需确认后端也认"不需要公司信息"这个说法。
    • ⚠️ "选中候选后提交文本是否会被当作 refine 而循环"仍未验证(上游持续 provider_failure)
  • 下一步最佳动作:上游恢复后补测有候选时的选中流程

Session 006

  • 日期:2026-09-16
  • 本轮目标:用 status 字段替掉猜测式判定
  • 已完成
    • 承认问题:用户指出 interruptstatus 字段(found/not_found), 查证后确认我完全没用,而是靠三样替代品在猜: kind === 'company_selection'!candidates.length、以及最脆的—— 拿 input_help 的中文文本做正则 /跳过|skip/i 来决定按钮文案。
    • ChatInterruptPayload.status 类型(含 found / not_found 与一一对应关系的注释)。
    • 判定改为以 status 为权威信号:showCandidateList = hasCandidates && status !== 'not_found'。 用户确认另外两种组合(company_selection+not_foundcompany_need+found)不会出现。
    • 删掉 input_help 正则嗅探,跳过选项文案改由状态决定。
    • 保留 result.error 分支(查无 vs 查询失败要分开说),status 缺失时退回"有没有候选"兼容旧版本。
  • 运行过的验证
    • npm run build 通过
    • 六种组合断言:found+有候选 / not_found+无候选 / 无status两种 / found但0候选 / not_found但带候选
    • 真实后端:kind=company_need status=not_found → 输入框在上 + [跳过公司查询] + 问题字段为空(已去重)
  • 已记录证据:见 feature_list.jsoncompany-interrupt-status-matching
  • 提交记录:无(用户自行提交)
  • 更新过的文件或工件src/components/api-chat-coordinator.tsharness/feature_list.json、本文件
  • 已知风险或未解决问题
    • ⚠️ 本轮自己引入过一次回归并已修复:重构时把兜底写成 question || '未查询到匹配的公司。', 而"正文已展示则清空问题"的去重逻辑把 question 置空后,被这个 || 又填回来了,等于去重失效。 已改为用 questionAlreadyShown 标志显式保留去重结果。教训:清空哨兵值时,后续的 || 兜底会把它复活。
    • ⚠️ "选中候选后提交文本是否会被当作 refine 而循环"仍未验证(上游持续 provider_failure)
  • 下一步最佳动作:上游恢复后补测有候选时的选中流程

Session 007

  • 日期:2026-09-16
  • 本轮目标:补齐 interrupt 的全部状态处理
  • 已完成
    • 用户给出补问状态的完整清单(后端确认),共四种情况,我此前只处理了两种: | kind | status | 含义 | |---|---|---| | company_selection | found | 有候选待选择 | | company_need | not_found | 未找到候选 | | company_need | failed | 查询服务失败 ← 未处理 | | company_need | 无 status | 初次询问是否需要公司信息 ← 未处理 |
    • 修正的两个错误
    • failed 此前会落进 not_found 分支,问题文案被填成"未查询到匹配的公司"—— 把失败说成了查无,违反接口文档"空候选与查询失败需分别展示""不把空候选说成查无公司"。 现在 failed 独立分支,文案说明失败原因(result.errordescribeCompanyError 翻译), 无 error 时退回"公司查询服务暂时失败"。
    • 无 status(初次询问)此前会给"跳过公司查询",现改为"不需要公司信息"(→不需要), 与该变体后端 input_help 的"不需要则输入'不需要'"一致。
    • 抽出 fallbackQuestion(),统一处理"已去重则保持为空"的逻辑,避免再次被 || 兜底复活。
  • 运行过的验证
    • npm run build 通过
    • 四种情况逐一断言,输出符合预期(含输入框位置、选项、问题文案)
    • 专项核对:failed 文案不含"未查询到";not_found 文案为"未查询到匹配的公司"
    • 去重回归:正文已含该段 → 问题字段仍为空
  • 已记录证据docs/reference/README.md 新增「interrupt 的全部状态」表; 见 feature_list.jsoncompany-interrupt-status-matching(已更新)
  • 提交记录:无(用户自行提交)
  • 更新过的文件或工件src/components/api-chat-coordinator.tsdocs/reference/README.mdharness/feature_list.json、本文件
  • 已知风险或未解决问题
    • ⚠️ failed 与"无 status 初次询问"两种情况只有静态断言,未在真实后端遇到过 (上游持续 provider_failure,实际只观测到 not_found)。真实出现时需确认文案是否合适。
    • ⚠️ "选中候选后提交文本是否会被当作 refine 而循环"仍未验证
  • 下一步最佳动作:上游恢复后补测有候选时的选中流程

Session 008

  • 日期:2026-09-16
  • 本轮目标:写 README 项目概述、首次推送远程、修正 CLAUDE.md 的失实描述
  • 已完成
    • README 重写为项目概述:业务能力、技术栈、对话接口的两条关键约束 (请求体只收两个字段 / 前端经适配层不直接渲染新协议)、命令与环境变量、 目录结构、harness 入口,以及用户指定的后续改动参考规范 (「华新镇产权单位和企业信息采集小程序」20260909 的升级清单全文)
    • 首次提交并推送到 http://47.103.92.60:3003/skyversation/zhaoshang_client_ui.git (远程原为空仓库)。107 个文件、64595 行。 按用户确认排除 .claude/settings.local.json(本机权限配置)与 stats.html (965KB 构建产物),两者已加入 .gitignore
    • 修正 CLAUDE.md 的失实描述(用户批准后执行,逐条核实过): 端口 8082→8083;删除"Vuex 4"(项目无状态管理库);删除不存在的 stream-message-coordinator-v2.tssrc/hooks/;修正 src/network/api/ 清单; 修正"3D avatar rendering"(three 仅用于粒子背景);修正主壳描述(BusinessAssistant.vue 只是 PC/移动端切换);CI/CD 段说明仓库内无 .gitlab-ci.yml;代理表补 /chat-api
  • 运行过的验证
    • git ls-remote origin 与本地 HEAD 一致(08524fb
    • 修正后 grep 复查:无 8082 / Vuex / stream-message-coordinator-v2 / src/hooks / gitlab-ci 等过时表述残留(剩余匹配均为"没有…"这类修正后的否定陈述)
  • 已记录证据:提交历史 08524fb(基线)、975dee9(文档修正)
  • 提交记录08524fb975dee9 均已推送 origin/main
  • 更新过的文件或工件README.mdCLAUDE.md.gitignore、本文件
  • 已知风险或未解决问题
    • ⚠️ 两个功能性问题仍挂着(非本轮引入): ①"选中候选后提交文本是否会被当作 refine 而循环"从未验证(上游持续 provider_failure, status: found 一次都没出现过);②answer.textsummary.text 首句重复, 已确认是后端内容重复(非前端渲染),待后端修或前端去重
    • .env.* 已入库,含 VITE_APP_KEY / VITE_STREAM_TOKEN。这类 VITE_ 变量本就会被 编译进前端产物、对访问者可见,不属额外泄露;但若安全规范要求不入库,现在改成本最低 (历史仅两个提交)
    • 提交者身份用的是本机 git 配置(gongtianxiao 1091877844@qq.com
  • 下一步最佳动作:等上游恢复后补测有候选的选中流程;确认 summary 重复由后端修还是前端去重

Session 009

  • 日期:2026-09-16
  • 本轮目标:调整入库范围与 README 形态;清除项目内的飞书内容
  • 已完成
    • 发现并纠正一个静默失误:首次提交时 docs/harness/ 未入库。 原因是项目原有.gitignore 第 4、5 行就是 /docs/harness—— 我创建这两个目录时被静默排除,而我在提交信息里却写了"含 docs/reference 与 harness", 属表述失实。经用户确认这两处是用户有意排除的,无需上传。 教训:新建目录后应 git check-ignore 确认是否被忽略,不要假定 git add -A 会带上。
    • CLAUDE.mdagents.md 按用户要求改为不入库(加入 .gitignoregit rm --cached,磁盘文件保留)
    • README 重写:按用户给的参考格式,只保留「按日期的改动记录」 (20260914/15/16 三节)。此前的项目概述、技术栈、目录结构、接口对接说明、 参考规范等全部撤下——用户明确要求项目结束再总结
    • 清除项目内全部飞书内容:README 链接 + src/styles/common/define.lessrule.less 头部指向飞书设计规则的外链(保留文件用途说明,仅去外链)
  • 运行过的验证
    • grep -riE "飞书|feishu|lark|awbm" 在工作区与 git 跟踪文件中零命中
    • npm run build 通过(改动了 .less 文件,确认不影响构建)
    • git status 干净,已推送 origin/main
  • 已记录证据:提交 d8178ca(去跟踪)、a41eec8(README CHANGELOG)、 14bab9f(README 精简 + 去飞书)
  • 提交记录:上述三个提交均已推送 origin/main
  • 更新过的文件或工件.gitignoreREADME.mdsrc/styles/common/define.lesssrc/styles/common/rule.less、本文件
  • 已知风险或未解决问题
    • 飞书链接仍在 git 历史里08524fb 初始提交、a41eec8)。用户确认留着无所谓, 故不做历史重写
    • 两个功能性问题仍挂着:①"选中候选后提交文本是否被当作 refine 而循环"从未验证 (上游持续 provider_failure,status: found 一次都没出现过); ②answer.textsummary.text 首句重复(已确认是后端内容重复,非前端渲染)
  • 下一步最佳动作:等上游恢复后补测有候选的选中流程;确认 summary 重复由后端修还是前端去重

Session 010

  • 日期:2026-09-16
  • 本轮目标:重构 harness 结构(用户反馈"现在的 harness 不是很好",要求结构更标准清晰、 docs 收进 harness、新建 tools 目录,参考资源库继续完善)
  • 已完成

    • 结构调整(参考资源库的 OpenAI 高级骨架 + "短入口,深链接"原则):

      harness/
      ├── README.md          结构导航(入口)
      ├── progress.md        ★ 进度日志(原 claude-progress.md 更名)
      ├── feature_list.json  ★ 功能清单
      ├── init.sh
      ├── tools/             ← 新增:临时脚本(以前写在项目根目录、用完就删)
      ├── docs/              ← 原仓库根的 docs/ 整体迁入
      │   ├── architecture.md   ← 新增:从 CLAUDE.md 拆出的架构深内容
      │   ├── quality.md        ← 新增:质量评分(按领域 + 架构层)
      │   ├── plans/            ← 新增:执行计划(active / done / tech-debt)
      │   └── reference/        ← 接口参考(原样迁入,4 份)
      └── sops/              ← 流程(原散落的三个文件归入并补两份)
      ├── session-start.md   ← 新增:开工流程
      ├── session-end.md     ← 原 clean-state-checklist.md
      ├── handoff.md         ← 原 session-handoff.md
      ├── verification.md    ← 新增:验证流程
      └── evaluator-rubric.md← 原文件迁入
      
    • CLAUDE.md 瘦身为简短入口:只留开工步骤、规则、完成门槛、速览与索引表; 架构细节整体移入 harness/docs/architecture.md

    • 把已知未决项沉淀为 docs/plans/tech-debt.md(5 条): ①公司候选提交文本 vs 序号(可能死循环,从未验证)②answer/summary 首句重复 ③文件上传待定 ④部门筛选在新数据下筛不出(刻意保持,勿当 bug 修) ⑤Question.message 字段定义了却没渲染

    • tools/README.md 写清了为什么要有这个目录:以前脚本写在项目根、用完即删, 导致根目录被污染、好用的脚本丢失、当时的验证方式无迹可查

  • 运行过的验证

    • npm run build 通过(结构改动只涉及文档与目录)
    • 路径一致性排查:更新了 4 处指向旧路径的引用 (tech-debt / quality / sops/verification / tools); progress.mdSession 000–009 的历史记录保留旧路径不改——那是当时的事实
  • 已记录证据:本文件 Session 010;harness/README.md 结构表

  • 提交记录:无(harness/CLAUDE.md 均不入库)

  • 更新过的文件或工件:整个 harness/ 目录重构;根 CLAUDE.md 重写; 原根 docs/ 迁入 harness/docs/

  • 已知风险或未解决问题

    • ⚠️ 上一轮的教训要盯住:上次建了 7 个文件,其中 4 个建完就没再用过。 这次新增了 quality.mdplans/sops/session-start.mdsops/verification.md同样有过期无人用的风险。判断它们是否有用的标准:下一轮开工时是否真的照着走了。 若某个文件连续几轮没被读过,就该删掉或合并——harness 简化是常规工作
    • harness/docs/harness/ 均不入库(用户要求),团队 clone 看不到; 入库的改动记录在根 README.md
  • 下一步最佳动作:等上游公司数据服务恢复后,补测 tech-debt.md #1(有候选时的选中流程)

Session 011

  • 日期:2026-09-16
  • 本轮目标:详情面板的「返回政策列表」按惠企政策区分
  • 已完成
    • 明确"惠企政策"的定义:= policies_public.v1.json 里的政策 (实测 262 条申报事项 / 38 个政策,由 index.html 预加载到 globalThis.policiesPublic)。 并验证了它与后端字段同源:后端 is_policy_library=true 的那条标题, 在库中能匹配到(该政策名下有 44 条申报事项)。
    • 判定采用"两者结合":优先用接口字段 is_policy_library(精确、无需匹配), 缺失时回退到"标题能在库里找到"。标题匹配做了归一化—— 库里带《》、卡片标题不带,另有全半角括号、连接符、空白差异。
    • 详情面板:顶部「返回政策列表」改为 v-if="selectedIsPolicyLibrary", 非惠企政策不显示该入口。
    • 列表新增数据源维度 listSourcecards | library): 卡片区「更多 >>」→ cards(维持卡片数据,不动); 惠企政策详情「返回」→ library(惠企政策库,本轮匹配到的惠企政策置顶)。
    • 库列表复用既有列表模板即可渲染:normalizePolicy 能吃 name/department/declaration_item,且 auto_granted 会自动出「免申即享」标签。 顺带说明:分类筛选(免申即享)在这个数据源上是真正可用的—— 它就是为这个库存设计的字段(此前在卡片数据上必然失效)。
    • 纯逻辑抽到 src/components/Chat/policy-library-utils.ts, 让验证脚本引用同一份实现而非复制——复制会漂移,测的就不是真代码。
    • 🐛 修掉自己写出的一个缓存 bug(重要教训):一开始把库数据写成 computed(() => globalThis.policiesPublic)。但 globalThis 的属性不是响应式依赖, computed 会把首次读到的值缓存住 —— 而 policiesPublic 是 index.html 异步 fetch 加载的(且应用不等待它,dataLoaded 事件与它无关、src 里无人监听), 首次读到很可能是空数组,加载完成后也不会更新。 改为 函数 getLibraryPolicies() / getLibraryNameSet(),每次调用现读
  • 运行过的验证
    • npm run build 通过;policy-library-utils.ts 无新增类型错误
    • 新建 harness/tools/verify-policy-library.mjs(13 项断言,全通过): 名称归一化、《》差异、判定正例与负例、命中收集、置顶排序(含"无命中时顺序不变")
    • 真实库数据抽样:库里前 3 条政策名均能被判定为惠企政策
  • 已记录证据:本文件 Session 011;harness/tools/verify-policy-library.mjsfeature_list.jsonpolicy-library-back-entry
  • 提交记录:无(本轮改动均入库文件,提交由用户处理)
  • 更新过的文件或工件src/components/Chat/PolicyMatch.vuesrc/components/Chat/policy-library-utils.ts(新增)、 harness/tools/verify-policy-library.mjs(新增)、 harness/tools/_policy-library-utils.mjs(esbuild 产物,供脚本 import)、 harness/feature_list.json、根 README.md
  • 已知风险或未解决问题
    • ⚠️ 未在浏览器实测:判定与排序逻辑有脚本断言、构建通过,但"点开非惠企政策卡片时 按钮确实消失""点返回后列表确实是 262 条且匹配项在最前"仍需人工看一眼。
    • ⚠️ 残留风险:库列表为空policiesPublic 异步加载且应用不等待它。 已修掉 computed 缓存问题(现读),但如果用户在下载完成前(约一秒内)就点开 惠企政策详情并点「返回」,列表会是空的。实测时请留意这一点; 若实际会发生,可加一个"库未就绪时短暂重试"的兜底。
    • harness/tools/_policy-library-utils.mjs 是 esbuild 产物,改了源文件要重新打包 才能在脚本里生效(脚本顶部已注明)。
  • 下一步最佳动作:浏览器实测上述两点;tech-debt.md #1(上游恢复后补测选中流程)

Session 012

  • 日期:2026-09-16
  • 本轮目标:修「政策事项列表」入口行为;按钮改名
  • 已完成
    • 按钮改名:详情面板的「返回政策列表」→「返回政策事项列表
    • 统一列表入口(用户明确要求:"政策事项列表不论从哪进,点更多进和点返回政策事项列表进, 效果是一样的")。上一轮我做成了两套数据源(listSource: cards | library: 卡片「更多」看卡片数据、详情「返回」看惠企政策库),这是理解错了—— 用户要的是一个列表、两个入口。
    • 两个入口合并为 openPolicyList(),模板两处按钮都指向它
    • 删掉 listSource
    • 列表统一来自惠企政策库;库未加载完时退回落卡片数据,避免空白
    • 用户反馈的现象:"点更多进去只看到一条,且那一条正是我刚看过的那条"。 成因已定位:筛选状态在两个入口间共享(部门/分类/关键词),而两套数据源的 部门写法不同(库里是简称"区科委",卡片是全称"青浦区科学技术委员会"), 筛选在卡片数据上几乎筛不出东西。统一数据源后两边行为一致,该偏差消失。
  • 运行过的验证
    • npm run build 通过
    • harness/tools/verify-policy-library.mjs 13/13 通过(判定与置顶逻辑未受影响)
    • 残留检查:listSource / openLibraryList / openList 均无残留
    • 接线检查:模板两处按钮(第 29 行卡片「更多」、第 220 行详情「返回」)都指向 openPolicyList
    • 筛选改动来源检查:searchKeyword / currentDeptFilter / currentFilterOption 只由用户点击触发,无程序写入
  • 已记录证据:本文件 Session 012;根 README.md 的 20260916 节已同步更正
  • 提交记录:无(由用户处理)
  • 更新过的文件或工件src/components/Chat/PolicyMatch.vue、根 README.mdharness/feature_list.json、本文件
  • 已知风险或未解决问题
    • ⚠️ 仍需浏览器实测:统一后点「更多」应看到 262 条的库列表、匹配项在最前
    • ⚠️ 筛选状态跨入口保留(用户未要求重置):若用户曾在列表里选过部门/填过关键词, 之后再从卡片「更多」进来,仍会带着该筛选 —— 现在两个入口一致,但可能仍显得"条目少"。 若实际体验不佳,可加"打开列表时重置筛选"。
    • ⚠️ 库未加载完时退回落卡片数据,这是临时的兜底,不是设计意图
  • 下一步最佳动作:浏览器实测两个入口;tech-debt.md #1(上游恢复后补测选中流程)

Session 013

  • 日期:2026-09-16
  • 本轮目标:改提交约定(用户要求"每次修改完都要提交")
  • 已完成
    • 约定变更:此前是"由用户自行提交"(见 Session 008 计划阶段用户的明确要求), 现改为每次改动完成即提交并推送到 origin
    • CLAUDE.md 新增「提交约定」一节:最低要求(npm run build 通过 + 已更新进度日志与功能清单)、提交信息写法(中文,说清改了什么为什么, 结尾带 Co-Authored-By)、命令示例;并注明 harness/CLAUDE.mdagents.md 不入库是预期行为
    • sops/session-end.md 的逐项检查首项改为"已提交并推送"
    • sops/handoff.md 的常用命令补上提交推送
    • 按新约定提交并推送了上一轮的改动(103e53b
  • 运行过的验证
    • npm run build 通过
    • git push 成功;git log 确认提交已上远程
  • 已记录证据:提交 103e53b;本文件 Session 013
  • 提交记录103e53b 已推送 origin/main(本条记录本身因 harness/ 不入库,不进提交)
  • 更新过的文件或工件:根 CLAUDE.mdharness/sops/session-end.mdharness/sops/handoff.md、本文件
  • 已知风险或未解决问题
    • ⚠️ 提交约定与"记录不入库"存在天然缝隙harness/CLAUDE.md.gitignore 排除, 所以"每次改动都提交"实际上只覆盖入库文件;状态记录与工作规则不会进提交。 这是用户有意的安排(Session 009 确认过),不是疏漏。
    • ⚠️ 仍待浏览器实测:政策事项列表两个入口是否都出 262 条、匹配项是否置顶(Session 012)
  • 下一步最佳动作:浏览器实测政策事项列表;tech-debt.md #1(上游恢复后补测选中流程)

Session 014

  • 日期:2026-09-16
  • 本轮目标:政策事项列表进入时重置筛选(用户要求)
  • 已完成
    • openPolicyList()每次进入都重置筛选:页码归 1、清空关键词、 部门回"全部部门"、分类回"所有分类"。
    • 这是 Session 012 那个"列表只剩一条"的根治:此前筛选跨入口保留, 而库里部门是简称、卡片是全角写法的差异会让筛选几乎筛不出东西。 上一轮统一了数据源(两边一致),这一轮把残留也消掉了。
    • 副作用说明:重置会连带触发 watch([currentFilterOption, currentDeptFilter, searchKeyword])watch(currentPage),导致列表被重复构建一次——两次结果相同,可接受,未做去抖。
  • 运行过的验证
    • npm run build 通过
    • 代码核对:openPolicyList 中四个重置项齐备;两个 entry(模板第 29 行的卡片「更多」、 第 220 行的详情「返回」)都走它
  • 已记录证据:本文件 Session 014;feature_list.jsonpolicy-library-back-entry; 根 README.md 的 20260916 节
  • 提交记录:见下条提交(本轮改动含入库文件,已按约定提交推送)
  • 更新过的文件或工件src/components/Chat/PolicyMatch.vue、根 README.mdharness/feature_list.json、本文件
  • 已知风险或未解决问题
    • ⚠️ 仍待浏览器实测:进入列表是否 262 条、匹配项是否置顶、筛选是否已清空
  • 下一步最佳动作:浏览器实测;tech-debt.md #1(上游恢复后补测选中流程)

Session 015

  • 日期:2026-09-16
  • 本轮目标:加快打字速度(用户反馈"偏慢",要求"参考市面上主流 AI 打字速度")
  • 已完成
    • 先查主流实测数据再动手,不凭感觉调: Claude 3.5 约 59.8 字符/秒、DeepSeek-R1 约 75.5、GPT-5.5 约 62 token/秒; 主流区间 50~120 字符/秒(快速模型取上沿)。
    • 原值 2 字/33ms = 61 字符/秒——数值上正好等于 Claude 3.5,并不算慢。 但用户仍感觉慢,原因已定位:主流产品是模型边生成边流出,用户感知的等待约等于 生成时间;而本项目后端一次返回整段(适配层在 done 时一次性输出), 打字机是纯额外延迟——同样 60 字符/秒会比主流慢一整段。
    • 因此取主流区间上沿typingCharsPerTick 2 → 4(≈121 字符/秒), 积压加速档位同步加倍(8/12/16/20/24)。
    • ⚠️ 未按字面设成 10:10 字/33ms ≈ 303 字符/秒,远超主流区间(最快约 120)。 已将依据写进代码注释,并告知用户"要更快说一声"。
  • 运行过的验证
    • npm run build 通过
    • 速度换算核对:改前 61、改后 121 字符/秒;各档位 242~727 字符/秒
  • 已记录证据:本文件 Session 015;根 README.md 的 20260916 节; 代码注释中写明基准来源与取值理由
  • 提交记录:见下条提交(已按约定提交推送)
  • 更新过的文件或工件src/components/Chat/TextContent.vue、根 README.md、本文件
  • 已知风险或未解决问题
    • ⚠️ 仍待浏览器实测:121 字符/秒的观感是否合适。数值对齐了主流,但"快慢"终归是主观的, 用户若仍觉得慢或反过来觉得太跳,都只需改 typingCharsPerTick 一个数。
    • ⚠️ 影响打字速度的其实有两处(TextContent 的逐字机 + BusinessRecordgapTime = 30), 本轮只动了前者。若实测仍偏慢,下一处该看 BusinessRecord.vuegapTime
  • 下一步最佳动作:浏览器实测打字速度观感

Session 016

  • 日期:2026-09-17
  • 本轮目标:修「切会话就显示请求已取消」(用户报告的 bug)
  • 已完成
    • 根因定位(用户先要我分析、不要改代码): 两处「内容是否为空」的判空口径不一致—— normalizeSessionHistorycontent.trim()<scope> 进度块有内容), isInterruptedEmptyScopeMessagestripScopeBlocks()(进度块不算内容)。 同一段内容一处认为「有」、一处认为「没有」。 新协议的正文要等 done 才写入,处理期间内容只有进度块, 把这个缝隙从偶发放大成「一切会话就出现」。
    • 判定责任方:前端(用户问「该前端还是后端改」)。三条理由: ①两处判空都在前端;②<scope> 标记是适配层发明的、后端看不见; ③后端能做的是改成流式(让回答更早出现),那是体验优化、是掩盖不是修复。
    • 修法(用户选定「统一判空口径」):抽出唯一的判空函数 hasVisibleMessageContent()src/utils/interrupted-message.ts), 归一化与渲染层都改用它(渲染层顺带把重复的两处内联 strip 也收敛了)。
    • 新增回归脚本 harness/tools/verify-empty-content-agreement.mjs: 这个 bug 的本质是「两处不一致」,所以断言的不变量就是 两处必须一致(9 例基准 + 18 例一致性 + 3 例回归 = 30 项)。
  • 运行过的验证
    • npm run build 通过
    • 验证脚本 30/30 通过;修复前「只有进度块」那一行会是 渲染=true / 归一化=false(即不一致),现在两处一致
    • 覆盖用例含边界:空串、空白、1 个进度块、多个进度块、只有 silence、 进度块+正文、正文+进度块、只有 POLICY_TABLE 标记
  • 已记录证据:本文件 Session 016;feature_list.jsonempty-content-agreementharness/tools/verify-empty-content-agreement.mjs;根 README.md 的 20260917 节
  • 提交记录:见下条提交(已按约定提交推送)
  • 更新过的文件或工件src/utils/interrupted-message.tssrc/components/business-assistant/shared.tsharness/tools/verify-empty-content-agreement.mjs(新增,含 esbuild 入口 _entry-empty-content.ts)、 根 README.mdharness/feature_list.json、本文件
  • 已知风险或未解决问题
    • ⚠️ 观感变化需确认:原本显示「请求已取消」的场景,现在可能显示 「会话已经取消」(CANCELLED_SESSION_TEXT)。这是代码库里「消息结束但无内容」的 既有措辞,且只在会话未在生成中时触发(switchSession 对正在生成的会话会跳过归一化)。 若该措辞也不合适,需另定文案或改为不落文案。
    • ⚠️ 本轮我一度把 README 的新日期节插到了旧日期前面,违反用户要求的时间正序, 已自行发现并修正(顺序:20260914 → 20260915 → 20260916 → 20260917)。
  • 下一步最佳动作:浏览器实测切会话是否还会出现该提示

Session 017

  • 日期:2026-09-17
  • 本轮目标:删除详情面板的「原文引用」板块(用户要求)
  • 已完成
    • 注释掉模板中的「原文引用」板块(PolicyMatch.vue 第 406-418 行,用 <!-- --> 整块包住)
    • 注释掉随之无用的 selectedCitations 计算属性
    • 按项目约定用注释而非直接删除(第一条指令就是「原代码注释掉,不要直接删除」), 并加了 [已移除] 标记说明原因
    • 检查过没有孤儿 import:resolveFieldCitations 本来就没被 PolicyMatch 引入, selectedCitations 也只被这个板块使用
    • 详情面板现在是两个区块:条件核验、匹配依据
  • 运行过的验证
    • npm run build 通过
    • 读回源码确认第 406-418 行确实被 <!-- --> 包住(grep 分不清注释内外,所以直接看了原文)
  • 已记录证据:本文件 Session 017;feature_list.jsonpolicy-detail-blocks(已改为两块); 根 README.md 的 20260917 节
  • 提交记录:见下条提交(已按约定提交推送)
  • 更新过的文件或工件src/components/Chat/PolicyMatch.vue、根 README.mdharness/feature_list.json、本文件
  • 已知风险或未解决问题
    • ⚠️ 未实测:需浏览器确认详情面板底部只剩两个区块
    • ⚠️ 链路上的两个未决项仍在(与本轮无关): ①tech-debt.md #1 —— 公司候选提交文本是否会被当作 refine 而循环,从未验证 (上游持续 provider_failure,status: found 一次都没出现过) ②政策事项列表的「政策名称」跳转 —— 库里 262 条 apply_link 全空, 仅 21 条能从 资源申请备注 取到可用外网地址(另有 25 条指向内网登录页); 已向用户说明,等其定方向
  • 下一步最佳动作:浏览器实测本轮改动;定政策名称跳转的方案

Session 018

  • 日期:2026-09-17
  • 本轮目标:政策事项列表里「政策名称」支持点击跳转
  • 已完成
    • 用户给出地址规则:https://zwdt.sh.gov.cn/qykj/shell_oc_policy_zq/policy/policy-detail?id=市级政策id
    • 数据核实data.市级政策id261 / 262 条(40 个不同 id), 只有 1 条缺 —— 覆盖率远好于之前考察的两个来源:
    • apply_link 字段:262 条全为空
    • 资源申请备注 里的 URL:只有 46 条有,且25 条指向内网登录页http://10.235.238.34:7202/imanage/login,用户打不开), 可用外网地址仅 21 条;且同名政策下的 URL 是按申报事项分别对应的, 不能互相借(借了会指向别的申报事项的页面)
    • 新增 buildPolicyDetailUrl()src/components/Chat/policy-library-utils.ts (纯逻辑,可被验证脚本引用);没有 id 时返回空串——不猜, 宁可没有链接也不给打不开的地址
    • 接入 normalizePolicyapply_link 兜底链:卡片的 apply_link 优先, 库条目的 市级政策id 兜底
  • 运行过的验证
    • npm run build 通过
    • harness/tools/verify-policy-library.mjs 扩到 21 项断言,全通过: 新增 7 项地址拼接用例(有 id / 无 id / data 缺失 / null / 空白 id / 去空格 / 特殊字符编码)
    • 真实库覆盖率断言(261/262)
  • 已记录证据:本文件 Session 018;harness/tools/verify-policy-library.mjsfeature_list.jsonpolicy-library-back-entry;根 README.md 的 20260917 节
  • 提交记录:见下条提交(已按约定提交推送)
  • 更新过的文件或工件src/components/Chat/policy-library-utils.tssrc/components/Chat/PolicyMatch.vueharness/tools/verify-policy-library.mjs、 根 README.mdharness/feature_list.json、本文件
  • 已知风险或未解决问题
    • ⚠️ 未实测:需浏览器确认从列表点进详情后「政策名称」确实是可点链接、且跳转地址正确
    • ⚠️ 只有 1 条(《上海市青浦区加强知识产权保护…》下的 「支持知识产权托管 -获批上海市知识产权托管项目」)没有 市级政策id, 该条不显示链接
    • ⚠️ 卡片路径的 apply_link 仍来自后端下发的 source url,与库路径的拼接地址 是两套来源 —— 若后端统一提供,可简化为一套
  • 下一步最佳动作:浏览器实测跳转;tech-debt.md #1(上游恢复后补测选中流程)

Session 019

  • 日期:2026-09-17
  • 本轮目标:修用户报告的两个问题
  • 已完成
    • 思考中卡片的阶段文案突然换行 —— 机制已定位: ScopeContent卡片宽度切样式(isMaxWidthReached = cardWidth >= parentWidth - ε): 没撑满容器时标题 nowrap + 省略号,撑满时整块切成 pre-wrap。 原协议标题是固定的短句"正在思考中...",永不撑满、从不翻转; 新协议我把整条阶段文案放进了标题(长得多),撑满即翻 → 突然换行、卡片高度跳动。 改法:把原本同时管标题与正文的那条规则拆开 —— 正文保持 pre-wrap(它是为多行设计的), 标题恒定 nowrap + 省略号。用户选的就是「一律不换行」。
    • ⚠️ 政策事项列表点进详情时「政策名称」是纯文本 —— 未定位到原因,需用户配合。 已排除的:
    • 源码 / dev server / 打包产物三处都确认含 buildPolicyDetailUrl,不是没编译进去
    • apply_link 的编译结果正确:item.apply_link || item['政策原文地址'] || item.data['政策原文地址'] || buildPolicyDetailUrl(item) || ""
    • 模板分支正确(v-if="selectedPolicyItem?.apply_link"<a>,否则纯文本)
    • .policy-link 样式正确(紫色 + 下划线 + 手型)
    • 元信息容器没有 pointer-events 阻挡
    • buildPolicyDetailUrl 对真实库数据覆盖 261/262(脚本断言过) 剩下的两个可能: ① 页面/内存状态陈旧 —— HMR 更新了组件代码,但已构建好的 policyListItems (旧 apply_link 为空)仍在内存里;硬刷新或关掉列表重新进才会用新逻辑重建 ② 当时列表其实走的是兜底路径globalThis.policiesPublic 未加载完 → 退回落卡片数据), 点的是卡片而非库条目 —— openDetailByItem 会匹配到它自己那张卡(index >= 0), 用的是卡片的下发地址;若该来源没有 url,apply_link 就是空 → 纯文本。 这与「库路径」是两套地址来源(见 Session 018 记录的风险项)
  • 运行过的验证
    • npm run build 通过
    • 样式核对:nowrap 分支在 :not(.is-max-width-reached).is-max-width-reached 下各有一份, 正文的 pre-wrap 未被误改
    • 新鲜度核对:dev server 与 dist 产物均含新代码
  • 已记录证据:本文件 Session 019;根 README.md 的 20260917 节
  • 提交记录:见下条提交(已按约定提交推送)
  • 更新过的文件或工件src/components/Chat/ScopeContent.vue、根 README.md、本文件 (feature_list.json 未动:本轮是样式修正,无新增能力)
  • 已知风险或未解决问题
    • ⚠️ 问题 1 未解决(详见上)。需要用户:硬刷新后再看;若仍是纯文本, 告知列表有多少行(262 行 = 库列表;几行 = 兜底走了卡片数据)
    • ⚠️ 改的是 ScopeContent(共享组件)的样式。理由是用户明确要求「一律不换行」, 且该组件当前只有新协议在用(旧协议未使用)
  • 下一步最佳动作:等用户反馈问题 1 的进一步信息

Session 020

  • 日期:2026-09-17
  • 本轮目标:开发环境改用本地政策库 json(用户指示「先用本地的json」)
  • 已完成
    • 顺手定位了 Session 019 遗留的问题 1(「政策名称」是纯文本) —— 真因是两个来源的字段不一致,Session 019 记的两个猜测(陈旧 HMR 状态 / 走了兜底路径) 都不对: | 来源 | 条数 | data.市级政策id | |---|---|---| | 本地 public/merchant-agent/policies_public.v1.json | 262 | 261/262 有 | | 远端 VITE_DOWNLOAD_URL | 327 | 0/327 全无 |
    • 远端那份没有 市级政策id;它有的 policy_id40 位、且与该条 id 相同, 与 市级政策id(24 位,如 676ea4ff61aee302a854ab72是两套 id 体系,拼不出一网通办的地址
    • index.html 原本是远端优先,而远端带 CORS 头(实测 Access-Control-Allow-Origin 回显 https://localhost:8083https://aixq.shqp.gov.cn)→ 浏览器里远端确实赢了 → buildPolicyDetailUrl 取不到 id → 返回空串 → 「政策名称」纯文本
    • 可验证的预测:改之前浏览器里政策事项列表应是 327 行(不是 262)——Session 019 问过这个数
    • 新增环境变量 VITE_POLICY_LOCAL_FIRST:仅 .env.developmenttrue.env.production / .env.qingpu / .env.test 显式写 false(四份都写,避免占位符不被替换)
    • index.html 改为按该开关排来源顺序:dev 本地优先、远端兜底;生产仍远端优先、本地兜底。 错误日志按来源名打印(不再用 isFallback 布尔反推)
  • 运行过的验证
    • npm run build 通过
    • 生产产物 dist/index.htmlvar localFirst = "false" === "true"生产行为未变
    • dev server:新起的 8084 与用户已在跑的 8083 都是 "true" === "true" (Vite 检测到 .env 变更会自行重启,故 8083 也已生效,无需手动重启)
    • dev server 提供的 /merchant-agent/policies_public.v1.json 与仓库本地文件 sha256 一致1a5127c0…),即 262 条、261 条带 id
    • harness/tools/verify-policy-library.mjs 21/21 通过
    • 新增 harness/tools/check-policy-sources.mjs:对比两个来源,输出 本地 262 / 261 带 id远端 327 / 0 带 id,退出码 0(与预期一致)
  • 已记录证据:本文件 Session 020;harness/tools/check-policy-sources.mjsfeature_list.jsonpolicy-library-back-entry;根 README.md 的 20260917 节
  • 提交记录:见下条提交(已按约定提交推送)
  • 更新过的文件或工件.env.development.env.production.env.qingpu.env.testindex.htmlharness/tools/check-policy-sources.mjs(新增)、根 README.mdharness/feature_list.json、本文件
  • 已知风险或未解决问题
    • ⚠️ dev 用的这份比远端少 65 条申报事项(262 vs 327;仅远端有的政策名 7 个、 仅本地有的 4 个)——这是「先用本地」的代价,生产不受影响
    • ⚠️ 生产环境「政策名称」仍不可点(远端那份没有 id)。这是后端数据问题, 不是前端能修的:需后端在远端补 data.市级政策id(或让远端也带 id)。 开关的移除条件已写进 check-policy-sources.mjs 的输出—— 远端哪天有 id 了,脚本会提醒可以去掉这个开关
    • ⚠️ 两个来源的条数/内容差异不止 id 一个字段,dev 与生产的库数据本来就不同版本, 排查政策库问题时先确认当前加载的是哪一份(控制台会打「加载完成(local/remote):N 条」)
  • 下一步最佳动作:浏览器硬刷新后从政策事项列表点进详情,确认「政策名称」是可点链接; 列表行数应为 262(若仍是 327,说明没走本地那份,需看控制台那行日志)

Session 021

  • 日期:2026-09-17
  • 本轮目标:按用户要求,就「用 DMS 接口替换前端接口」出方案(用户先看方案再定是否可行)
  • 已完成
    • ⚠️ 用户指出的流程问题:本轮我漏执行了 harness 的开工流程。 具体漏项:
    • 没跑 bash harness/init.shsops/session-start.md 第 4 步「跑基础验证」)—— 事后补跑,exit 0 通过
    • 没读 docs/architecture.md —— 根 CLAUDE.md 写明「第一次进这个仓库必读」, 而本会话是第一次进,我跳过了
    • 一处严重失误:列 harness/docs/reference/ 时,ls 的输出里没有 DMS 三份文档, 我据此说「缺失」,随后 find 证明它们存在(mtime 9/16 17:17,不是新拷入的)。 原因不明(疑似输出被截断),但教训明确:不要用单次 ls 的输出当「文件不存在」的证据, 要用 find / test -e 复核
    • 修好 DMS 三份文档里的断链(用户要求:与实际不符就更新文件): 它们是从另一个项目 F:\yysk\AI_zhaoshang\DMS_Data_Migration\harness\docs\ 复制来的, 复制时没带同伴,导致 7 个引用全部指向不存在的位置。已逐一核实外部原件存在后, 把相对链接改为绝对路径,并在每份文档头部加了「引用的 artifacts / tools 不在本仓库」的说明: dms_openapi.json(118KB)、created_columns.jsondms_column_fields.jsoncreate_dms_columns.pyfix_field_alias.pyRUNBOOK.mdSKILL.mdapifox-dms.md —— 8/8 均存在
    • reference/README.md 索引表补上 DMS 三份文档(原索引只列了 api-chat 系列, 而 DMS 文档在里面躺了一整天没人知道),并写明 DMS 栏目当前全空、别当在线数据源用
    • 写了方案 docs/plans/active/dms-api-replacement.md(该目录此前一直空着):
    • 能替换 3 条只读:企业信息(栏目 1888)、会话列表(1887)、会话历史(1889) —— 都是「精确查 + 分页」,与 selectContentList 语义吻合
    • 能替换但要注意 3 条写:会话改名、删会话、反馈(后两条 DMS 用记录 uuid 定位, 而前端用 session_id;delContentById 参数未文档化)
    • 替换不了:对话 SSE、政策列表(无栏目)、企业登录(DMS 依赖它)、 OSS 上传(用途不同)、ASR、卡片媒体/历史
    • 硬前置:DMS 五栏目现在全空code=202),数据迁移(F4-*)没做完不能切
    • 需要用户拍板 4 条(见方案第六节),其中最关键是: DMS 是给前端在线查询用的,还是只做数据底座? 以及前端直连还是后端转发 (直连等于把数据管理 token 发给每个访客,我建议后端转发)
    • 本轮为方案分析,未改任何代码
  • 运行过的验证
    • bash harness/init.sh 通过(exit 0)—— 事后补跑的开工验证
    • 断开链接核验:grep 确认三份 DMS 文档已无残留的 ../artifacts / ../tools / (RUNBOOK.md) 相对引用
    • 外部原件核验:8 个被引用文件逐一 test -e 确认存在(含带空格的 2. DMS-Manage 路径)
    • 前端接口清单:逐个 grep 出调用方与字段消费面(见下)
  • 已记录证据:本文件 Session 021;docs/plans/active/dms-api-replacement.mddocs/reference/README.md 的 DMS 索引节
  • 提交记录 —— 本轮改动(harness/ 下的文档与方案)按 .gitignore 约定不入库, 故无内容可提交。这是预期行为,不是漏提交
  • 更新过的文件或工件docs/reference/DMS_API.mddocs/reference/DMS_COLUMNS.mddocs/reference/DMS_MAPPING.mddocs/reference/README.mddocs/plans/active/dms-api-replacement.md(新增)、本文件
  • 本轮查证到的两个关键事实(方案里用到,也值得单独记住):
    1. 前端只用了 EnterpriseInfo 的 2 个字段namecredit_codeuseEnterpriseAuth.ts:58-59;另外 19 个字段在 src/零引用) → 用 DMS 的企业信息替换不需要字段映射层,比预想的简单
    2. DMS 的「反馈」就在问答记录栏目里:1889 的 13 个字段包含 c_feedback_status / c_feedback_option / c_feedback_remark / c_feedback_at → 前端 POST /chat/feedback 理论上可由 1889 承担
  • 已知风险或未解决问题
    • ⚠️ 方案未获批准,一行代码都没动;feature_list.json未新增条目 (避免在没有决定前把未定的事写成待办)
    • ⚠️ DMS 的读接口(selectContentList 等)在文档里标的是 📄未实测, 本项目从未调用过 —— 方案第 0 步就是先小样本验证
    • ⚠️ ls 漏文件那件事没有查到根因。若再遇到「列目录看不到文件」,先 test -e 复核再下结论
  • 下一步最佳动作:等用户回答方案第六节的 4 个问题(最关键是「DMS 是底座还是在线数据源」); 确认后再拆成 feature_list.json 条目逐项做

Session 022

  • 日期:2026-09-17
  • 本轮目标:会话/问答记录切换到 DMS(用户指令),反馈一并落地
  • 方案docs/plans/active/dms-api-replacement.md 的后续落地; 本轮方案与用户拍板记录见本文件末尾「Step-0 结论」。 用户拍板三条:①反馈写入 DMS;②会话改名/删除一并切;③访客 id 复用埋点 visitorId (c_credit_code = 访客_<assistant_statistics_visitor_id>

Step-0 实测结论(DMS content 接口,此前全是「📄 未实测」)

跑法:node harness/tools/dms-step0-verify.mjs <TOKEN>(14/17 通过)+ dms-delete-probe.mjs三个 FAIL 里一个是我判据写错,两个是真发现

# 结论 影响
1 addContent 不需要 c_id(自动填 0) ✅ 最大阻塞风险解除(模型里 c_id 是 must=true,实测不强校验)
2 addContent 返回记录 uuid,形态是纯字符串{"code":200,"content":"056dbae8-…"} ✅ 新增行后不必再反查一次;不是 content.id 对象
3 c_created_at 不自动填,必须自己写 ⚠️ 写入时要带时间戳
4 时间戳格式:"YYYY-MM-DD HH:mm:ss" 与 ISO 串都接受读回是 epoch 毫秒(数字) 写入用前者;读取按毫秒处理
5 删除不是 delContentById(各种传法都 code=-1/POST 405) ⚠️ 见 #6
6 删除的正确姿势:POST /content/updateAudit,form {columnId, id:<uuid>, state:4} state=4=销毁,返回 200 且行从列表消失。这条文档里完全没有,是本次试出来的
7 长文本(1105 字含 <scope>/POLICY_TABLE/<question-cards> 标记)逐字节往返一致 ✅ 协议标记可原样存 DMS
8 中文+下划线的 c_credit_code 精确 search 命中 ✅ 访客 访客_<uuid> 可查
9 反馈字段(c_feedback_status/option/remark/at)经 updateContent 写入生效 ✅ 反馈方案可行
10 pageSize=200 被接受;写后不带 states 也能查回

⚠️ 测试数据已全部清理(1887/1889 复查均 0 行)。清理过程中发现探测脚本把 1887 的 columnId 用到了 1889 的行上,导致一度残留 1 行,已手工补删。

记录:本文件 Session 022;脚本 harness/tools/dms-step0-verify.mjsdms-delete-probe.mjs

实现(本轮改动)

新增

  • src/network/api/dms/client.ts —— DMS HTTP 层(token 由代理注入、form body、code===200 判定、202 当空、任何失败都不抛异常
  • src/network/api/dms/chat-sessions-dms.ts —— 业务层:upsertDmsSession / upsertDmsRecord / fetchDmsSessions / fetchDmsSessionRecords / deleteDmsSession / writeDmsFeedback, 含顺序链去重(同幂等键的写串行,避免「提交写 question」与「完成补 answer」并发各 add 一行)
  • harness/tools/verify-dms-chat-storage.mjs + _entry-dms.ts(验证脚本,26 项断言)

  • src/network/api/chat-sessions.ts —— 4 个导出换成 DMS 实现,签名与返回类型不变(调用方零改动); RemoteSessionRecord 新增可选 record_id
  • src/components/business-assistant/useBusinessAssistantChat.ts —— 挂钩: sendMessage 提交时 upsertDmsSession + upsertDmsRecord(question)totalResponseanswerclose(停止/断流)也补一次(否则中断的问答只有 question); submitQuestionAnswers 补问路径同样写入(存真实提交文本,不存「已提交企业信息补充」占位); 历史回读用 record_id 回填 AI 消息 id → 从 DMS 读回的历史消息点赞仍命中原始行syncSessionTitleToServer / syncSessionDeleteToServer 去掉登录 guard(访客也写)
  • src/components/Chat/BusinessRecord.vue —— 反馈三函数换 writeDmsFeedback,移除 cardAPI 调用
  • vite.config.ts —— 改用 loadEnv(mode, cwd, "") + 新增 /dms-api/ 代理 (rewrite 补回 /dms 前缀、在代理侧注入 token,浏览器产物里没有 token)
  • src/utils/runtime-config.ts —— 新增 getDmsApiBaseUrl()index.html 注入 globalThis.VITE_DMS_API
  • .env.development —— VITE_DMS_API="/dms-api"VITE_DMS_TARGET
  • .env.development.local不入库.gitignore 已覆盖 *.local)—— DMS_TOKEN

运行过的验证

  • npm run build 通过(改动后重跑)
  • 验证脚本 26/26 通过harness/tools/verify-dms-chat-storage.mjs,打的是真实 DMS, 经 vite 代理、与浏览器同一条路径):会话增/改不重复、问答 question→answer 补写不重复、 长文本含协议标记往返一致、反馈三种状态、删除连带清理、时间戳解析、访客归属
  • 代理链路实测:经 https://localhost:8084/dms-api/... 查询 → 202(token 已注入); 直连 DMS 不带 token → 208 无token → 证明 token 确实只在代理侧
  • DMS 残留复查:1887 / 1889 均 202 空(测试数据已清理干净)

🐛 验证脚本抓到的真 bug(已修)

findRowBy 里写死了 orderBy: [{field:'c_updated_at'}],但栏目 1889 没有这个字段: 按不存在的字段排序 → DMS 报错(返回非标准响应)→ 反查恒空 → 每次 upsert 都判定「不存在」→ 新增,同一问答被写成多行(实测第 4、5 步各多一行)。 修法:findRowBy 不加 orderBy(两栏目字段不同,只有 c_created_at 是共同的)。 fetchDmsSessionsc_updated_at 排序保留 —— 那个字段在 1887 里确实存在

教训:跨栏目复用同一个查询函数时,排序字段必须两个栏目都有

已知风险或未解决问题

  • ⚠️ 未做浏览器实测:写入挂钩挂在 useBusinessAssistantChat(PC/Mobile 共用), 逻辑与 DMS 契约均已用真实 DMS 验证,但「发一条消息 → 库里出现两行」这一步 仍需在浏览器点一遍确认。
  • ⚠️ token 今晚 22:19 过期:过期后写入静默降级(仅控制台 warn), 聊天与本地历史不受影响;需用户提供新 token(替换 .env.development.local 并重启 dev server)。
  • ⚠️ 生产未接.env.production 未配 VITE_DMS_API,需 nginx 做等价转发 (location /dms-api/ { proxy_pass http://121.43.55.7:10081/dms/; proxy_set_header token "…"; }), 且 DMS 的长效凭据方案待与 DMS 侧确认(现在给的是临时 token)。
  • ⚠️ 旧会话不在 DMS:切换后远端会话列表为空(本地 localStorage 历史仍在); F4 数据迁移未开始,本轮不做历史回填。
  • ⚠️ 访客只写不读fetchRemoteSessions 对空 credit_code 直接返回 [](保持原行为), 符合用户「访客也存」的要求;若将来要「访客也能看自己的历史」,需要另开一条读路径。

下一步最佳动作

浏览器实测:发一条消息 → 查 DMS 两栏目应各出现一行(c_session_id=localStorage ba_current_session_id);刷新后列表出现该会话;改名/删除/点赞各验一次。

Session 023

  • 日期:2026-09-17
  • 本轮目标:用户提的三件事 —— ①会话消息的存储粒度 ②为什么默认登录 ③参考官方资料完善 harness
  • 已完成

① 存储粒度改为「一个会话一行」(用户要求)

用户原话:「每次一问一答就会在 DMS 存一条消息,我希望一条消息同时包含同一个会话窗口的所有消息」。 用户三个决策:一行一个会话(1889);反馈存进该行 JSON 里对应那条消息;dev 自动登录去掉。

  • chat-sessions-dms.ts
    • 新增 saveDmsTranscript() 取代 upsertDmsRecord() —— 幂等键从 c_record_id 改为 c_session_id,整段消息序列化成 {version:1, messages:[…]} 存进 c_answerc_question 存首问
    • 写入方式改为「以本地 messages 为准整段重写」,不做读-改-写 → 从根上消除了并发覆盖问题(本地状态就是权威)
    • fetchDmsSessionRecords() 改成从该行 JSON 里还原问答对
    • writeDmsFeedback() 改成「读整行 → 只改命中那条消息 → 写回」,与对话写入 共用 session: 顺序链;定位不到时不新增行(避免库里出现只有反馈没有内容的行)
    • ⚠️ 不存消息级时间戳:本地消息本就没有时间戳,整段重写时现造 Date.now() 会让所有消息的时间都变成最后一次保存的时刻 —— 假数据比没有更糟。顺序由数组顺序表达
  • useBusinessAssistantChat.ts:4 处调用点(sendMessage / totalResponse / close / submitQuestionAnswers)统一换成 saveSessionTranscriptToDms(session)

② 去掉开发环境硬编码账号的自动登录

  • 根因:useEnterpriseAuth.tsisDevLogin = MODE === 'development'无条件用 硬编码的 devLogin token 登录 → 表现为「我没登录也进入登录态」
  • 已整段移除该分支;要企业态请显式带 ?access_token=?credit_code=。 开发环境默认即访客态,正好方便验证访客记录。
  • 影响:dev 下 globalThis.token 不再自动注入,依赖它的旧接口(企业信息等) 需要显式登录才可用 —— 本轮涉及的会话/历史/反馈已全部走 DMS(代理注入 token),不受影响

③ harness 结构对齐官方资料

参考:https://walkinglabs.github.io/learn-harness-engineering/zh/resources/ (该站被网络策略挡了,改从 GitHub raw 取到正文与 OpenAI 高级包结构)

  • docs/plans/docs/exec-plans/done/completed/tech-debt.mdtech-debt-tracker.md(对齐参考资料命名)
  • 新增硬规则并写进入口:计划类文件只能写在 harness/docs/exec-plans/active/。 用户指出的问题属实:本轮的实施计划先前被写到了 Claude Code 自带的 ~/.claude/plans/仓库外),换个会话接手看不到。已把内容落进仓库 (exec-plans/active/dms-chat-storage.md),并在 CLAUDE.mdexec-plans/README.mdsops/session-end.md 三处写明
  • 新增机械约束 harness/tools/validate-harness.mjs:参考资料的「机械约束优先于 口头约定」。校验必备文件、游离的计划文件(仓库根/src/ 下)、 feature_list.json 的假 passing / 重复 id / 多个 in_progressprogress.md 会话编号重复。已实测它能报错(放一个 plan-test.md 后 exit=1)
  • sops/session-start.md 加「4b 校验结构」;sops/session-end.md 加两条收尾项
  • harness/README.md 结构图与「机械约束」一节更新
  • 更正 reference/README.md已过期的「DMS 五个栏目全空、别当在线数据源用」—— 1887/1889 现在正是前端在用的在线数据源

  • 运行过的验证

    • npm run build 通过
    • node harness/tools/verify-dms-chat-storage.mjs <代理> —— 36/36 通过 (新增关键断言:*第二轮后仍是同一行*、*反馈只改命中那条消息*、 *另一条消息未被误改*、*定位不到时不新增行*)
    • node harness/tools/validate-harness.mjs —— 通过;并实测其能报错(exit=1)
    • DMS 残留复查:测试数据已清理(残留 0 行)
  • 已记录证据:本文件 Session 023;docs/exec-plans/active/dms-chat-storage.mdharness/tools/validate-harness.mjs;根 README.md 的 20260917 节

  • 更新过的文件或工件src/network/api/dms/chat-sessions-dms.tssrc/components/business-assistant/useBusinessAssistantChat.tssrc/components/useEnterpriseAuth.tsharness/docs/exec-plans/*(重构)、 harness/tools/{validate-harness.mjs,_entry-dms.ts,verify-dms-chat-storage.mjs}harness/README.mdharness/sops/{session-start,session-end}.mdharness/docs/reference/README.mdharness/docs/quality.mdCLAUDE.md、根 README.md

  • 已知风险或未解决问题

    • ⚠️ 浏览器端到端仍未人工点过(沿用 Session 022 的遗留)
    • ⚠️ 反馈状态刷新后不恢复:JSON 里存着,但 BusinessRecordlocalFeedback 初始恒为 None,没做「从 DMS 读回反馈态」的回填。要做得另接一条线
    • ⚠️ DMS token 今晚 22:19 过期;生产未接(nginx + 长效凭据待定)
    • ⚠️ harness 仍不入库.gitignore 有意排除,Session 009/013 用户确认过)。 参考资料的原则是「计划、质量、技术债和代码一起版本化」,本仓库与之有张力: 团队 clone 看不到 harness。若哪天想改,是一行 .gitignore 的事 —— 但那是用户的决定,没动
  • 下一步最佳动作:浏览器实测(发消息 → DMS 一行 → 刷新 → 列表/历史 → 点赞 → 改名/删除); 另需用户提供新 token(旧的今晚过期)

Session 024

  • 日期:2026-09-17
  • 本轮目标:用户指示「按照参考资料执行」→ 把 harness 纳入版本控制 (这是 Session 023 末尾我提出的那条张力:参考资料的原则是「计划、质量、技术债 和代码一起版本化」,而本仓库此前把 harness/ 排除在 git 之外)
  • 已完成
    • .gitignore 去掉 /harness/CLAUDE.md/agents.md 三条排除, 改为入库;并在原位置留了说明注释(为什么改、何时改)
    • 仍然排除(都是带凭据的本机文件,不能入库): .env.*.local.claude/settings.local.json
    • 新增排除 harness/tools/_*.mjs:那是 esbuild 从 _entry-*.ts 生成的 构建产物,入库会与源码漂移、让人误以为测的是新代码
    • 修掉一个由此暴露的真问题_policy-library-utils.mjs入口文件从来没被保留 (Session 011 生成产物后丢了入口),产物一旦不入库,政策库验证脚本就没人能重跑。 已补 _entry-policy-library.ts,并模拟 fresh clone(删光三个产物 → 按 README 的命令重新生成 → 跑验证)确认全链路可用
    • harness/tools/README.md 补上三个产物逐一可抄的重新生成命令, 并写明 --define:import.meta.env='{}' 这个坑(node 里没有 import.meta.env
    • 改掉三处已过时的「harness 不入库」说明:根 CLAUDE.md 的提交约定、 harness/README.md 的关系表
  • 入库前的凭据扫描(推共享远端前必做):
    • JWT 形态扫描:harness 内只有 legacy/API.md 里两处带 ... 的截断示例,无真实凭据
    • 关键词扫描(password/secret/api_key 等):零命中
    • git check-ignore 逐项核对:.env.development.local(含 DMS token)已排除
    • 入库 35 个文件;内网地址(192.168.2.23、121.43.55.7 等)与仓库中已有的 vite.config.ts.env.* 属同一类,不是新增暴露面
  • 运行过的验证
    • 模拟 fresh clone 全链路:删光 harness/tools/_*.mjs → 按 README 命令重新生成 三个产物 → 四个验证脚本全绿
    • verify-empty-content-agreement.mjs 30/30
    • verify-policy-library.mjs 21/21(用新入口重新生成的产物)
    • verify-dms-chat-storage.mjs 36/36(打真实 DMS)
    • check-policy-sources.mjs 符合预期
    • node harness/tools/validate-harness.mjs 通过
    • npm run build 通过
  • 已记录证据:本文件 Session 024;.gitignoreharness/tools/README.md; 根 README.md 的 20260917 节
  • 更新过的文件或工件.gitignoreCLAUDE.mdharness/README.mdharness/tools/README.mdharness/tools/_entry-policy-library.ts(新增)、 根 README.md、本文件
  • 已知风险或未解决问题
    • ⚠️ 不可逆:harness 的内部工作笔记(对后端问题的直白记录、踩坑过程、 需求取舍)从现在起对团队可见,且已在 git 历史里。此前若要撤回需重写历史
    • ⚠️ 本次提交较大(35 个文件、含 900+ 行的 progress.md),是入库必然的
    • ⚠️ 浏览器端到端仍未人工点过(沿用 Session 022/023 的遗留)
  • 下一步最佳动作:浏览器实测;以及「harness 入库后」的一个新问题: progress.md 会持续变大(已 1000+ 行),后续可考虑按会话分文件或定期归档, 但那是另一个改动,本轮不做

Session 025

  • 日期:2026-09-17
  • 本轮目标:修用户报的 bug ——「两张卡片政策名相同、申报事项不同,点『查看详情』内容一样」
  • 根因读原始 SSE 载荷 + 数据实证,不是靠猜):
    1. 后端 item 事件里:id / policy_id 恒为 undefined;每条唯一的信息在 data.title(形如「政策名 - 申报事项」,如 …通知 - 创业开办费补贴)与 data.source_id
    2. 适配层 toPolicyTableItemapi-chat-coordinator.ts:322-337)把 title 取自 card.name.text只有政策名),并把 declaration_item 也设成同一个值 —— 每条唯一的 data.title 被丢掉了
    3. 于是同一政策的多个申报事项,其 declaration_item 完全相同, 而 openDetailByItem 的兜底匹配正是拿它比 → findIndex 永远命中第 0 张
    4. 实证:用旧逻辑跑同政策两卡片的场景 —— 点第 1 张返回 0、点第 2 张也返回 0 ✓ 与截图现象吻合
  • 修法:把反查逻辑抽成纯函数 resolvePolicyDetailIndex()policy-match-utils.ts),匹配顺序改为:
    1. 对象同一性(卡片区传入的就是 policies 里的那个对象,精确)
    2. 字段比对(id / policy_id / declaration_item)只有唯一命中才采信; 多命中说明这组数据本就区分不了 → 返回 -1,交给「直接展示被点条目自己」的兜底 —— 宁可走兜底,也不张冠李戴
    3. PolicyMatch.vueopenDetailByItem 改用它;未改动任何 UI 文案或样式
  • 运行过的验证
    • npm run build 通过
    • 新增 harness/tools/verify-policy-detail-index.mjs —— 10/10 通过, 关键断言「点第 2 张 → 下标 1(修复前这里会返回 0)」与「字段无法区分时返回 -1」
    • 旧逻辑对照实证(见上,点第 2 张返回 0)
    • 真实载荷探针:harness/tools/probe-policy-cards.mjs(打 item 原始事件, 确认 id/policy_id 为 undefined、card.name.text 两条相同)
  • 已记录证据:本文件 Session 025;harness/tools/verify-policy-detail-index.mjsfeature_list.jsonpolicy-detail-index-mismatch
  • 更新过的文件或工件src/components/Chat/policy-match-utils.tssrc/components/Chat/PolicyMatch.vueharness/tools/_entry-policy-match.ts(新增)、 harness/tools/verify-policy-detail-index.mjs(新增)、 harness/tools/probe-policy-cards.mjs(新增)、harness/tools/_entry-coordinator.ts(新增)、 harness/feature_list.json、根 README.md、本文件
  • 已知风险或未解决问题
    • ⚠️ 浏览器未人工点过:修复的证据是纯函数断言 + 旧逻辑对照 + 构建, 仍需用户点一次确认两张卡片内容确实不同
    • ⚠️ 一个更深的、本次没动的观察:适配层丢掉了 data.title 里的申报事项后缀, 所以详情面板的标题(declaration_item)对同一政策的所有申报事项都一样—— 内容虽然修好了,但用户从标题上仍分不出自己看的是哪个申报事项。 要改就得动显示文案,属于 UI 变更,需用户拍板(已在回复里提出)
    • 遗留:DMS 相关(Session 022/023)的浏览器端到端仍未人工验证
  • 下一步最佳动作:请用户点一次确认;并就「详情面板是否显示具体申报事项」给答复

续:详情面板显示具体申报事项(用户拍板后追加)

用户选择「详情标题显示具体申报事项」。改动:

  • api-chat-coordinator.tstoPolicyTableItemdeclaration_item 改用后端每条唯一的 item.title(同政策多事项时形如「…的通知 - 创业开办费补贴」),不再与卡片标题取同一个值。 卡片标题 title 仍旧取 card.name.text(政策名)—— 卡片区观感不变
  • ⚠️ 连带影响已处理panelData.title(卡片区标题)原先优先取 originalData.declaration_item,改后会连带把卡片标题变成「政策名 - 申报事项」。 已改成优先取 item.title(政策名),卡片区保持原样——用户只批准改详情标题
  • 未处理(有意):buildRecordPolicyList 兜底列表会显示完整申报事项—— 列表本就叫「政策事项列表」,且只在库未加载时走这条路,更正确

新增验证 harness/tools/verify-policy-card-fields.mjs —— 8/8 通过: ①同政策两条的 declaration_item 必须不同 ②卡片标题仍是政策名(观感不变) ③无后缀时退化为政策名不出现空值 ④与 resolvePolicyDetailIndex 配合能直接命中下标 1 (跑这段测试时先写错过夹具:把「无后缀」场景的 item.titlecard.name.text 传成了不同值, 那其实是「有后缀」形态;已修正夹具,不是代码问题

Session 026

  • 日期:2026-09-17
  • 本轮目标:卡片区「更多」改为打开新侧边栏「政策列表」(用户需求)
  • 方案docs/exec-plans/active/policy-card-list-sidebar.md
  • 用户拍板三条:①内容 = 本会话全部卡片(跨消息聚合、去重)②只换「更多」入口 —— 详情面板的「返回政策事项列表」保留旧列表③热门搜索沿用现有 5 词

已完成

  • 新增第 3 种面板模式 cardListpanelMode: "list" | "cardList" | "detail")。 旧列表逻辑靠 fetchPolicyList 开头的 panelMode !== "list" 守卫自然短路openPolicyList / watchers / filteredPolicies 一行未动
  • 会话级卡片聚合(此前不存在):
    • 采集层 useBusinessAssistantChat.ts 新增 provide('collectSessionPolicyCards', …) (放在既有 provide('submitQuestionAnswers', …) 旁,PC/Mobile 零改动, 沿用同一先例)。调用时才读 currentSession → 切会话自动跟随
    • 纯逻辑层 policy-match-utils.ts 新增 5 个纯函数: collectPolicyCardsFromMessages(从 AI 消息 content 重新解析 POLICY_TABLE)、 buildPolicyCardDedupKeydedupePolicyItems(首见优先)、 sortPolicyCardsByMatchScore(降序 + 稳定 tie-break)、matchesPolicyCardKeyword
    • 组装留在 PolicyMatch(复用组件私有 mergeWithPublicPolicies/normalizePolicy); inject 拿不到 provider 时回退本组件卡片,不崩不空白
  • 去重键 = declaration_item(2026-09-17 起 =「政策名 - 申报事项」,每条唯一); 精简格式的占位符「查看政策详情」会退化为「标题|匹配度|匹配理由|id」组合键, 避免把不同卡片错并成一条
  • 模板 6 处改动:更多按钮 → openCardList;背景类/标题按模式分叉; list 与 cardList 共用同一个 body 与全部样式类(CSS 零新增); 筛选块加 v-if="panelMode === 'list'" → cardList 下隐藏部门/分类, 保留搜索框与热门搜索
  • 改写了一条过时注释openPolicyList 顶部原写「卡片区『更多』与详情返回走同一条路径」, 改后已不成立——不改写会误导后续维护者

运行过的验证

  • npm run build 通过
  • 新增 harness/tools/verify-policy-card-list.mjs —— 31/31 通过: 跨消息聚合并集+保序、用户消息跳过、坏 JSON/空输入/null 不抛错、 去重键主键与占位符退化、首见优先(保留首见的 match_score 而非后来的)、 降序+同分稳定、六个检索面+大小写+空词、端到端组合
  • 其余脚本全绿:卡字段 8/8、详情反查 10/10、政策库 21/21、判空口径 30/30、DMS 36/36
  • node harness/tools/validate-harness.mjs 通过

已记录证据

本文件 Session 026;docs/exec-plans/active/policy-card-list-sidebar.md(计划已落库); harness/tools/verify-policy-card-list.mjsfeature_list.jsonpolicy-card-list-sidebar

更新过的文件或工件

src/components/Chat/policy-match-utils.tssrc/components/Chat/PolicyMatch.vuesrc/components/business-assistant/useBusinessAssistantChat.tsharness/tools/{_entry-policy-cards.ts,verify-policy-card-list.mjs}(新增)、 harness/docs/exec-plans/active/policy-card-list-sidebar.md(新增)、 harness/feature_list.json、根 README.md、本文件

已知风险或未解决问题

  • ⚠️ 浏览器端到端未人工点过:验收要点是「标题政策列表 / 有搜索+热门词 / 无筛选 / 跨两轮问答聚合且去重 / 点条目详情一致 / 「返回政策事项列表」仍是旧列表」
  • ⚠️ 两套列表并存是用户明确选择(只换「更多」入口);openPolicyList 现在只由 详情页的返回按钮使用,若将来也不需要了,可连同筛选状态一并清理
  • ⚠️ 聚合去重键依赖 declaration_item 的唯一性——若后端将来改回只下发政策名, 同政策的多个申报事项又会被并成一条(届时需换键,如 source_id

下一步最佳动作

浏览器实测上述验收要点;若通过,本条目转 passing

Session 027

  • 日期:2026-09-17
  • 本轮目标:用户两条指示 —— ①DMS 存储粒度改回初版「一问一答一条」计划文件更新不及时 (实现完的还堆在 active/,没移进 completed/
  • 已完成

① 存储粒度改回「一问一答一条」(用户要求)

  • chat-sessions-dms.ts:撤掉「一会话一行 + 整段对话 JSON」的实现,恢复 upsertDmsRecord()(幂等键 c_record_idc_question / c_answer 各存一半); 删除 DmsTranscriptMessage / serializeTranscript / parseTranscript
  • fetchDmsSessionRecords() 改成按 c_session_id 查、按 c_created_at 正序, 逐行映射(record_id 回填 c_record_id,反馈闭环保留)
  • writeDmsFeedback() 改回「按 c_record_id 找行 → 更新 c_feedback_* 列」, 不再做 JSON 读-改-写;定位不到时不新建行(避免造出只有反馈没有问答的空记录)
  • deleteDmsSession() 改回「删该会话名下全部问答行 + 会话行」
  • useBusinessAssistantChat.ts 四处挂钩(sendMessage / totalResponse / close / submitQuestionAnswers)改为调 saveTurnToDms()(提交写 question、完成补 answer); Session 026 加的聚合 provider 原样保留
  • ⚠️ 改回的理由(写进代码注释与计划归档):一问一答一条更贴近库表原本语义 (chat_record 就是一行一轮问答),也便于按轮次检索/统计

② 计划归档 + 加机械约束(用户指出)

  • 用户指出属实completed/ 完全是空的,三个计划全在 active/, 其中 dms-chat-storagepolicy-card-list-sidebar 早已实现
  • 已归档两个到 completed/,并更新其状态头为实际结果(含「浏览器人工点验未做」的显式标注)
  • dms-api-replacement.md 保留在 active,但加了「落地进度」表: 1887/1889 相关的 3 读 3 写已实现,企业信息(1888)未做——如实标出,不假装全做完
  • validate-harness.mjs 加了两条检查(机械约束,不靠人记):
    1. active/ 里的计划若状态写着「✅/已完成」→ 提醒该归档
    2. completed/ 里的计划若状态仍写「🔄/进行中/待拍板」→ 提醒状态与位置不一致
    3. 实测会响:把 completed 里的文件复制回 active,校验器立刻报出该文件
  • harness/docs/exec-plans/README.md 的「机械检查」一节已说明该脚本会管归档

  • 运行过的验证

    • npm run build 通过
    • node harness/tools/verify-dms-chat-storage.mjs 36/36 通过(已改为「一问一答一条」的断言: 第 1 轮 1 行 → 补 answer 仍 1 行且不覆盖 question → 第 2 轮 2 行 → 反馈只改命中那一轮、另一轮未被误改 → 删除连带清理)
    • node harness/tools/validate-harness.mjs 通过;并实测新检查会报错
  • 已记录证据:本文件 Session 027;completed/ 下两个计划的状态头; harness/tools/validate-harness.mjs 的归档检查

  • 更新过的文件或工件src/network/api/dms/chat-sessions-dms.tssrc/components/business-assistant/useBusinessAssistantChat.tsharness/tools/{_entry-dms.ts,verify-dms-chat-storage.mjs,validate-harness.mjs}harness/docs/exec-plans/(两个计划归档 + 一个加进度表)、根 README.md、本文件

  • 已知风险或未解决问题

    • ⚠️ 浏览器端到端仍未人工点验(DMS 与政策列表两条都还挂着)—— 这是目前唯一的验证缺口,自动化证据都是齐的
    • ⚠️ 归档不及时是我的执行问题,不是规则缺失exec-plans/README.md 里早就写了 「做完移入 completed/」,我没做。现在有了机械检查,但仍需要我每次收尾时真的跑它
    • ⚠️ 改回一问一答一条后,若将来又要「整段对话」的视图,需要重新设计(当时的代码在 git 历史 8f60119 里,未丢失)
  • 下一步最佳动作:浏览器验收 DMS 与政策列表两条;以及从本轮起,收尾必跑 validate-harness.mjs 并当场归档计划(已加进 sops/session-end.md 的清单)

Session 028

  • 日期:2026-09-17
  • 本轮目标:用户在浏览器完成人工点验,回报「点完了,没问题」——把这条验证落进记录
  • 已完成
    • 浏览器人工点验通过(用户确认),此前挂了几轮的「唯一验证缺口」关闭
    • 从库里的真实数据取证(不是只记一句「用户说没问题」):
    • 1887 助手会话:该会话 1 行c_title = 首问「猫和老鼠讲述了什么故事」, c_credit_code = 访客_68e82b71-a3ea-4332-b616-d174c4e9e4d4
    • 1889 助手问答记录:2 轮 = 2 行(同一 c_session_id、不同 c_record_idc_questionc_answer 均已写入)
    • 这组数据同时证实三件事:一会话一行 + 一问一答一条的粒度正确、 访客记录确实落了库访客_ 前缀)、开发环境确实处于访客态(没有自动登录)
    • 更新条目证据(去掉「未做」标注,换成上述实测留痕): dms-chat-storagepolicy-card-list-sidebarremove-dev-auto-login
    • 两个已归档计划的状态头改为「已完成并人工验证
  • 运行过的验证
    • 直接查 DMS 两个栏目,读到用户测试留下的真实行(见上),与代码约定逐条对上
    • node harness/tools/validate-harness.mjs 通过
  • 已记录证据:本文件 Session 028;feature_list.json 三条的 evidence/verification; completed/ 下两个计划的状态头
  • 更新过的文件或工件harness/feature_list.jsonharness/docs/exec-plans/completed/{dms-chat-storage,policy-card-list-sidebar}.md、 根 README.md、本文件
  • 已知风险或未解决问题
    • ⚠️ policy-detail-index-mismatch(同政策多事项点详情内容一样)的浏览器验证仍未单独确认。 用户本次点验的是 DMS 与政策列表两条;那条 bug 的验收场景是「同一政策的两个申报事项 分别点开、内容不同」,属于特定构造场景。保守起见仍保留「浏览器未人工点过」的标注, 等用户确认过再改——不靠推断把状态改好看
    • ⚠️ DMS 临时 token 今晚 22:19 过期(现在 15:41 仍有效)。过期后写入会静默降级 (仅控制台 warn),聊天与本地历史不受影响;需要新 token 时替换 .env.development.local 并重启 dev server
  • 下一步最佳动作:若用户能顺手确认一下「同政策两个申报事项点开内容不同」, 即可关闭最后一条浏览器验证;否则维持现状标注

Session 029

根因(两层,均已实证)

  1. 按钮语义是全局的InputShellisGenerating ? emit('stop') : emit('send') 用的是全局 isGenerating → 切到别的会话后按钮仍是「停止」,用户想发送却触发了停止
  2. 停止打错对象handleStopGeneratesmc.stopGenerate()(mitt 同步 emit close → 监听器把 activeGenerationSessionId 清空),再取目标时 getActiveGenerationSession() || currentSession 必然 fallback 到当前会话 → 停止打在用户正在看的会话上;stopAiMessage 还会 ensurePendingAiMessage 凭空造出一条空的"请求已取消"消息

已完成

  • 新增 chat-generation-task.ts(类型 + 4 个纯决策函数,无 vue/网络依赖,可被 harness 打包): isSessionGenerating / resolveSendAction / resolveStopTargetTaskId / shouldNormalizeSessionHistory
  • composable 重构:生成状态从「一个全局 smc + 一个 activeGenerationSessionId」 改为 tasks = ref(new Map<sessionId, ChatGenerationTask>())
    • 每轮新建 ApiChatCoordinator 实例(生命周期 = 任务,mitt 随实例销毁)
    • 每个任务自己的 chunkBuffer、监听器闭包捕获 sessionId(不再读「当前会话」)
    • 新增 finishTask / stopTask / stopAllTasks;补问数据按会话存 sessionInterruptPayloads
    • 删除 smc / messageChunkBuffer / activeGenerationSessionId / dmsAnswerWrittenFor / getActiveGenerationSession / finishActiveAiMessage / buildAppendHistoryPayload / setupCoordinator
    • isGeneratingcomputed(() => tasks.size > 0);新增 currentSessionGenerating 给按钮用
  • handleStopGenerate 改为 resolveStopTargetTaskIdstopTask —— 旧 bug 自然消失
  • startNewChat 不再打断生成(后台任务继续,与主流产品一致)
  • 新增 toast 通道toastMessage + showToast;PC/Mobile 各挂 <Toast>; info 事件终于有人监听了(此前 409/错误提示用户完全无感)
    • 同会话重复发送 → toast「该会话已有回答在生成中」(替换原来的静默 return)
  • 退出登录先 stopAllTasks()(在 loadHistory 之前,避免旧账户任务写到新身份下)
  • 删除正在生成的会话 → 先停任务并抑制 DMS 补写(不留孤儿行)
  • UI:4 处 :is-generating 绑定改 currentSessionGenerating;InputShell/Composer 零改动

运行过的验证

  • npm run build 通过;npx vue-tsc --noEmit 本次涉及文件无新增类型错误
  • 新增 verify-chat-task-utils.mjs — 24/24:空/ null 边界、发送决策三态、 停止目标解析(「当前在 B 时不会误停 A」)、归一化守卫、多会话并行组合语义
  • 新增 verify-coordinator-multi-instance.mjs — 11/11两个协调器实例的事件完全隔离(停 A 时 B 零事件、B 出错时 A 零事件、 各自计数独立)—— 这是并行化的地基
  • 全量回归:政策卡列表 31/31、卡字段 8/8、详情反查 10/10、政策库 21/21、 判空口径 30/30、DMS 36/36;validate-harness 通过

已记录证据

本文件 Session 029;两个新脚本;exec-plans/active/multi-session-parallel-generation.md

更新过的文件或工件

src/components/business-assistant/chat-generation-task.ts(新增)、 src/components/business-assistant/useBusinessAssistantChat.tssrc/components/BusinessAssistantPC.vuesrc/components/BusinessAssistantMobile.vueharness/tools/{_entry-chat-tasks.ts,verify-chat-task-utils.mjs,verify-coordinator-multi-instance.mjs,_entry-coordinator.ts}harness/docs/exec-plans/active/multi-session-parallel-generation.md、根 README.md、本文件

已知风险或未解决问题

  • ⚠️ 浏览器端到端未人工验证(这是本轮唯一缺口,自动化证据齐备): A 生成中切 B 发送成功、各自停止互不影响、同会话重复发送被 toast 拦截、 退出登录停全部、删除生成中的会话无孤儿、6 个 mock 测试按钮逐一回归(本轮动过它们)
  • ⚠️ 每轮新建协调器实例,稳态最多 16 个存活实例(后端并发上限),无累积
  • ⚠️ 停止后立即重发可能撞后端 409(会话锁保留到执行线程结束)→ 现在有 toast 提示, 属预期行为而非 bug
  • ⚠️ mock 测试流任务没有协调器,其终止依赖 typewriterTestSession,已同步改为 per-session 判断

下一步最佳动作

浏览器双会话实测(上述清单),特别是 mock 测试按钮回归

Session 030

  • 日期:2026-09-17
  • 本轮目标:修用户报告的「mock 测试点击后内容输出完,按钮仍是运行时的按钮」
  • 根因(我上一轮引入的响应式陷阱)
    • 任务表当时写成 ref(new Map())ref 会把 Map 变成响应式代理, tasks.value.get(id) 返回的是代理、不是存进去的那个对象。 实测:ref(new Map()).get('A') === 原始对象falseshallowRef → true。
    • 而我在所有监听器里写的守卫是 tasks.value.get(sessionId) !== task永远成立 → 监听器全部提前返回
    • 后果:mock 流的 finishTask 从不执行 → 任务永驻 → isGenerating 恒真 → 按钮卡在"停止"态(用户看到的);真实对话的内容追加也被同一个守卫挡掉message 监听器同样提前返回)—— 这是比用户报告更严重的一面,同轮一并修掉
  • 修法
    • tasksshallowRef(new Map()) + 整体替换;新增纯函数 withTaskAdded / withTaskRemoved(复制出新 Map,不改原 Map),4 处写入点全部改用
    • chat-generation-task.ts 末尾写清这个坑(含实测结论),防止以后被"简化"回去
  • 新增回归断言verify-chat-task-utils.mjs 第 6、7 节,脚本从 24 → 33 项):
    • 不可变替换语义(新 Map 有、原 Map 没有;取出来就是存进去的那个对象)
    • 把陷阱本身钉死:断言 ref(new Map()).get(k) !== raw(若哪天 Vue 行为变了这条会失败, 提示需重新评估写法)、shallowRef 下同一性保持、整体替换能触发 computed、 原地 mutate 不触发响应式
    • 为此 _entry-chat-tasks.ts 从 vue 重导出 ref/shallowRef/computed仅测试用
  • 运行过的验证
    • npm run build 通过
    • verify-chat-task-utils.mjs 33/33;其余全绿(11/31/8/10/21/30);validate-harness 通过
  • 已记录证据:本文件 Session 030;verify-chat-task-utils.mjs 第 6、7 节
  • 更新过的文件或工件src/components/business-assistant/chat-generation-task.tssrc/components/business-assistant/useBusinessAssistantChat.tsharness/tools/{_entry-chat-tasks.ts,verify-chat-task-utils.mjs}harness/feature_list.json、本文件
  • 已知风险或未解决问题
    • ⚠️ 浏览器仍未人工验证:请重新点一遍 mock 测试按钮(内容输出完按钮应恢复成"发送"), 并请务必试一次真实对话(这是被我引入的同一个 bug 波及、本轮修好的部分)
    • ⚠️ 同轮还修了一处我自己的测试错误:断言 computed 惰性时忘了先求值填充缓存 (computed 不先读就没有缓存可失效),已修正——是测试写错,不是代码问题
  • 下一步最佳动作:浏览器复验 mock 按钮 + 真实对话

Session 031

  • 日期:2026-09-17
  • 本轮目标:用户回报「测试无误」——把并行生成的浏览器验证落进记录并归档计划
  • 已完成
    • multi-session-parallel-generationpassing(用户确认:mock 测试按钮跑完恢复、 真实对话内容正常流式输出);verification 里那条「❌ 未做浏览器验证」已替换为实测确认
    • 计划 multi-session-parallel-generation.md → 从 active/ 归档到 completed/, 状态头改为「已完成并人工验证」,并注明过程中踩过的响应式陷阱(Session 030)
  • 运行过的验证validate-harness 通过(19 项功能,in_progress 0 项
  • 已记录证据:本文件 Session 031;completed/multi-session-parallel-generation.mdfeature_list.json 的 evidence
  • 更新过的文件或工件harness/feature_list.jsonharness/docs/exec-plans/(归档)、 根 README.md、本文件

⚠️ 当前没有 in_progress 的功能 —— 下一个重点(按优先级)

  1. dms-api-replacement 里剩下的企业信息(1888) —— 计划仍在 active/, 1887/1889 相关的 3 读 3 写已实现,企业信息那条没做(前端只用到 name/credit_code 两个字段,替换成本很低)。需要用户拍板要不要做
  2. policy-detail-index-mismatch 的浏览器验证仍未单独确认 —— 那个 bug 的验收场景是 「同一政策的两个申报事项分别点开、内容不同」,属特定构造场景(Session 025 修, 自动化证据齐备;用户尚未针对该场景单独确认过)。
  3. file-upload(not_started)/ chat-markdown-format(blocked) —— 都卡在等待决策 (后端契约 / 是否接受在适配层做格式归一化),见各自条目。
  4. 已知遗留(非阻塞):DMS 生产接入需 nginx 转发 + 长效凭据;DMS 临时 token 已过期, 需要时替换 .env.development.local 并重启 dev server。

Session 032

  • 日期:2026-09-17
  • 本轮目标:用户要求「修改环境变量」——新增后端存放会话 JSON 的路径 /data/dms/dms_upload/yszs,并说明后端地址是 http://192.168.2.23:8000
  • 背景(问清后才动手):仓库与 DMS 项目里都没有 dms_upload 的任何引用, 是全新信息。问了两个问题才定清楚:①用途 = 后端存放会话信息 JSON 的目录; ②形态 = 前缀是后端地址(http://192.168.2.23:8000
  • 已完成
    • .env.development 新增两个变量:
    • VITE_CHAT_TARGET="http://192.168.2.23:8000" —— 聊天后端真实地址。 dev 下 VITE_CHAT_API 只是代理前缀 /chat-api,真实地址此前硬编码在 vite.config.ts;现改为读 env
    • VITE_CHAT_SESSION_JSON_DIR="/data/dms/dms_upload/yszs"
    • .env.production / .env.qingpu / .env.test 各加 VITE_CHAT_SESSION_JSON_DIR (值相同,后端同一个)
    • vite.config.ts/chat-api/ 代理 target 改读 env.VITE_CHAT_TARGET(保留同值兜底)
    • docs/architecture.md 环境变量表补上两个新变量,并写明「地址往 env 放、别写死在代码里」
  • 运行过的验证
    • npm run build 通过
    • dev 代理实测未被改坏npm run dev(用户已在跑的 8083,vite 检测到 config 变更自动重启) → 首页 200;经 /chat-api/api/chat 打真实后端 → http=200(不是 502)
    • 四份 env 逐一核对,变量与注释都正确落位
  • 已记录证据:本文件 Session 032;.env.*vite.config.ts 的改动; docs/architecture.md 的环境变量表
  • 更新过的文件或工件.env.development.env.production.env.qingpu.env.testvite.config.tsharness/docs/architecture.md、根 README.md、本文件
  • 已知风险或未解决问题
    • ⚠️ 有一处我按字面理解写、需要用户确认:把 /data/dms/dms_upload/yszs 当作 URL 路径直接接在后端地址之后(http://192.168.2.23:8000/data/dms/dms_upload/yszs)。 如果 /data/dms/ 其实是服务器上的文件系统前缀、对外访问路径是别的 (例如 http://192.168.2.23:8000/dms_upload/yszs),告诉我改一行即可
    • ⚠️ VITE_CHAT_SESSION_JSON_DIR 目前没有消费方(前端还没有读会话 JSON 的功能), 只作约定记录——已在变量注释与架构文档里写明,避免以后有人以为它在起作用
  • 下一步最佳动作:确认上面那条路径理解是否正确;若将来前端要读会话 JSON, 从 VITE_CHAT_SESSION_JSON_DIR 取,不要另写死地址

Session 033

  • 日期:2026-09-17
  • 本轮目标:用户要我看清 /data/dms/dms_upload/yszs 到底哪种形态有数据 (承接 Session 032 我按字面把 URL 假设写进 env 的那处不确定)
  • 实测结论:没有任何 URL 形态有数据;能取到数据的接口里,该目录是空的

探过的东西(都是真跑的)

探测 结果
192.168.2.23:8000/data/dms/dms_upload/yszs//dms_upload/yszs//dms/dms_upload/yszs/ 等 7 种路径 全部 404,响应体固定 9 字节 Not Found
同主机的 /openapi.json/docs/redoc 也全是 404 —— 该服务不提供任何静态文件,是纯 API 服务
121.43.55.7:10081/data/.../dms_upload/... 404(649 字节,Tomcat 默认错误页)
同主机 /dms/dms_upload/... 404(150 字节,Spring 的 JSON —— 说明这些请求确实进了 DMS 应用,但没有对应路由)
真实会话 id(你测试留下的 a2fdfbc2-…)当文件名去探 仍全 404
聊天后端原始 SSE 里找路径/文件线索 无任何线索(不含路径字段)
两个 IP 是否同一台机器 不是192.168.2.23 只开 8000,121.43.55.7 只开 10081

✅ 真正能取到数据的方式:DMS 的 POST /dms/file/getFiles

  • 参数只有 path(form body),且是相对 DMS 进程工作目录(Tomcat 的 bin)
  • ⚠️ 传绝对路径不报错但永远取不到(会返回「当前文件不是文件夹,无子文件」)—— 因为它是相对拼接的。这是这轮最容易踩的一点
  • 换算实测:. = Tomcat bin;.. = Tomcat 根(含 webapps_dmslogsuploads); ../.. = /usr/local../../../.. = /../../../../data/dms/dms_upload 才是目标
  • /data/dms/dms_upload/yszs 存在,但返回里没有 content 字段 = 目录为空 (对比:同级 photo 7027 项、file 6324 项、video 3 项、other 1 项)
  • getFiles 只能列目录、读不了文件内容(把文件路径传进去 → 「当前文件不是文件夹,无子文件」)

结论

/data/dms/dms_upload/yszsDMS 服务器上新建的目录,目前没有数据; 它不是 URL(Session 032 我那句「完整地址 = {后端地址}{路径}」是错的), 前端也拼不出来。要读它只能走 DMS 接口,且目前没有读文件内容的接口

已完成

  • 修正 .env.* 的注释(4 份):如实写明「不是 URL、实测全 404、读取要走 getFiles、 相对路径怎么写、目录当前为空」,并删掉那句错误的「完整地址 = …」
  • docs/reference/DMS_API.md 新增 §8「浏览服务器文件目录」:参数、相对路径换算表、 返回形态(空目录没有 content 字段 / 传文件路径会怎样 / 参数名写错会 500)、 实测到的目录布局
  • docs/architecture.md 里该变量的说明同步为「服务器目录,不是 URL」

  • 运行过的验证:上述探测全部有原始响应为证;npm run build 通过; validate-harness 通过

  • 已记录证据:本文件 Session 033;docs/reference/DMS_API.md §8;四份 env 的注释

  • 更新过的文件或工件.env.development.env.production.env.qingpu.env.testharness/docs/reference/DMS_API.mdharness/docs/architecture.md、根 README.md、本文件

  • 已知风险或未解决问题

    • ⚠️ 该目录为空:如果预期它现在就该有会话 JSON,那是后端还没往里写 (或写到了别处 —— dms_upload/ 根下确实有 7 个 uuid 命名的 .json, 但我没有读文件内容的接口,无法确认它们是不是会话 JSON)
    • ⚠️ 用户说的后端是 192.168.2.23:8000,而该路径在 121.43.55.7 上(两台机器)—— 若后端要写这个目录,是跨机器写(可能经 DMS 的文件接口),这点需用户/后端确认
    • ⚠️ 相对路径依赖 Tomcat 工作目录的位置,Tomcat 挪目录就会失效
  • 下一步最佳动作:请用户确认两件事 ——①后端是否已经开始往这个目录写会话 JSON; ②如果已有文件而我看不到,请给一个文件名/完整示例,我按它验证读取路径

Session 034

  • 日期:2026-09-17
  • 本轮目标:用户指示「不改了,修改都回退」——回退与 /data/dms/dms_upload/yszs 有关的改动 (问清过范围:只针对这个路径,不是今天全部改动)
  • 已完成(回退清单)
    • 四份 .env.*:删掉 VITE_CHAT_SESSION_JSON_DIR 变量及其 6 行注释块
    • docs/architecture.md:删掉该变量的表格行
    • docs/reference/DMS_API.md:删掉 §8「浏览服务器文件目录」(它是为查这个路径写的)
    • README.md:删掉该变量的那条记录
  • 有意保留VITE_CHAT_TARGET(聊天后端真实地址)——它与 yszs 路径无关, 是「把硬编码在 vite.config.ts 里的后端地址收进 env」的独立改进。若也要一并回退,说一声即可
  • 运行过的验证
    • 残留检查:.env.*architecture.mdDMS_API.md 里已无 dms_upload / VITE_CHAT_SESSION_JSON_DIR 的任何引用
    • npm run build 通过;node harness/tools/validate-harness.mjs 通过
  • 已记录证据:本文件 Session 034;回退后的 git 提交
  • 更新过的文件或工件.env.development.env.production.env.qingpu.env.testharness/docs/architecture.mdharness/docs/reference/DMS_API.md、根 README.md、本文件
  • 已知风险或未解决问题
    • ⚠️ 回退掉的东西仍在 git 历史里(提交 0caf1ae7ce6ed3)—— 包括那次探测得到的「DMS 有 POST /file/getFilespath 是相对 Tomcat 工作目录的路径」 这条通用知识(不止对 yszs 有用)。如果哪天要浏览 DMS 服务器上的文件, 可以 git show 7ce6ed3 取回,或让我把不含 yszs 的部分重新写进参考文档
    • ⚠️ 该路径本身未做任何改动,前几轮查到的结论(不是 URL、目录为空)仍然成立, 只是不再落在仓库文件里
  • 下一步最佳动作:无(本轮为回退);功能清单里仍无 in_progress,可做的事见 Session 031 列的优先级

    Session 035

  • 日期:2026-09-17

  • 本轮目标:用户指出「进度日志顺序反了」——修正 progress.md 的会话记录顺序

  • 已完成

    • 原顺序是倒序(Session 034 在最上、000 在最下);已改为正序(旧 → 新), 与根 README.md 的日期节一致(README 的正序是 Session 016 时用户明确要求的)
    • 在前言加了顺序约定:「最新的记录在最下面,加新记录请追加到文件末尾,不要插到开头」 —— 写明是为了不让它再漂回去
    • 顺带调整:原先挂在文件最末尾的「### 还没写进 feature_list 的待办」 (常驻待办,不属于任何一轮)移到「## 会话记录」之前—— 正序下留在末尾会看起来像最新那轮会话的内容
  • 运行过的验证

    • 逐块内容校验(脚本比对重排前后):会话块 35 → 35,无丢失、无多出; 逐块 md5 比对只有 Session 000 变化,原因是它末尾原本挂着待办小节、被摘出(预期内)
    • 新顺序核对:Session 000(基线) → … → Session 034,升序正确
  • 已记录证据:本文件 Session 035;上面的逐块校验输出

  • 更新过的文件或工件harness/progress.md

  • 已知风险或未解决问题

    • ⚠️ 顺序是约定,没有机械约束兜底(validate-harness.mjs 目前只查会话编号重复, 不查顺序)。若将来有人又插到开头,需要靠这条约定挡;要更强的话可以加一条校验
  • 下一步最佳动作:无(本轮为文档整理)

  • 补充:同轮给 validate-harness.mjs 加了正序校验(会话编号必须升序),并实测能抓到乱序——故意把最新一条插到开头,校验器立刻报出「Session 035 之后跟了 Session 000」。测试后已恢复正序

Session 036

  • 日期:2026-09-17
  • 本轮目标:用户问「当前项目还有哪些功能使用的是老接口」
  • 已完成
    • 全仓库 grep 所有网络调用点,再逐个反查调用方判断死活 (判据:出现在 grep 里 ≠ 还在用,必须查有没有调用方)
    • 结论落到仓库:新建 docs/reference/current-api-call-sites.md,分三类列出 (已迁新接口 / 仍在用老接口 / 已无调用方的死代码),并在 reference/README.md 索引里登记
  • 主要发现
    • 仍在用老接口(活):企业登录 /enterprise/login、企业信息 /fta_ent_policy/enterprise_info、 统计埋点(page_view / feedback)、ASR、OSS 上传(入口被注释,用户触发不到)
    • 几乎不可达{VITE_MODEL_SERVER}/v1/policies —— fetchPolicyList 前面两档 (惠企政策库 / 本轮卡片兜底)几乎总能命中
    • 只在 test 环境openid_to_token(四份 env 里只有 .env.testVITE_USE_KNOWLEDGE_API=false
    • 死代码src/network/api/card/index.js 的三个函数 (getMediaList / giveFeedback / appendHistory都没有调用方—— 反馈切 DMS 后最后一个调用点消失;目前只剩 useBusinessAssistantChat.ts:2 一个未使用的 import
    • PolicyListUpdatedMatch.vue 仍挂在 BusinessRecord 上,但新协议不再产出 policy-list-updated 块,只有旧历史内容可能触发
  • 运行过的验证
    • 每个「仍在用」的条目都反查到了具体调用方与触发条件(写进了清单的「什么时候走」列)
    • node harness/tools/validate-harness.mjs 通过
  • 已记录证据:本文件 Session 036;docs/reference/current-api-call-sites.md(含可重跑的复核命令)
  • 更新过的文件或工件harness/docs/reference/current-api-call-sites.md(新增)、 harness/docs/reference/README.md、本文件
  • 已知风险或未解决问题
    • ⚠️ 死代码(card/index.js 等)没有删除,只标明了「没人调」——要清理需另开一轮, 并确认没有旧数据/历史内容依赖
    • ⚠️ 本清单是快照,接口会继续迁移;清单里写了可重跑的复核命令,别只信文档
  • 下一步最佳动作:若要继续收敛老接口,优先级建议: ①企业信息(1888)切 DMS(前端只用到 name/credit_code 两个字段,成本低) ②统计埋点是否收口到 DMS(需产品定) ③清理 card/index.js 死代码与未使用的 import

Session 037

  • 日期:2026-09-17
  • 本轮目标:用户要求「功能具体一点」——上一轮的接口清单是按接口路径列的, 改成按用户能感知的功能
  • 已完成:重写 docs/reference/current-api-call-sites.md
    • 每条改成「用户看到什么 / 怎么触发 → 用哪个接口 → 现状」,并补上影响面
    • 补查出几条上次没写清的:
    • 企业身份识别不止"显示个名字":它同时决定 ①账号区显示 ②是否登录判定 ③历史存哪个 localStorage key(登录/访客分家)④会话列表拉取 ⑤DMS 记录的 c_credit_code 归属 —— 共 5 处依赖
    • 点赞是双写:既写 DMS 的 c_feedback_*(业务),又发统计接口(运营)
    • 统计基址 VITE_ASSISTANT_STATISTICS_BASE_URL 只有 .env.qingpu 配了, 其余环境回退 VITE_API
    • 上传的 OSS 地址是硬编码useBusinessAssistantUpload.ts 里的
    • 死代码里补上了 appendHistory 的来历(早就注释,历史改由接口 enable_history 控制)
  • 运行过的验证:每条「触发条件」都反查到了调用方(如第 5 条要看 fetchPolicyList 前两档 return 才能判断几乎走不到);validate-harness 通过
  • 已记录证据:本文件 Session 037;docs/reference/current-api-call-sites.md
  • 更新过的文件或工件harness/docs/reference/current-api-call-sites.md、本文件
  • 已知风险或未解决问题:同上轮(死代码未清理;清单是快照,文末有可重跑的复核命令)
  • 下一步最佳动作:若要继续收敛,优先级仍是①企业信息切 DMS(前端只用 name/credit_code, 但注意它有 5 处依赖)②统计埋点是否收口 ③清理死代码

Session 038

  • 日期:2026-09-17
  • 本轮目标:用户两条要求 ——①改会话标题后 DMS 的 title 同步 ②存问答记录时把问题文本写进 title
  • 先验契约再动手(新脚本 harness/tools/probe-dms-title.mjs,用完即清理),查出三件原本不知道的事
    1. 1887 的栏目模型今天 17:50 被扩过:字段 7 → 23 个,新增 4 个 must=truesummary 摘要 / qpyszx 是否青浦营商咨询 / qyzt 是否识别到企业主体 / rzqpyx 是否落户青浦意向)。 不带它们写入会被 214 数据错误 拒绝(实测:不带 214、带上 200)
    2. DMS 的系统字段 title 只在创建那一刻初始化:改 c_titletitle 不跟随 (实测:改完 c_title=标题Btitle 仍是 标题A)——这正是用户说"改名后 title 不同步"的真因
    3. title 可以直接在 content 里写(写了就是那个值),且 DMS 会静默忽略未知字段 (1889 没有 summary 字段,多发不会报错)
  • 改动src/network/api/dms/chat-sessions-dms.ts):
    • 新增 buildAnalyticsFields(summary):补上那 4 个 must 字段。 只有新增时才带(实测更新不校验 must);取值:qpyszx 恒 true、 qyzt = 有企业信用代码、summary = 会话用标题 / 记录用问题文本、 rzqpyx 前端无从判断 → 先 false(已在代码注释里标明是猜的,待确认/后端回填)
    • upsertDmsSession:每次 upsert 都显式带 title = 会话标题 → 改名后 DMS 的 title 跟着变
    • upsertDmsRecord:新增记录时 title = 问题文本、summary = 问题文本 → 不再是字符串 "null",DMS 列表里能看出是哪一轮
  • 运行过的验证
    • verify-dms-chat-storage.mjs 扩到 42 项(新增第 7 节 title 断言),42/42 通过: 新建带 title、改名后 c_title 与 title 都变、记录 title = 问题文本、 以及「1889 无 summary 字段、多发被忽略」这条差异
    • 过程中有一条断言是我写错的(以为 1889 也有 summary),已改成如实断言
    • npm run build 通过;其余脚本全绿(33/11/31);validate-harness 通过
  • 已记录证据:本文件 Session 038;verify-dms-chat-storage.mjs 第 7 节; harness/tools/probe-dms-title.mjs(探测脚本,保留可重跑)
  • 更新过的文件或工件src/network/api/dms/chat-sessions-dms.tsharness/tools/{probe-dms-title.mjs,verify-dms-chat-storage.mjs}、根 README.md、本文件
  • 已知风险或未解决问题
    • ⚠️ rzqpyx(落户青浦意向)的取值是猜的——前端拿不到判断依据。若该字段应由后端 分析会话后回填,请告知,我把前端这一项去掉或改值(改一个常量即可)
    • ⚠️ 同理 qpyszx(恒 true)与 qyzt(有企业码为 true)也请确认是否符合业务口径
    • ⚠️ 这条必须说清楚:在我做这轮改动之前,前端的会话写入其实已经被 214 拒绝 (模型 17:50 扩容后缺 must 字段)。用户认为"前端写入没问题",但实测是坏的; 本轮补上 must 字段后恢复。若用户看到的"正常"另有原因,请告知
  • 下一步最佳动作:确认上面三个分析字段的业务口径

Session 039

  • 日期:2026-09-18
  • 本轮目标:用户指示 —— qpyszx / qyzt / rzqpyx 三个字段不要传值, 后续要传哪些他会告知
  • 已完成
    • chat-sessions-dms.tsbuildAnalyticsFields 去掉那三个字段,只保留 summary (会话用标题、记录用问题文本)
    • 代码注释写明:这三个属业务口径、前端不再替它们猜值(此前一度传过 true/false 已去掉), 并记下「它们在 DMS 模型里是 must=true,若再出 214 先看这里」
  • ⚠️ 本轮未能验证(重要)
    • DMS 的临时 token 已于 2026-09-17 22:19 过期,验证脚本打真实 DMS 时全部报 212 无效token(32 项失败全部由此引起,与本次改动无关)
    • 因此「去掉三个字段后写入是否仍成功」未知 —— 但那三个字段在模型里是 must=true, 按 Step-0/上一轮的实测规律,缺 must 字段很可能再次被 214 数据错误 拒绝
    • ⚠️ 也就是说:在拿到新 token 之前,前端往 DMS 的写入可能又是不通的 (或需要 DMS 侧放宽 must 约束)
  • 运行过的验证
    • node harness/tools/verify-dms-chat-storage.mjs因 token 过期无法验证(已如实记录, 未把失败算到改动头上)
    • 把验证脚本里那条「qpyszx 应为 true」的断言改成只断言 summary(否则会误报)
  • 已记录证据:本文件 Session 039;verify-dms-chat-storage.mjs 第 7 节的注释
  • 更新过的文件或工件src/network/api/dms/chat-sessions-dms.tsharness/tools/verify-dms-chat-storage.mjs、根 README.md、本文件
  • 已知风险或未解决问题
    • ⚠️ 需要新的 DMS token 才能继续验证与联调(放 .env.development.localDMS_TOKEN 并重启 dev server)
    • ⚠️ 去掉三个 must 字段后写入是否被拒,未知;若被拒,需要用户告知是指望 DMS 侧放宽 must 约束,还是这三个字段仍要传(那就得定取值口径)
  • 下一步最佳动作:拿到新 token 后重跑 verify-dms-chat-storage.mjs,确认写入是否仍通

Session 040

  • 日期:2026-09-18
  • 本轮目标:用户澄清需求 —— 「我的需求是传 title,不要传类似于 summary 的多余的字段」 (上一轮我把 summary 留下了,是理解偏了)
  • 已完成
    • chat-sessions-dms.ts 删掉 buildAnalyticsFields 及其两处调用; 写入只发业务字段 + title
    • 会话(1887):c_credit_code c_session_id c_title title c_source c_created_at c_updated_at
    • 问答记录(1889):c_credit_code c_session_id c_record_id c_question title c_source c_created_at
    • 代码注释改成明确的规矩:写入只带业务字段 + title,不发运营分析字段; 要传哪些由用户指定,不要在代码里替它们猜值或顺手填;若再遇 214 先确认是不是缺 must 字段, 但那属于"要不要传"的决策 —— 先问用户,别自行补
  • 运行过的验证不依赖 token):
    • 新增 harness/tools/verify-dms-payload-fields.mjs —— 打桩 fetch 抓真实请求体, 解析 form 里的 content 键集合,断言「不多不少」:9/9 通过
    • 会话新增字段集合与预期完全一致(含 title,不含 summary/qpyszx/qyzt/rzqpyx)
    • 会话更新也带 title(原先不带 → DMS 里标题不跟随改名)
    • 记录新增字段集合一致,且 title 的值就是问题文本
    • 这个脚本绕开了 token 问题:把 globalThis.fetch 打桩即可断言发出的内容
    • npm run build 通过;validate-harness 通过
  • 已记录证据:本文件 Session 040;harness/tools/verify-dms-payload-fields.mjs
  • 更新过的文件或工件src/network/api/dms/chat-sessions-dms.tsharness/tools/{verify-dms-payload-fields.mjs(新增),verify-dms-chat-storage.mjs}、 根 README.mdharness/feature_list.json、本文件
  • 已知风险或未解决问题
    • ⚠️ 打真实 DMS 的验证仍做不了(token 2026-09-17 22:19 过期)。 已验的是「发出去的字段对不对」;没验的是「DMS 收不收」—— 那三个字段在模型里是 must=true,去掉后是否被 214 拒绝,要等新 token
    • ⚠️ 若写入被拒:是等 DMS 侧放宽 must,还是这些字段仍要传——由用户定,前端不自行补
  • 下一步最佳动作:拿到新 token 后跑 verify-dms-chat-storage.mjs 确认真实写入是否通过

Session 041

  • 日期:2026-09-18
  • 本轮目标:用户给了新的 DMS token,重跑真实写入验证
  • Token:有效期至 2026-09-18 21:33(已写入 .env.development.local,按约定不入库; vite 代理已自动重启拾取,实测经代理查询返回 200)
  • ⚠️ 真实写入验证结果(决定性): | 栏目 | 结果 | |---|---| | 1889 助手问答记录 | ✅ 写入正常title = 问题文本 已确认生效 | | 1887 助手会话 | ❌ 214 数据错误 拒绝 |
  • 最小必需字段实测(addContent 二分):1887 必须四个字段全带,少任何一个都 214:

| 载荷 | 结果 | |---|---| | 全不带(= 当前前端) | 214 | | +summary | 214 | | +summary+qpyszx | 214 | | +summary+qpyszx+qyzt | 214 | | 四个都带 | 200 成功 | | 只带 qpyszx(不带 summary) | 214 |

  • 结论:用户的两条要求与 DMS 现状冲突 —— 用户要求「只传 title,不要 summary 这些多余字段」, 而 DMS 的 1887 模型把这四个字段设成了 must=true二者不能同时满足,必须有一方让步。
  • 当前应用的实际状态1887 会话写入是坏的(新建会话、改名、删除同步都写不进去), 1889 记录行照常写入 → 表现为「DMS 里有问答记录、但没有对应的会话行」 (这也是用户此前反馈「改名后 title 不同步」的另一层原因)
  • 已清理:本轮二分探测产生的 probe 行已删除(残留 0)
  • 运行过的验证verify-dms-chat-storage.mjs(打真实 DMS)—— 1889 相关全通过、1887 相关因 214 失败(如实记录,未粉饰)
  • 已记录证据:本文件 Session 041;上面的二分表(原始响应为证)
  • 更新过的文件或工件.env.development.local(不入库)、本文件
  • 已知风险或未解决问题需用户决策):
    • 三条路选一: ①DMS 侧把这四个字段改成非必填must=false)→ 前端就能只传 title(最贴合用户要求) ②前端仍传这四个 → 取值口径要定;也可以先传空值/默认值summary:"" 等) 让写入先通,后续再定口径 —— 但这是"为了通过校验而填",需用户认可 ③维持现状 → 1887 会话写入持续失败
  • 下一步最佳动作:等用户在①②③中选一个

Session 042

  • 日期:2026-09-18
  • 本轮目标:用户拍板「先传空值/默认值」—— 让写入先通,业务口径后续再定
  • 先验证空值能不能过 must 校验(直连实测):summary:"" + 三个 false200 成功 ✓ (must 只校验"字段在不在",不校验非空)
  • 改动chat-sessions-dms.ts):
    • 新增 buildRequiredSessionFields(),返回 {summary:"", qpyszx:false, qyzt:false, rzqpyx:false}
    • 只在新增(addContent)时带;⚠️ 更新时一律不带 —— 否则会把后端将来回填的值冲成空
    • 注释写清:这四个是 must(不带就 214),但故意传空值不要顺手填业务判断(如按登录态推 qyzt、拿首问当 summary)—— 那会把「没数据」变成「有数据但可能是错的」,比空值更糟
    • 1889(问答记录)不带这些字段:实测它没这个约束,保持干净
  • 运行过的验证(全部打真实 DMS,token 有效)
    • verify-dms-chat-storage.mjs 43/43 通过 —— 1887 写入已恢复 关键项:新建会话带 title ✓、改名后 c_title 与系统 title 都跟着变 ✓、 四个必填字段确实写进去了且为空/默认 ✓、记录 title = 问题文本 ✓
    • verify-dms-payload-fields.mjs 10/10 通过(打桩抓请求体,不需要 token): 会话新增字段集合「不多不少」、更新不带那四个字段、记录不带
    • npm run build 通过
  • 过程中修正两条我自己写错的断言
    1. verify-dms-chat-storage.mjs 里「摘要应为会话标题」——已过期,改成 「写入且为空(不猜业务值)」+「三个布尔为默认 false」
    2. verify-dms-payload-fields.mjs 头部说明还写着旧的「不要传 summary」,已更正
  • 已记录证据:本文件 Session 042;两个脚本的通过输出(43/43、10/10)
  • 更新过的文件或工件src/network/api/dms/chat-sessions-dms.tsharness/tools/{verify-dms-chat-storage.mjs,verify-dms-payload-fields.mjs}、 根 README.mdharness/feature_list.json、本文件
  • 已知风险或未解决问题
    • ⚠️ 这四个字段目前是空值:运营侧看到的 qpyszx/qyzt/rzqpyx 都是 false、summary 为空。 若业务口径定了(或该由后端回填),需要再改这里
    • ⚠️ 更新不带它们是刻意的:保护后端/他人回填的值不被冲掉。若将来发现更新后这些值被清空, 先看这条
    • ⚠️ DMS token 有效期至今晚 21:33;过期后打真实 DMS 的脚本不可复跑 (但 payload 脚本不需要 token,随时可跑)
  • 下一步最佳动作:等业务方给这几个字段的取值口径(或确认由后端回填)

Session 043

  • 日期:2026-09-18
  • 本轮目标:用户指示「只传具体的会话记录,另一个接口不传了」 → 只写 1889 问答记录,1887 会话不再写
  • 已完成(按项目约定注释而非删除,各调用点都标了 [已停用] 与原因):
    • sendMessage:去掉 upsertDmsSession(会话行不再新建/更新)
    • submitQuestionAnswers:同上
    • 改名(renameSessionAt):不再同步到服务端,只改本地
    • 首轮完成时的标题同步:整块注释
    • syncSessionTitleToServer 函数整体注释;upsertDmsSession / updateSessionInfo 两个 import 一并去掉(注释说明去处)
    • 保留:1889 问答记录写入(含 title=问题文本)、反馈写入、删除会话仍清理 1889 记录
  • 运行过的验证
    • verify-dms-payload-fields.mjs 扩到 12 项(新增第 4 节源码级断言): 断言 composable 里不再有未注释的 upsertDmsSession / syncSessionTitleToServer 调用, 且停用点以注释形式保留 —— 12/12 通过 (这条是为防「以后被顺手改回去」——毕竟它是靠注释实现的)
    • verify-dms-chat-storage.mjs(打真实 DMS):43/43,跑了 6 次
    • npm run build 通过
  • ⚠️ 发现该脚本偶发不稳定:6 次里有 1 次 40/43(未捕获到具体失败项,随后 5 次连过)。 未做进一步追查——若再现,先看 FAIL 行再判断是 DMS 抖动还是脚本假设有问题。 记录在此,免得下次误以为是本次改动引入的
  • 已记录证据:本文件 Session 043;verify-dms-payload-fields.mjs 第 4 节
  • 更新过的文件或工件src/components/business-assistant/useBusinessAssistantChat.tsharness/tools/verify-dms-payload-fields.mjs、根 README.mdharness/feature_list.json、本文件
  • 已知风险或未解决问题
    • ⚠️ 1887 栏目从此不再有新数据:DMS 的「助手会话」列表不会再增长; 前端会话列表退化为纯本地mergeRemoteSessions 拿到空数组是无害的,行为与接 DMS 前一致)
    • ⚠️ 删除会话仍会删 1887 的旧行(若历史上有):这是清理而非新增,按「留着无害」保留; 若你希望连删除也别碰 1887,说一声
    • ⚠️ 那四个必填字段的处理(空值)随 1887 停用而不再被触发——相关代码与断言保留, 将来若恢复 1887 写入可直接复用
  • 下一步最佳动作:浏览器确认「问答记录仍正常写入、会话列表走本地」;业务口径定了再议 1887

Session 044

  • 日期:2026-09-18
  • 本轮目标:用户问「当前有哪些接口在使用」——重新清点并更新参考文档
  • 已完成
    • 全仓库重扫所有网络调用点(排除注释行),逐个确认调用方是否还活着
    • 更新 docs/reference/current-api-call-sites.md
    • 第一节改为表格,并写明 1887 会话写入已停用、会话列表退化为纯本地
    • 复查死代码:card/index.js 仍无调用方,唯一残留引用是 useBusinessAssistantChat.ts:2 那个未使用的 import(仍在)
  • 当前在用的接口(详见文档):
    • 新接口POST /api/chat(对话)、DMS 1889(问答记录写入+读取、反馈、删除清理)
    • 老接口:企业登录 /enterprise/login、企业信息 /fta_ent_policy/enterprise_info、 统计埋点(page_view / feedback,与 DMS 双写)、ASR、OSS 上传(入口被注释)
    • 几乎不可达{VITE_MODEL_SERVER}/v1/policies
    • 仅 test 构建openid_to_token
  • 运行过的验证validate-harness 通过;每条与代码核对过
  • 已记录证据:本文件 Session 044;docs/reference/current-api-call-sites.md
  • 更新过的文件或工件harness/docs/reference/current-api-call-sites.md、本文件
  • 已知风险或未解决问题
    • ⚠️ useBusinessAssistantChat.ts:2 那个未使用的 import cardAPI 还在(是死模块 card/index.js 的唯一引用)——要清理另说,属独立的小清理
  • 下一步最佳动作:无(本轮为清点)

Session 045

  • 日期:2026-09-18
  • 本轮目标:用户要求 ①删掉统计埋点的老接口 ②清死代码 ③结束后再列接口
  • 先问清了冲突:用户此前定过「原代码注释掉,不要直接删除」(Session 017), 与「清死代码」按字面=删除相冲突 → 用户确认直接删除
  • 已完成
    1. 统计埋点(老接口)整条线删除
      • 调用点:BusinessAssistant.vue(页面浏览)、BusinessRecord.vue(反馈统计)
      • 整个模块 src/network/api/assistant-statistics.ts 删除
      • 已无人用的 env VITE_ASSISTANT_STATISTICS_BASE_URL.env.qingpu 删除
      • ⚠️ 只留下访客 id 的读写getOrCreateAssistantVisitorId)——DMS 的 访客_<id> 归属在用它,已搬进 src/network/api/dms/chat-sessions-dms.tslocalStorage 的 key 字符串保持不变assistant_statistics_visitor_id), 改了会让已存在的访客换新 id、同一浏览器在 DMS 里变成两个访客
      • 副作用:点赞从「双写」变成只写 DMS
    2. 死代码清理(用户确认直接删):
      • src/network/api/card/index.js 整文件删除(三个函数都无调用方), 连同 useBusinessAssistantChat.ts 里那个未使用的 import cardAPI
      • assistant-statistics.ts(同上)
      • ⚠️ PolicyListUpdatedMatch.vue 有意保留:它虽对新协议无效, 但老会话的历史内容里可能还有 <policy-list-updated>,删了那些卡片列表渲染不出来
  • 运行过的验证
    • npm run build 通过;vue-tsc --noEmit 本次涉及文件无报错
    • verify-dms-payload-fields.mjs 12/12verify-dms-chat-storage.mjs(真实 DMS)43/43
    • 残留检查:assistant-statistics / network/api/card 在 src 下零引用;validate-harness 通过
  • 已记录证据:本文件 Session 045;docs/reference/current-api-call-sites.md
  • 更新过的文件或工件:删 src/network/api/{card/index.js,assistant-statistics.ts}; 改 src/network/api/dms/chat-sessions-dms.tssrc/components/BusinessAssistant.vuesrc/components/Chat/BusinessRecord.vuesrc/components/business-assistant/useBusinessAssistantChat.ts.env.qingpudocs/reference/current-api-call-sites.md、根 README.md、本文件
  • 已知风险或未解决问题
    • ⚠️ 统计埋点数据从此不再上报:若运营侧依赖 page_view / feedback 看板,需要告知他们 (这是用户明确要求删除的)
    • ⚠️ 删掉的代码在 git 历史里可查(git log --diff-filter=D --name-only
  • 下一步最佳动作:把当前在用的接口清单给用户(本轮第三项要求)

Session 046

  • 日期:2026-09-18
  • 本轮目标:用户问「现在的语音识别功能依赖什么接口」
  • 查证结果只依赖 1 个 WebSocket 接口

    wss://qingpu-data-api.metamaker.cn/common/asr_hub
      ?project=qingpu&engine=aliyun_dashscope&is_long_connection=true&heartbeat=true
    
    • host 取自 VITE_ASR引擎是 aliyun_dashscope(阿里云百炼),不是讯飞
    • 鉴权不调接口:token 硬编码在 src/three-libs/asr/index.ts(JWT iss: human-large-screenexp 2034-12-29)→ ASR 没有 DMS 那种每天换 token 的问题
    • 调用方硬编码传了个 appKey,但当前分支用不到(只在讯飞分支的 gen_auth 用)
    • 讯飞分支(gen_auth + wss://{VITE_XF_ASR}/api/human_asr/v2/asr走不到index.html 硬编码 globalThis.isXF = false
  • ⚠️ 修正了我自己写错的文档current-api-call-sites.md 第 3 条原先写 「接口:{VITE_ASR}/api/human_asr/v2/asr/gen_auth 取鉴权,再开 WebSocket」—— 那是讯飞分支的地址、而且是未启用的那条;已改成实际在跑的 /common/asr_hub (含引擎、鉴权、未启用分支三部分)

  • 运行过的验证:逐行读 asr/index.tsfetchToken / openSocket 分支与 isXF 赋值处;validate-harness 通过

  • 已记录证据:本文件 Session 046;docs/reference/current-api-call-sites.md 第 3 条

  • 更新过的文件或工件harness/docs/reference/current-api-call-sites.md、本文件

  • 已知风险或未解决问题

    • ⚠️ 那条硬编码的 ASR token(exp 2034)是写在源码里、且入库的凭据——既有状况, 不是本轮引入;若要收敛,得 ASR 服务那边给个正规的取 token 方式
    • ⚠️ VITE_XF_ASR / 讯飞分支整条是死代码(isXF 恒 false),但没删(不在本轮范围)
  • 下一步最佳动作:无(本轮为查证 + 改文档)

Session 047

  • 日期:2026-09-18
  • 本轮目标:用户认为「three-libs 是数字人,不需要,删掉」,并问语音识别该用厂商的还是独立
  • ⚠️ 先纠正了一个会误删的前提three-libs 不是数字人
    • 实测 find src/three-libs -type f只有 asr/index.ts 一个文件
    • 正在被使用BusinessAssistantPC.vue:347BusinessAssistantMobile.vue:405import ASR from '@/three-libs/asr/index' —— 点麦克风走的就是它
    • 若按原计划删除,语音识别功能会直接消失
    • 顺带发现并更正了 architecture.md 的一句失实描述:「src/three-libs/metamaker 是未被引用的 SDK 包」——该目录根本不存在(同日核查)。已改为 「three-libs 下只有 asr,且在用;目录名与 three.js 无关,有误导性」
    • 数字人相关的实际残留是别的东西VITE_STREAM_SERVER / VITE_STREAM_TOKEN / VITE_HUMAN_IDLE_MP4_URL 三个无人读取的 env + vite 的 /stream/ 代理(target 硬编码)
  • 已完成:更正 architecture.md 的失实描述(上)
  • 运行过的验证find 列出 three-libs 全部文件;grep 反查 import 方;validate-harness 通过
  • 已记录证据:本文件 Session 047;architecture.md 的更正
  • 更新过的文件或工件harness/docs/architecture.md、本文件
  • 已知风险或未解决问题
    • ⚠️ 待用户确认:要删的到底是哪个?数字人那部分实际只剩三个没人读的 env + 一个 vite 代理, 删它们不影响任何功能;而 three-libs/asr 不能删
    • ⚠️ 语音识别「用厂商的还是独立」——本轮只给了建议,未动代码
  • 下一步最佳动作:等用户确认删除范围;并给 ASR 的去留建议(见对话)

Session 048

  • 日期:2026-09-18
  • 本轮目标:用户决定「先不动,先用原来的语音识别功能」
  • 已完成
    • 不动代码。把这个决定与相关风险落进 docs/exec-plans/tech-debt-tracker.md 第 6 条, 免得下次会话又从头讨论:
    • 现状:ASR 走厂商 wss://qingpu-data-api.metamaker.cn/common/asr_hub(引擎 aliyun_dashscope), token 与 appKey 硬编码在源码里(token exp 2034,入库可见)
    • 决定:先用厂商的(能用、无成本、前端零维护)
    • ⚠️ 风险:厂商若失效 token 或停掉网关,语音会突然不可用且无预警;音频流经厂商服务器
    • 退路:协议契约已存档(reference/current-api-call-sites.md)→ 自建网关时前端零改动
    • 低成本前置动作(未做):把 isXF 写死改成 env 开关,便于改配置切讯飞分支
  • 运行过的验证git status 干净;validate-harness 通过
  • 已记录证据:本文件 Session 048;tech-debt-tracker.md 第 6 条
  • 更新过的文件或工件harness/docs/exec-plans/tech-debt-tracker.md、本文件
  • 已知风险或未解决问题:见技术债第 6 条(厂商依赖 + 硬编码凭据)
  • 下一步最佳动作:无(本轮为记录决定)

Session 049

  • 日期:2026-09-18
  • 本轮目标:完善上传图片/附件功能(用户选方案②:把 OSS 地址拼进 question), 并说明「若要替换厂商接口需要提供什么」
  • 已完成
    1. 新增纯函数 buildQuestionWithAttachments(question, fileUrls)shared.ts): 原文 + 换行 + 每条 URL 一行;不加「附件:」之类自定义措辞(不凭空发明后端可能不认的写法); 无有效 URL 时原样返回(不能凭空多换行,否则影响所有普通提问)
    2. sendMessage 拆开两个变量(关键):
      • questionForBackend = 拼了附件地址 → 只发给后端
      • text = 用户原文 → 本地消息内容、会话标题、DMS 记录 这样用户气泡里不会出现一长串 URL,DMS 记录也保持可读
    3. 恢复两个上传入口按钮BusinessAssistantMobileInputShell.vue 里原先整块注释): 上传图片 / 上传附件。拖拽与粘贴本来就是通的,不是本轮才恢复
    4. OSS_SIGNATURE_URL 改为可配置:新增 env VITE_UPLOAD_SIGN_URL(四份 env 都写, 值仍是厂商地址)→ 将来换自建签名服务只改配置、不改代码
  • 实测确认(重要)
    • 厂商的签名接口今天可用,且完全不需要鉴权POST //qingpu-data-api.metamaker.cn/common/qp_signed_url body {ext} → 返回 file_urlhttps://prod.heijingai.com/qingpu/<uuid>.<ext>)、hosthttps://heijing-products.oss-cn-hangzhou.aliyuncs.com)、keypolicy
    • ⚠️ 上传的文件实际存在厂商的 OSS bucket(heijing-products)里,不是我们的 —— 政务场景下这点值得评估(数据在第三方)
    • 签名有效期约 15 分钟(policy 的 expiration)
  • 运行过的验证
    • 新增 harness/tools/verify-attachment-question.mjs —— 10/10 通过: 无附件原样返回(undefined/null/空数组/全空串)、有附件时原文在前且 URL 各一行、 不添加自定义措辞、脏数据过滤、URL 去空格
    • npm run build 通过
    • 签名接口 curl 实测(见上)
  • 已记录证据:本文件 Session 049;verify-attachment-question.mjs;签名接口实测响应
  • 更新过的文件或工件src/components/business-assistant/shared.tssrc/components/business-assistant/useBusinessAssistantChat.tssrc/components/business-assistant/useBusinessAssistantUpload.tssrc/components/BusinessAssistantMobileInputShell.vue、四份 .env.*harness/tools/{_entry-attachments.ts,verify-attachment-question.mjs}(新增)、 根 README.mdharness/feature_list.json、本文件
  • 已知风险或未解决问题
    • ⚠️ 未在浏览器实测:要确认「选文件 → 上传成功 → 提问 → 后端确实收到带 URL 的 question」
    • ⚠️ 后端如何解析这段 URL 未知:现在只是把 URL 附在问题后面,后端认不认、会不会去抓取, 没有验证过(这是方案②的固有不确定性)
    • ⚠️ 附件地址会随 question 进入后端;DMS 记录存的是原文(本轮特意拆开), 若要改成一并入库,改一行
    • ⚠️ 上传的图片/附件存放在厂商的 OSS bucket(数据在第三方)
  • 下一步最佳动作:浏览器实测整条链路;并确认后端能否识别拼接的 URL

Session 050

  • 日期:2026-09-18
  • 本轮目标:用户要求「先帮我清理下当前项目」
  • 先做了审计再动手(不凭印象删):
    • 逐个 env 变量反查「谁在代码里读」→ 找出5 个无人读的
    • 扫重复定义 → .env.development 里 4 个变量定义多次
    • 扫未引用文件 / 空目录 / 构建产物
  • 已删除
    1. 5 个无人读的 env 变量(四份 env 里共删 17 行): VITE_STREAM_SERVERVITE_STREAM_TOKEN(含两个凭据)、 VITE_HUMAN_IDLE_MP4_URLVITE_PLUGIN_URLVITE_PLUGIN_ASR —— 都是数字人/插件那条从未接入本项目前端的链路的遗留
    2. .env.development 的重复定义VITE_ASRVITE_XF_ASR 各定义两次,合并成一份
    3. vite 的 /stream/ 代理(无代码使用);/asr/ 代理保留(讯飞退路的一部分)
    4. 两个空目录 public/imagespublic/pencil_exports
    5. stats.html(968K,rollup visualizer 的分析产物,npm run build 会重新生成)
  • ⚠️ 过程中我自己犯了一个错并当场修正:合并重复 env 时,我的脚本保留了第一份, 但 .env 的语义是后定义的生效 —— 于是 VITE_ASR 被改成了原本的死值 (wss://human-screen-v3...),会弄坏语音识别。核对 diff 时发现,已把保留的那两行 改回正确的生效值(https://qingpu-data-api.metamaker.cn/asr)。 教训:合并重复配置前要确认哪一份生效(.env 是后者覆盖前者)。
  • 刻意保留(没删,附理由)
    • src/three-libs/asr/ —— 语音识别,在用(用户明确「先用原来的」)
    • VITE_XF_ASR + vite /asr/ 代理 —— 切引擎的退路(与 ASR 那个决定配套)
    • src/components/stream-message-coordinator.ts —— 架构文档明确「保留未用,勿改」
    • PolicyListUpdatedMatch.vue —— 老会话历史里可能仍有 <policy-list-updated>
    • dist/(3.1M)—— 是部署产物,不是垃圾;要删另说
  • 运行过的验证
    • npm run build 通过
    • 关键功能自检:VITE_ASR 生效值正确、VITE_XF_ASR=/asr 与代理保留、 上传签名地址在、政策库 json 在
    • 残留检查:5 个被删变量在代码里零命中vite.config.ts 里那 3 处是我写的说明注释)
    • dev server 实测:8083 首页 200、/chat-api 打真实后端 200(env 缩减后没坏)
    • validate-harness 通过
  • 已记录证据:本文件 Session 050;上面的审计与自检输出
  • 更新过的文件或工件:四份 .env.*vite.config.ts、根 README.md、本文件 (删除:stats.htmlpublic/images/public/pencil_exports/
  • 已知风险或未解决问题
    • ⚠️ 被删的凭据(VITE_STREAM_TOKEN仍在 git 历史里;它们是数字人流的 token, 现无代码使用,但若安全要求彻底清除需重写历史
    • ⚠️ dist/ 未删(部署产物),要腾空间可自行删
  • 下一步最佳动作:无(本轮为清理);上传功能的浏览器实测仍待做

Session 051

  • 日期:2026-09-18
  • 本轮目标:用户问「回答时间很长,是前端还是后端的问题」——用测量回答,不猜
  • 新增 harness/tools/probe-answer-latency.mjs:打真实后端,记录每个 SSE 事件的时间戳, 并把前端打字机的展开时间按 TextContent.vue 的真实参数模拟出来,拆成两段

实测结果(三个问题各一次)

问题 后端耗时 回答字数 前端打字机 用户感知
「你好」 1.3s 81 0.5s 1.7s
「高新技术企业能享受什么优惠」 49.8s 767 1.9s 51.7s
「奶茶店有什么扶持政策」 65.0s 896 2.1s 67.1s
  • 结论:后端问题。前端打字机只占 1.9~2.1 秒(约 3%),后端占 49.8~65 秒
  • 后端不是流式的(观感的第二大因素):summary(正文)在 48.4s 整段到达, 在此之前界面上没有任何正文,只有「思考中」卡片 + 心跳提示"还在为您准备答复"
  • 时间花在哪(事件时间线,以 33s 那次为例): 0~4.3s handoff→plan→retrieve;4.3~30.8s 是 recommend 阶段(26.5 秒,期间只有 heartbeat); 30.8s 才出 source + summary
  • 简单问题(「你好」)1.3s → 说明不是链路普遍慢,是业务问题的检索/推荐环节慢

⚠️ 过程中我自己的测量口径错了一次(已修正)

第一版脚本只统计 answer 事件的字数,而这个后端把正文放在 summaryresult.response 是同样的快照)→ 一开始得出「回答总字数 0」,误以为是"后端零输出"。 已改为统计 answer + summary不含 result 快照,因为适配层也只展示一次)后重测, 上面的表是修正后的数据。

前端这边的情况(结论:不是瓶颈)

  • 打字机已调到主流上沿(121 字符/秒起步 + 积压加速,实测平均 400+ 字符/秒)
  • 即便再调快,收益是秒级;而后端是几十秒级 → 优先治后端

  • 运行过的验证:三个问题各实测一次(原始事件时间戳为证);validate-harness 通过

  • 已记录证据:本文件 Session 051;harness/tools/probe-answer-latency.mjs(可重跑)

  • 更新过的文件或工件harness/tools/probe-answer-latency.mjs(新增)、本文件

  • 已知风险或未解决问题

    • ⚠️ 样本只有 3 次,且每次问题不同;若要更可靠,应固定问题重复多次取中位数
    • ⚠️ 「后端为什么慢」超出的前端范围(recommend 阶段内部发生了什么,前端看不到)
  • 下一步最佳动作:把这份数据给后端;最有效的一步是让后端改成流式(边生成边推 answer/summary),用户等待感会立刻不同;其次是查 recommend 阶段为何要 26 秒

Session 052

  • 日期:2026-09-18
  • 本轮目标:用户要求「『更多』侧边栏只展示回答中存在的政策,不要整个会话的」 → 即把 Session 026 做的会话级跨消息聚合收回成本条回答的卡片
  • 已完成
    • PolicyMatch.vuerebuildCardList():数据源改回本组件自己的卡片props.policyData / props.policyContent,与 syncPolicies 同源), 不再 inject('collectSessionPolicyCards') 往上层取会话级聚合
    • 连带清理(已成死代码):
    • useBusinessAssistantChat.ts:删掉 provide('collectSessionPolicyCards', …) 与对应的 import
    • policy-match-utils.ts:删掉 collectPolicyCardsFromMessages() 及其 parsePolicyTableContent import(该函数是它唯一的使用者)
    • harness/tools/_entry-policy-cards.ts:去掉该导出
    • verify-policy-card-list.mjs:删掉第 1 节(跨消息聚合的断言)、 第 6 节改用直接传入卡片数组;头部两段说明也改了(有一段还写着 「侧边栏要的是整个会话的聚合」,与新行为自相矛盾,留着会误导)
  • 保留(这三步仍是列表要用的):去重 dedupePolicyItems、排序 sortPolicyCardsByMatchScore、关键词过滤 matchesPolicyCardKeyword —— 同一回答里若出现重复条目,仍会去重
  • 运行过的验证
    • verify-policy-card-list.mjs 24/24 通过(原 31 项中去掉 7 项聚合断言)
    • npm run build 通过
    • 残留检查:collectSessionPolicyCards / collectPolicyCardsFromMessages 在源码与入口里零命中(只剩我写的"已移除"说明注释)
    • validate-harness 通过
  • 已记录证据:本文件 Session 052;verify-policy-card-list.mjs
  • 更新过的文件或工件src/components/Chat/PolicyMatch.vuesrc/components/Chat/policy-match-utils.tssrc/components/business-assistant/useBusinessAssistantChat.tsharness/tools/{_entry-policy-cards.ts,verify-policy-card-list.mjs}、 根 README.mdharness/feature_list.json、本文件
  • 已知风险或未解决问题
    • ⚠️ 未在浏览器实测:要确认点「更多」看到的确实是本条回答的政策
    • ⚠️ 与 Session 026 的记录方向相反(那次用户选的是「本会话全部卡片」)—— 已在本次记录与 feature_list 里写明是用户 2026-09-18 的调整,不是回归
  • 下一步最佳动作:浏览器实测

Session 053

  • 日期:2026-09-18
  • 本轮目标:用户在浏览器确认「更多」侧边栏改动「检查无误」——落进记录
  • 已完成
    • policy-card-list-sidebar 的 verification 去掉「❌ 未做浏览器实测」, 换成用户的实际确认;evidence 同步更新
  • 运行过的验证validate-harness 通过;git status 干净
  • 已记录证据:本文件 Session 053;feature_list.jsonpolicy-card-list-sidebar
  • 更新过的文件或工件harness/feature_list.json、本文件
  • 已知风险或未解决问题
    • ⚠️ file-upload 仍是 in_progress:它的「浏览器端到端」还没确认过 (本轮用户的「检查无误」是指「更多」侧边栏那条)。上传那条要确认的是: 选文件 → 上传成功 → 提问 → 后端确实收到带 URL 的 question
  • 下一步最佳动作:确认上传功能的浏览器实测(或明确挂起)

Session 054

  • 日期:2026-09-18
  • 本轮目标:用户确认上传功能「确认无误」——落进记录
  • 已完成
    • file-upload 的 verification 去掉「❌ 未做浏览器端到端」,换成用户确认; 状态 in_progress → passing
    • 仍如实保留一条边界说明:前端能保证的是「按约定把附件地址拼进 question 发出去了」; 后端如何处理这段 URL(认不认、会不会抓取)属后端行为,不在前端验证范围内
  • 运行过的验证validate-harness 通过(19 项功能,in_progress 0 项);git status 干净
  • 已记录证据:本文件 Session 054;feature_list.jsonfile-upload
  • 更新过的文件或工件harness/feature_list.json、本文件
  • 已知风险或未解决问题
    • ⚠️ 上传的图片/附件存放在厂商的 OSS bucketheijing-products,杭州), 链接为 prod.heijingai.com——数据在第三方,政务场景下需评估(用户知悉)
    • ⚠️ 取签名的接口也是厂商的(实测无需鉴权)——已做成 env VITE_UPLOAD_SIGN_URL, 换自建服务时只改配置;替换需要提供的清单已在本会话给出(OSS 凭据 + 一个签名服务 + 文件访问前缀)
  • 下一步最佳动作:功能清单里已无 in_progress;可选方向见 Session 031 列的优先级

Session 055

  • 日期:2026-09-18
  • 本轮背景:用户反馈移动端真机问题(夸克:第 3 段建议词被输入框遮住;vivo:后两段被遮), 并给出自己的方案:顶栏矮一点、图片小一点、标题字号小且不换行、按需合理调整
  • 已完成(按用户方案,BusinessAssistantMobile.vue 首页):

| 元素 | 改前 | 改后 | |---|---|---| | 顶栏 .mobile-header | 64px | 52px | | logo .overlay-icon(含下边距) | 130×88 + 41 | 96×65 + 20 | | 大标题 .heading | 28px(13 字放不下会折两行) | 22px + clamp(18px,5.6vw,22px) + nowrap(恒定单行) | | 欢迎区 gap | 7px | 6px | | 介绍文案行高 | 1.6 | 1.5 | | 建议词区 | padding 28 / gap 12 | padding 18 / gap 10 |

  • 标题字号用「先固定 22px、再 clamp 覆盖」的写法:老内核拿到 22px(窄屏也放得下), 新内核按屏宽继续收 —— 目的是保证恒定单行
  • 估算内容总高 ~723px → ~601px(−17%)
  • ⚠️ 未解决(本轮不假装解决):手机可视高度(去掉地址栏)通常 560~600px, 601 仍很紧,小屏可能还是差一点根因没动.home-fixed-input 仍是 position: fixed 悬浮、不占文档流,而给它预留的是写死的 padding-bottom: 180px,与输入框实际高度无关 —— 字体一放大、输入框一换行,180px 就不够
  • 运行过的验证npm run build 通过;逐项核对新值已落进源码
  • 已记录证据:本文件 Session 055;技术债第 7 条;上面的量化表
  • 更新过的文件或工件src/components/BusinessAssistantMobile.vue、本文件、技术债清单
  • 已知风险或未解决问题
    • ⚠️ 未在真机验证:需用户在夸克 / vivo 上再看一次
    • ⚠️ 若仍被遮,下一步只能上「输入框占位」(去掉 position: fixed,改成 .ba-mobile-container 的 flex 子项 + .mobile-main { min-height: 0 })—— 那样剩余高度是浏览器算出来的,与字体/内核无关,数学上不会再遮
  • 下一步最佳动作:用户真机确认;若仍遮则做「输入框占位」

Session 056

  • 日期:2026-09-18
  • 本轮背景:用户在真机确认「不遮了」,但反馈观感太丑:中间文字太大、两侧贴边
  • 已完成BusinessAssistantMobile.vue 首页):
    • .welcome-section 新增两侧留白 padding: 0 28px(连 .mobile-main 的 8px, 视觉留白约 36px)—— 标题与介绍文案都不再贴屏幕边
    • .heading 字号 22px → 20pxclamp 同步收为 clamp(17px, 5.2vw, 20px)
    • .desc 字号 14px → 13px:加留白后可用宽度变窄,字号同步收一点以避免多折一行
  • 排版验算(360px 宽手机,脚本实算):
    • 可用宽度 288px(360 − main 16 − 留白 56)
    • 标题 13 字 × 20px = 260px → 单行,两侧各富余 ~14px
    • 介绍文案 61 字:@14px@13px 都是 3 行 → 字号调小没有增加高度 ✓
  • 运行过的验证npm run build 通过;排版用脚本实算(非目测)
  • 已记录证据:本文件 Session 056
  • 更新过的文件或工件src/components/BusinessAssistantMobile.vue、本文件
  • 已知风险或未解决问题
    • ⚠️ 未在真机确认:需用户在夸克 / vivo 上再看一眼观感
    • ⚠️ 那个「思考过程文字超出屏幕」仍未处理(见技术债第 7 条:靠 JS 测量决定换行, 字体一变就判错)
  • :本轮发现上一轮的压缩改动(顶栏 52px、logo 96×65、标题 clamp 等)已在 9ccf536 提交里;本次是在其基础上继续调字号与留白
  • 下一步最佳动作:用户真机看观感;如仍不满可继续微调数值

Session 057

  • 日期:2026-09-18
  • 本轮目标:用户两条指示 —— ①首页标题「你好,有什么我可以帮您的?」删掉「你好」 ②上一段会话异常结束,检查是否留下异常
  • 已完成

① 文案改动(用户要求)

  • BusinessAssistantPC.vue:127BusinessAssistantMobile.vue:39 的标题 你好,有什么我可以帮您的?有什么我可以帮您的?(两端一致,只动文字)
  • 页面介绍文案里的「请问有什么可以帮助您?」未动(用户只说标题那句)

② 异常检查(用户要求)—— 先说结论:上次会话没丢工作,异常在"记录"不在"代码"

查过的项与结果(全部为实际执行输出,非推断)

检查 结果
git status / HEAD 与 origin/main ✅ 干净、一致——没有未提交或未推送的改动
最后一次提交之后有无源文件被改(find -newermt,排除忽略项) ✅ 无(只有我本轮改的两个文件)
bash harness/init.sh ✅ exit 0(含 npm install + npm run build
node harness/tools/validate-harness.mjs ✅ 通过(19 项功能,in_progress 0 项)
10 个验证脚本 ✅ 全绿:33/11/24/8/10/21/30/10/12 + 打真实 DMS 的 43/43
exec-plans/ ✅ active 只剩 dms-api-replacement.md(在办),completed 3 个
dev server 8083 ✅ 在跑,首页 200

→ 上一段会话(Session 056)在提交 f1951af 之后结束,文件夹层面没有半成品。

查到 5 处异常,全是「记录与事实不符」,已当场修掉

  1. CLAUDE.md 末尾自相矛盾:底部仍写着「harness/harness/docs/.gitignore 约定不入库」,而上方「提交约定」写着 2026-09-17 起已入库 (Session 024 声称改过三处,漏了这一处)→ 已改为如实说明。
  2. README.md 从未有过 ## 20260918:09-18 的改动(DMS 系列、上传恢复、 统计埋点删除、清理…)一直被追加在 ## 20260917 标题下面(从 09-18 第一个提交 061bcfc 起就这样,不是本次才漂的)→ 已在正确位置补上 ## 20260918
  3. README 漏记移动端两轮改动(Session 055/056 都没写 README)→ 已补两条: 真机"被输入框遮住"的布局瘦身、以及随后的留白/字号调整;并如实写明"根因没动"dvhvh 兜底、无 text-size-adjustpadding-bottom: 180px 写死)。
  4. feature_list.jsondms-chat-storage 条目内部打架:同一段 notes 里 同时存在「1887 写入已恢复(43/43)」与「🚨 1887 会话写入当前是坏的」、 已推翻的「一个会话一行」粒度、以及「浏览器人工点验始终没做」 (而同条 verification 里写着用户 09-17 已确认);meta.last_updated 也停在 09-17。 → 已把这些按当前事实重写成一段(在写的/已停用的/规矩/踩过的坑/已知缺口), 并去掉与 verification 冲突的表述;payload 脚本断言数由 9 更正为 12
  5. 技术债第 7 条没反映最新进展 → 补「当前进度」:已按用户方案给首页瘦身、 用户真机确认"不遮了",但三个根因一个都没动,换机/调字号仍可能变样,方案 A/B/C 仍有效。
  • 运行过的验证
    • npm run build 通过(改动后重跑)
    • bash harness/init.sh exit 0;validate-harness.mjs 通过
    • 10 个验证脚本全绿(含打真实 DMS 的 verify-dms-chat-storage.mjs 43/43, token 有效期至今晚 21:33)
    • 标题文案 grep:全仓库只剩新文案,PC/移动端一致
  • 已记录证据:本文件 Session 057;上面的检查表;根 README.md 的 20260918 节; feature_list.jsondms-chat-storage 重写;技术债第 7 条的「当前进度」
  • 提交记录:见下条提交(已按约定提交推送)
  • 更新过的文件或工件src/components/BusinessAssistantPC.vuesrc/components/BusinessAssistantMobile.vueCLAUDE.mdREADME.mdharness/feature_list.jsonharness/docs/exec-plans/tech-debt-tracker.md、本文件
  • 已知风险或未解决问题
    • ⚠️ 文案改动未在浏览器/真机看过:需确认 PC 与移动端首页标题都是「有什么我可以帮您的?」, 且移动端标题仍是单行nowrap + 字号没变,去掉 3 个字只会更宽松,理论无风险)
    • ⚠️ 记录漂移是系统性的,不是一次意外:README 的日期节、feature_list 的 notes 都是"只往上加、不回头改",越积越互相矛盾。下一轮起,改完当场回写—— 这次靠人工发现,validate-harness.mjs 目前查不出这类内容矛盾。若要加机械约束, 可考虑"README 的日期节必须覆盖最近提交日期""notes 里出现互相矛盾的结论"这类检查。
    • ⚠️ policy-detail-index-mismatch 仍保留「浏览器未人工点过」的保守标注(Session 028 的决定), 用户若顺手验过「同政策两个申报事项点开内容不同」,即可去掉该标注
  • 下一步最佳动作:真机/浏览器扫一眼新标题;若要继续收敛,见 Session 031 的优先级清单

Session 058

  • 日期:2026-09-18
  • 本轮目标:用户改主意 —— ①把「你好」加回来 ②首页标题字号整体收小 4px
  • 已完成
    1. 文案加回(上一轮刚删的):PC 与移动端首页标题恢复为 你好,有什么我可以帮您的?
    2. 标题字号 −4px: | 端 | 改前 | 改后 | |---|---|---| | PC .heading | 32px | 28px | | 移动端 .heading | 20px + clamp(17px,5.2vw,20px) | 16px + clamp(13px,4.2vw,16px) |
    3. 🐛 顺带移除一处会「吃掉」这个改动的陈旧覆盖(本轮新发现,不是用户要求的):
      • 移动端 @media (max-width: 360px) 里有一条 .heading { font-size: 28px }
      • 原本是空操作(那时 .heading 基础值就是 28px), 但 2026-09-18 基础值改小后,它在窄屏上反而把标题放大回 28px
      • 后果:标题是 white-space: nowrap,13 个字 × 28px ≈ 356px, 而 360px 屏可用宽度只有 272px标题被顶出屏幕约 84px(正是用户此前抱怨的那类现象)
      • 已按项目约定注释保留原文并写明原因(不是直接删);窄屏收缩交给 .heading 自己的 clamp
  • 运行过的验证
    • npm run build 通过
    • 排版实算(脚本,非目测):8 种屏宽(320/360/375/390/393/412/428/768)逐一算 「生效字号 × 13 字 + 字距」与「可用宽度」——全部单行放得下, 最紧的 320px 屏富余 66px;同时算出被移除的 28px 覆盖在 360px 屏会溢出 84px
    • grep 复核:全仓库 .heading 只剩 PC 与移动端两处定义(窄屏覆盖已注释),无其他覆盖源
  • 已记录证据:本文件 Session 058;上面的排版实算输出;根 README.md 20260918 节 (上一轮那条「去掉你好」的记录已改写为「一度去掉、随即加回」——不留错误的旧记录)
  • 提交记录:见下条提交(已按约定提交推送)
  • 更新过的文件或工件src/components/BusinessAssistantPC.vuesrc/components/BusinessAssistantMobile.vueREADME.md、本文件 (feature_list.json 未动:纯样式/文案调整,无新增能力)
  • 已知风险或未解决问题
    • ⚠️ 未在真机/浏览器看过:需确认①两端标题都有「你好」②字号确实小了 ③窄屏(≤360px)手机上标题不再被顶出屏幕(这条是本轮修的那个覆盖,值得顺手一看)
    • ⚠️ 那个 28px 覆盖是一类通病:样式表里"与基础值同值的 media query 覆盖", 基础值一改它就变成有效覆盖。本轮只发现并处理了这一处,没有全表排查
  • 下一步最佳动作:真机确认三点(尤其窄屏);若还有别处"改基础值后行为变了", 按同样方法找同值覆盖

Session 059

  • 日期:2026-09-18
  • 本轮目标:用户否掉上一轮的字号改动 —— 「PC 不改,移动端太小了,变回原来的」
  • 先问清了两处歧义(不猜):
    1. 「PC 不改」可两解(保持 28px / 撤回 32px)→ 用户选 32px,撤回
    2. 移动端"原来的"可两解(20px / 更早的 28px)→ 用户选 20px
  • 已完成
    • PC 标题字号还原:28px → 32px(本次改动前的原值)
    • 移动端标题字号还原clamp(13px,4.2vw,16px)clamp(17px,5.2vw,20px)
    • 「你好」保持加回状态(用户未要求再改文案)
    • 移动端窄屏那条 28px 覆盖继续维持移除(用户选了 20px 这一档,不是 28px)
  • ⚠️ 本轮发现并当场告知用户的一件事(他手动改的那行其实没生效): 用户在文件里手动把基础值写成了 font-size: 20px,但紧跟着的 font-size: clamp(13px, 4.2vw, 16px) 在后面、会覆盖它(同优先级后者胜) → 现代浏览器上实际渲染仍是 ≤16px,手改等于没改。 已在代码注释里写明「两行必须同进同退」,避免下次再踩。
  • 运行过的验证
    • npm run build 通过
    • 排版实算(脚本):8 种屏宽逐一核对,全部单行放得下; 360px 屏生效字号 18.72px、富余 37px(该屏可容纳的最大字号是 21.6px, 即 20px 这档是安全的)
    • 源码核对:PC 为 32px、移动端两行同为 20px 档;两处注释都已更新(不留上一轮的 −4px 记录)
  • 已记录证据:本文件 Session 059;上面的排版实算输出;README 20260918 节 (上一轮写的「收小 4px」条目已改写为「一度收小、已全部还原」——不留失效记录)
  • 提交记录:见下条提交(已按约定提交推送)
  • 更新过的文件或工件src/components/BusinessAssistantPC.vuesrc/components/BusinessAssistantMobile.vueREADME.md、本文件
  • 已知风险或未解决问题
    • ⚠️ 仍未在真机/浏览器看过:需确认移动端标题观感(现在 20px 档,比刚才的 16px 大 4px; 窄屏手机上按 clamp 会到 17~18.7px)
    • ⚠️ 一条待用户确认的遗留:窄屏(≤360px)那条 28px 覆盖已移除。 如果用户手机上标题"原来的"其实是 28px 那种大小(说明该覆盖此前一直在他设备上生效), 他这次选的 20px 会比印象中更小 —— 届时说一声即可恢复那一条
    • 📌 教训(写进注释了):font-size 写两行做降级时,只改基础值不生效; 这类"同值覆盖"在改基础值后会变成有效覆盖(本轮 Session 058 已经踩过一处 28px 的)
  • 下一步最佳动作:真机看一眼移动端标题;确认窄屏那条覆盖是否要恢复

Session 060

  • 日期:2026-09-18
  • 本轮目标:用户指示「移动端调整到 22px」(PC 不动)
  • 已完成
    • 移动端 .headingclamp(17px, 5.2vw, 20px)clamp(17px, 5.7vw, 22px) (基础值同步改 22px;两行一起改,见 Session 059 的坑)
    • 5.7vw 这个系数是按上限等比推的(原 5.2vw 对应上限 20px): ≥386px 的屏恒为 22px,更窄的屏按比例收缩
    • PC 未动(保持 32px)
    • 注释里写清了这一串演进(20 → 16 → 20 → 22),免得下个会话看到 clamp 系数变化不明所以
  • 运行过的验证
    • npm run build 通过
    • 排版实算(脚本,9 种屏宽):全部单行放得下 —— 386px 及以上生效 22px(富余 ≥36px);375px 生效 21.4px;360px 生效 20.5px; 320px 生效 18.2px(富余仅 3px,是最紧的一档
    • 同时算出各屏上限:320px 屏最多 18.5px、360px 最多 21.6px、375px 最多 23.9px → 22px 在 375px 及以上才完整生效,360px 上会收到 20.5px(不是 bug,是防溢出
  • 已记录证据:本文件 Session 060;上面的排版实算输出;README 20260918 节 (该条已改写为「当前值:PC 32px / 移动端 22px」,并列出反复过程)
  • 提交记录:见下条提交(已按约定提交推送)
  • 更新过的文件或工件src/components/BusinessAssistantMobile.vueREADME.md、本文件
  • 已知风险或未解决问题
    • 用户当轮回复「可以了」,本档(移动端 22px / PC 32px)定稿 —— 不再继续调整。⚠️ 用户未逐项说明是否在真机看过,故不写成「真机验证通过」, 只记为「用户接受当前值」;若日后真机上发现问题,回来改 clamp(17px,5.7vw,22px) 即可
    • ⚠️ 320px 档富余只剩 3px,字体渲染有细微差异时可能贴边——真机若见贴边, 把下限或系数再收一点
    • ⚠️ 硬约束备忘:标题 13 字且 nowrap各屏能容纳的上限是 320px→18.5px / 360px→21.6px / 375px→23.9px(脚本实算)。再往上调就得 先收窄两侧留白或允许折行
  • 下一步最佳动作:无(本轮已定稿);若要继续做别的,见 Session 031 的优先级清单

Session 061

  • 日期:2026-09-18
  • 本轮目标:用户给出新接口文档 reference/company-classification.md 与业务需求 —— 对话返回 company_info 非空时调分类接口,把结果同步到 DMS 的 1888(企业基础信息) 与 1886(企业荣誉信息):没有就新增、有不一致就更新;DMS 缺的字段我来加或列清单给他
  • 方案docs/exec-plans/active/company-classify-dms-sync.md (计划已落库,含 Step-0 实测与字段清单)

用户拍板的四条

①DMS 读写复用现有路径(「原来怎么用现在就怎么用」,不走 121.43.55.7:2101/proxy_dms) ②六个角度各存 1888 一个字段 ③1886 一个标签一行 ④1888 对比范围 = 身份三字段 + 6 个标签字段

Step-0 探测(本轮最有价值的两条结论)

探测 结果
DMS 字段 7 个字段已经加好了POST /dms/model/getModelById,2026-09-18 17:35–17:36 由 DMS 侧更新):1888 增 c_tag_basic/c_tag_honor/c_tag_sector/c_tag_operation/c_tag_industry/c_tag_license(模型 41→47 字段),1886 增 c_tag_name(11→12 字段)。不需要再请人加字段,原计划那份清单作废
模块 must 1888 只有 c_credit_code;1886 是 c_id + c_credit_code(都实测自模型定义,不再是猜)
🚨 classify 接口 已部署,但对任何带内容的请求体返回 422 invalid_request —— 文档自己的合成示例、纯 ASCII 体(排除编码因素)、company/profile/honors/company_info/Company 各种键名、{"company":{}}{"company":null} 全部 422;只有 {} 返回 200(六角度全空 + company_info_empty)。响应头有 x-request-id 可查后端日志。属后端问题,需后端排查

实现(新增 3 个模块 + 6 处改动)

新增

  • src/network/api/dms/classification-sync-utils.ts —— 纯逻辑层(不 import vue、不发网络): 身份抽取(profile.data 优先、逐项回退 company)、标签编解码(JSON 数组字符串)、 buildEnterpriseRowdiffEnterpriseRowbuildHonorTagspartitionHonorRowsdiffHonorRowsextractFailedAngles
  • src/network/api/company-classification.ts —— classifyCompany + resolveClassifyEndpoint (base URL 复用 getChatApiBaseUrl()VITE_CHAT_API 的两种写法都处理)
  • src/network/api/dms/company-info-sync.ts —— 编排:syncCompanyInfoFromChat(入口,fire-and-forget)、 applyCompanyClassification单独导出,供验证脚本注入假分类结果)、1888 upsert、1886 集合同步

  • api-chat-coordinator.ts 5 处:ChatResultPayload/ApiChatTotalResponsePayloadcompany_info、新增 pendingCompanyInfo 实例字段、case 'result' 由空分支改为暂存、 resetTurnContent() 清空、finishTurn 的 totalResponse 非空才带出
  • useBusinessAssistantChat.ts 1 处:totalResponse 监听器里 saveTurnToDms 之后、 finishTask 之前调 syncCompanyInfoFromChat(PC/Mobile 共用,挂一次两端生效)
  • dms/client.ts:加 1888/2036、1886/2032 常量;findRowBy/enqueueWritechat-sessions-dms.ts 上移为 findDmsRowBy/enqueueDmsWrite 共用(那两处坑不想再踩第二遍)

运行过的验证

  • npm run build 通过;npx vue-tsc --noEmit 本次涉及文件无新增类型错误
  • 新增 verify-company-classify-sync.mjs —— 53/53 通过(纯逻辑,六节:身份抽取、 编解码、期望行与 diff、1886 集合、partial/保守降级、接口地址拼接)
  • 新增 verify-company-classify-dms.mjs —— 23/23 通过(打真实 DMS,注入假分类响应): 首写两栏目内容正确(含富文本字段 c_tag_honor 的逐字往返断言——它 frontType 是 content,我特意单列一条防被清洗)、同数据重跑零新增、改标签后 1888 仍一行且已更新、 1886 收敛到新集合(旧的删、新的增)、资质荣誉空集删光我方行而迁移行不动、 缺信用代码整条跳过、清理后两栏目残留 0 行
  • 回归(因上移了两个工具函数):verify-dms-chat-storage.mjs(真实 DMS)43/43verify-dms-payload-fields.mjs 12/12,其余 8 个离线脚本全绿
  • node harness/tools/validate-harness.mjs 通过(20 项功能,in_progress 1 项)

  • 已记录证据:本文件 Session 061;两个新脚本;exec-plans/active/company-classify-dms-sync.md 的 Step-0 表;reference/README.mdDMS_COLUMNS.md 的字段更新;feature_list.jsoncompany-classify-dms-sync

  • 更新过的文件或工件:新增 classification-sync-utils.tscompany-classification.tscompany-info-sync.ts_entry-company-classify.ts、两个验证脚本、 exec-plans/active/company-classify-dms-sync.md;改 api-chat-coordinator.tsuseBusinessAssistantChat.tsdms/client.tsdms/chat-sessions-dms.tsreference/README.mdreference/DMS_COLUMNS.mdreference/DMS_MAPPING.mdtools/README.md、根 README.mdfeature_list.json、本文件 (另:reference/company-classification.md 原本 untracked,本次一并入库)

  • 已知风险或未解决问题需后端配合,用户已知悉):

    • 🚨 classify 的 422 阻塞真实链路:在它修好之前,「真实分类响应」与浏览器端到端 都无法验证;DMS 侧已用注入假响应验证到位。修好后请重跑: node harness/tools/verify-company-classify-dms.mjs,并补浏览器实测
    • ⚠️ company_info 的真实载荷未知api-chat.md 里没有它的契约章节(classify 文档的 引用超前),前端按「全可选 + profile.data 优先回退 company」容错读取。 拿到真实载荷后应把契约回填进 api-chat.md
    • ⚠️ completed + 空数组 = 覆盖清空(会删光我方荣誉行):这是「分类结果即最新事实」的默认语义, 若业务要保留历史荣誉,改成「只增不删」即可(一行)
    • ⚠️ 荣誉行超 200 行会截断漏删(低风险)
    • ⚠️ 未在浏览器实测:需要在真实对话里走一遍(等后端修 422)
  • 下一步最佳动作:把 422 反馈给后端;修好后跑一次真实分类 + 浏览器端到端, 通过后本条转 passing

续:用户给了真实载荷 → 端到端打通,并更正上一段的两个结论

用户贴了一份真实的 company_info(上海元以数智科技有限公司)。用它一跑,结论翻了

  1. 📌 classify 不是「后端坏了」 —— 它是对载荷完整性的校验: 精简/合成的请求体一律 422(连接口文档里自己的示例片段都被拒), 真实完整的 {company, profile, honors} 返回 200(7 秒,六角度真实结果: 基本信息=成立年限5-10年/是否内资公司、产业信息=第三产业/软件信息、经营活动=是否在青浦区实际经营)。 → 上一段把它记成「任何带内容的请求体都 422 / 需后端排查」是不准确的,特此更正; 已写进 api-chat.md 的 company_info 章节
  2. 📌 1888 / 1886 都不是空的 —— 2026-09-17 18:12 由 DMS 侧以 ams_user 灌过数据:
    • 1888 的行是完整工商信息(53 个键:企业名/状态/地址/经营范围/注册资本…)
    • 1886 的行是真实荣誉记录c_level 国家级/区级、系统 title 存荣誉名、 c_source 存来源通知名,如「关于公布第三批国家邮政局技术研发中心认定结果的通知」) → 所以同步多数时候走的是「更新已存在的行」:只动身份三字段 + 6 个 c_tag_*, 其余工商字段不碰;1886 的所有权标记让它绝不会误删灌入的荣誉行(这条已实测) → reference/README.md 里我当轮刚写的「F4 未开始、这两栏目只有前端写的行」已改回事实

本轮补充的验证(两个新脚本 + 一次真实探测)

  • 🆕 verify-company-info-passthrough.mjs 10/10:用假 SSE 流喂真实协调器, 断言 result 里的 company_info 原样带出到 totalResponse不混进渲染正文、 无该字段时不带键、null 不传、跨轮不沿用上一轮(resetTurnContent 生效)
  • 🆕 verify-company-classify-e2e.mjs 16/16:真实 company_info → 真实分类接口 → 真实 DMS 落库;断言六角度正确落位、1886 行数=标签数、再落一次零新增
  • 真实探测 /api/chat:公司类提问的 SSE 里,result.data 的键为 response / company_info / recommendation / errors —— company_info 确实存在且拼写正确 (用户贴的 "compang_info" 是笔误),结构与代码假设逐字段一致

过程中修正了三条我自己的错

  1. e2e 首跑 3 项 FAIL,是断言写错:空角度的期望值是 [],而库里读回来是「字段不存在」 —— DMS 不落空值;已改成按解码后的集合比较(代码本身一直是对的,diff 判定「无变化零写入」)
  2. 中文请求体用 shell 里的 curl -d 会被 Git Bash 弄成 422(/api/chat 也一样), 要用 node 写成文件 + --data-binary @file —— 这条踩了两次,值得记住
  3. 上一段那条「classify 全量 422」的结论(见上)

数据现状:真实企业「上海元以数智科技有限公司」(91310118MA1JLRL16G)的 1888 行 已被同步更新(新增 c_tag_basic / c_tag_sector / c_tag_operation),这是功能的正常行为、 且幂等;需要删可用 e2e 脚本的 --cleanup

仍未做浏览器端到端(自动化证据已齐,只差真人在页面上问一个公司类问题)。 验证脚本清单已更新进 harness/tools/README.md

续 2:用户在浏览器实测宇树科技 → 「荣誉应该不止两条」→ 1886 换数据源

用户反馈:测了宇树科技股份有限公司,荣誉出来 2 条,感觉不止两条。查证结论: 同步没写错,是数据源选错了。

数量
分类接口的「资质荣誉」标签(原实现的数据源) 2(中国独角兽企业、国家级高新技术企业)——165 个标签里挑的粗标签
company_info.honors.records(真实荣誉) 31 条,含荣誉名/级别/来源/发布机构/发布日期/证书编号
库里 1886 已灌入的行 28 条——同一份企查查荣誉数据的旧快照,字段与 records 一一对应

用脚本算出的差集:载荷有、库里没有 4 条库里有、载荷没有 1 条

用户拍板:①1886 改存 honors.records,与库里的行全量对齐(补 4、删 1、同键行更新差异) ②荣誉名同时写 titlec_tag_name(顺带修掉我上轮 title="null" 的瑕疵)。

改动

  • classification-sync-utils.ts:删掉按 c_source 认所有权的旧方案,改为 parseHonorRecords / buildHonorKey(荣誉名+级别+来源)/ readHonorName(c_tag_name → title 回退, 字符串 "null" 不算名字)/ diffHonorRows(toAdd / toUpdate / toDelete,删只在 complete=true 时做)
  • company-info-sync.ts:拆成两个互不依赖的入口——applyCompanyClassification(1888 ← 分类接口) 与 applyCompanyHonors(1886 ← company_info.honors)。分类失败不再牵连荣誉同步
  • 一个真 bug 在 e2e 里被抓出来并修掉:已匹配的行不会补写 c_tag_name (字段映射漏了荣誉名那一列,只写了 title)——灌入的行本来就只有 title,不补就永远缺这一列

跑过的验证:纯逻辑 65/65(新增荣誉解析/键/增删改对齐/complete=false 不删/名字回退等断言); 真实 DMS 23/23(新脚本覆盖:一条荣誉一行、幂等、改字段、删多余、complete=false 不删、 迁移行也参与对齐);e2e 20/20(真实载荷 → 真实分类 → 真实 DMS,1886 = 31 行); 回归全绿(33/11/24/8/10/21/30/10/12 + DMS 43/43)。

顺带修好的真实数据:宇树科技(91330108MA27YJ5H56)的 1886 已从「28 灌入 + 2 标签」 对齐为与后端一致的 31 条(灌入行补上了 c_tag_name,缺的 4 条由 user_gtx 写入)。

续 3:用户新建 c_honor 存荣誉名 → 名字列改用新字段

用户指示:「现在没有存具体的荣誉名称,我新建了 c_honor 字段,在这个字段里存荣誉名称」。 探测确认:1886 模型已扩到 13 字段(18:17 更新),新增 c_honor(别名「荣誉名称」,text), 同时 c_tag_name 的别名被改成「荣誉标签」——两个字段的语义分开了。

改动

  • 荣誉名改写进 c_honor + 系统字段 title(title 供 DMS 列表显示,灌入行本来就有)
  • c_tag_name 不再当名字用,只在读取名字时作为中间回退项 (回退链 c_honor → c_tag_name → title,字符串 "null" 仍不算名字)—— 早期同步写进去的值还在,靠它仍能把已存在的行匹配上,且首次对齐会给这些行补上 c_honor
  • 新增常量 HONOR_NAME_FIELD = "c_honor"HONOR_TAG_FIELD 保留并注明只作兼容

跑过的验证:纯逻辑 66/66(新增「早期只有 c_tag_name 的行也会补上 c_honor」等断言); 真实 DMS 23/23e2e 20/20npm run build 通过;回归抽查(33/24/10/12)全绿。

真实数据:宇树科技的 31 行全部补上了 c_honor(复查「缺 c_honor 的行 = 0」, 键去重后 31 = 行数,无重复行)。

仍未做:浏览器端到端(用户实测后改了两次数据源/字段,尚未再走一遍浏览器)。