# 进度日志 > 通用的仓库内会话进度日志。文件名沿用课程历史约定,不绑定 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-upload` 与 `chat-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.md`、`harness/README.md`), 并删除原先"它是仓库内唯一依据"的错误说法——判据优先级明确为**源码备份 > 生成文档** - **运行过的验证**: - `npm run build` 通过(本次改动后又重跑) - `toChatQuestionAnswer` 单元断言 14/14 通过 - 措辞一致性 grep:无"已停用的旧协议""唯一依据"等旧表述残留 - **已记录证据**:见 `feature_list.json` 的 `questionnaire-submit-text` - **提交记录**:无(用户自行提交) - **更新过的文件或工件**:`src/components/api-chat-coordinator.ts`、`docs/reference/*`、 `CLAUDE.md`、`harness/README.md`、`harness/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.json` 的 `questionnaire-submit-text` - **提交记录**:无(用户自行提交) - **更新过的文件或工件**:`src/components/api-chat-coordinator.ts`、 `src/components/business-assistant/useBusinessAssistantChat.ts`、 `docs/reference/README.md`、`harness/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.json` 的 `company-query-error-states` - **提交记录**:无(用户自行提交) - **更新过的文件或工件**:`src/components/api-chat-coordinator.ts`、 `harness/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.json` 的 `company-need-input-on-top` - **提交记录**:无(用户自行提交) - **更新过的文件或工件**:`src/components/Chat/QuestionCard.vue`、 `src/components/api-chat-coordinator.ts`、`harness/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.json` 的 `company-need-input-on-top`(已更新) - **提交记录**:无(用户自行提交) - **更新过的文件或工件**:`src/components/api-chat-coordinator.ts`、 `src/components/Chat/QuestionCard.vue`、`harness/feature_list.json`、本文件 - **已知风险或未解决问题**: - ⚠️ **保留了一处不一致,需用户确认**:`不需要公司信息` 仍然发 `"不需要"` 而不是选项文本。 理由是那个变体的后端 `input_help` 写的是"不需要则输入'不需要'",按它给的原字符串发最稳。 若统一改成发选项文本,需确认后端也认"不需要公司信息"这个说法。 - ⚠️ "选中候选后提交文本是否会被当作 refine 而循环"**仍未验证**(上游持续 provider_failure) - **下一步最佳动作**:上游恢复后补测有候选时的选中流程 ## Session 006 - **日期**:2026-09-16 - **本轮目标**:用 `status` 字段替掉猜测式判定 - **已完成**: - **承认问题**:用户指出 `interrupt` 有 `status` 字段(`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_found`、`company_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.json` 的 `company-interrupt-status-matching` - **提交记录**:无(用户自行提交) - **更新过的文件或工件**:`src/components/api-chat-coordinator.ts`、 `harness/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** | **初次询问是否需要公司信息** ← 未处理 | - **修正的两个错误**: 1. `failed` 此前会落进 not_found 分支,问题文案被填成"未查询到匹配的公司"—— **把失败说成了查无**,违反接口文档"空候选与查询失败需分别展示""不把空候选说成查无公司"。 现在 failed 独立分支,文案说明失败原因(`result.error` 经 `describeCompanyError` 翻译), 无 error 时退回"公司查询服务暂时失败"。 2. 无 status(初次询问)此前会给"跳过公司查询",现改为"不需要公司信息"(→`不需要`), 与该变体后端 input_help 的"不需要则输入'不需要'"一致。 - 抽出 `fallbackQuestion()`,统一处理"已去重则保持为空"的逻辑,避免再次被 `||` 兜底复活。 - **运行过的验证**: - `npm run build` 通过 - 四种情况逐一断言,输出符合预期(含输入框位置、选项、问题文案) - 专项核对:`failed` 文案**不含**"未查询到";`not_found` 文案为"未查询到匹配的公司" - 去重回归:正文已含该段 → 问题字段仍为空 - **已记录证据**:`docs/reference/README.md` 新增「interrupt 的全部状态」表; 见 `feature_list.json` 的 `company-interrupt-status-matching`(已更新) - **提交记录**:无(用户自行提交) - **更新过的文件或工件**:`src/components/api-chat-coordinator.ts`、 `docs/reference/README.md`、`harness/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.ts` 与 `src/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`(文档修正) - **提交记录**:`08524fb`、`975dee9` 均已推送 origin/main - **更新过的文件或工件**:`README.md`、`CLAUDE.md`、`.gitignore`、本文件 - **已知风险或未解决问题**: - ⚠️ 两个功能性问题仍挂着(非本轮引入): ①"选中候选后提交文本是否会被当作 refine 而循环"从未验证(上游持续 provider_failure, `status: found` 一次都没出现过);②`answer.text` 与 `summary.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.md` 与 `agents.md` 按用户要求改为**不入库**(加入 `.gitignore`, `git rm --cached`,磁盘文件保留) - **README 重写**:按用户给的参考格式,只保留「按日期的改动记录」 (20260914/15/16 三节)。此前的项目概述、技术栈、目录结构、接口对接说明、 参考规范等**全部撤下**——用户明确要求项目结束再总结 - **清除项目内全部飞书内容**:README 链接 + `src/styles/common/define.less` 与 `rule.less` 头部指向飞书设计规则的外链(保留文件用途说明,仅去外链) - **运行过的验证**: - `grep -riE "飞书|feishu|lark|awbm"` 在工作区与 git 跟踪文件中**零命中** - `npm run build` 通过(改动了 .less 文件,确认不影响构建) - `git status` 干净,已推送 origin/main - **已记录证据**:提交 `d8178ca`(去跟踪)、`a41eec8`(README CHANGELOG)、 `14bab9f`(README 精简 + 去飞书) - **提交记录**:上述三个提交均已推送 origin/main - **更新过的文件或工件**:`.gitignore`、`README.md`、`src/styles/common/define.less`、 `src/styles/common/rule.less`、本文件 - **已知风险或未解决问题**: - 飞书链接**仍在 git 历史里**(`08524fb` 初始提交、`a41eec8`)。用户确认**留着无所谓**, 故不做历史重写 - 两个功能性问题仍挂着:①"选中候选后提交文本是否被当作 refine 而循环"从未验证 (上游持续 provider_failure,`status: found` 一次都没出现过); ②`answer.text` 与 `summary.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.md` 里 **Session 000–009 的历史记录保留旧路径不改**——那是当时的事实 - **已记录证据**:本文件 Session 010;`harness/README.md` 结构表 - **提交记录**:无(`harness/` 与 `CLAUDE.md` 均不入库) - **更新过的文件或工件**:整个 `harness/` 目录重构;根 `CLAUDE.md` 重写; 原根 `docs/` 迁入 `harness/docs/` - **已知风险或未解决问题**: - ⚠️ **上一轮的教训要盯住**:上次建了 7 个文件,其中 4 个建完就没再用过。 这次新增了 `quality.md`、`plans/`、`sops/session-start.md`、`sops/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"`, 非惠企政策不显示该入口。 - **列表新增数据源维度** `listSource`(`cards` | `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.mjs`; `feature_list.json` 的 `policy-library-back-entry` - **提交记录**:无(本轮改动均入库文件,提交由用户处理) - **更新过的文件或工件**: `src/components/Chat/PolicyMatch.vue`、`src/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.md`、 `harness/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.md`、`agents.md` 不入库是预期行为 - `sops/session-end.md` 的逐项检查**首项**改为"已提交并推送" - `sops/handoff.md` 的常用命令补上提交推送 - 按新约定提交并推送了上一轮的改动(`103e53b`) - **运行过的验证**: - `npm run build` 通过 - `git push` 成功;`git log` 确认提交已上远程 - **已记录证据**:提交 `103e53b`;本文件 Session 013 - **提交记录**:`103e53b` 已推送 origin/main(本条记录本身因 `harness/` 不入库,不进提交) - **更新过的文件或工件**:根 `CLAUDE.md`、`harness/sops/session-end.md`、 `harness/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.json` 的 `policy-library-back-entry`; 根 `README.md` 的 20260916 节 - **提交记录**:见下条提交(本轮改动含入库文件,已按约定提交推送) - **更新过的文件或工件**:`src/components/Chat/PolicyMatch.vue`、根 `README.md`、 `harness/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` 的逐字机 + `BusinessRecord` 的 `gapTime = 30`), 本轮只动了前者。若实测仍偏慢,下一处该看 `BusinessRecord.vue` 的 `gapTime`。 - **下一步最佳动作**:浏览器实测打字速度观感 ## Session 016 - **日期**:2026-09-17 - **本轮目标**:修「切会话就显示请求已取消」(用户报告的 bug) - **已完成**: - **根因定位**(用户先要我分析、不要改代码): 两处「内容是否为空」的判空**口径不一致**—— `normalizeSessionHistory` 用 `content.trim()`(`` 进度块**算**有内容), `isInterruptedEmptyScopeMessage` 用 `stripScopeBlocks()`(进度块**不算**内容)。 同一段内容一处认为「有」、一处认为「没有」。 新协议的正文要等 `done` 才写入,处理期间内容**只有进度块**, 把这个缝隙从偶发放大成「一切会话就出现」。 - **判定责任方:前端**(用户问「该前端还是后端改」)。三条理由: ①两处判空都在前端;②`` 标记是适配层发明的、后端看不见; ③后端能做的是改成流式(让回答更早出现),那是体验优化、是掩盖不是修复。 - **修法(用户选定「统一判空口径」)**:抽出**唯一**的判空函数 `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.json` 的 `empty-content-agreement`; `harness/tools/verify-empty-content-agreement.mjs`;根 `README.md` 的 20260917 节 - **提交记录**:见下条提交(已按约定提交推送) - **更新过的文件或工件**:`src/utils/interrupted-message.ts`、 `src/components/business-assistant/shared.ts`、 `harness/tools/verify-empty-content-agreement.mjs`(新增,含 esbuild 入口 `_entry-empty-content.ts`)、 根 `README.md`、`harness/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.json` 的 `policy-detail-blocks`(已改为两块); 根 `README.md` 的 20260917 节 - **提交记录**:见下条提交(已按约定提交推送) - **更新过的文件或工件**:`src/components/Chat/PolicyMatch.vue`、根 `README.md`、 `harness/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.市级政策id` 有 **261 / 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 时返回空串——不猜**, 宁可没有链接也不给打不开的地址 - 接入 `normalizePolicy` 的 `apply_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.mjs`; `feature_list.json` 的 `policy-library-back-entry`;根 `README.md` 的 20260917 节 - **提交记录**:见下条提交(已按约定提交推送) - **更新过的文件或工件**:`src/components/Chat/policy-library-utils.ts`、 `src/components/Chat/PolicyMatch.vue`、`harness/tools/verify-policy-library.mjs`、 根 `README.md`、`harness/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"` → ``,否则纯文本) - `.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_id` 是 **40 位、且与该条 `id` 相同**, 与 `市级政策id`(24 位,如 `676ea4ff61aee302a854ab72`)**是两套 id 体系**,拼不出一网通办的地址 - `index.html` 原本是**远端优先**,而远端**带 CORS 头**(实测 `Access-Control-Allow-Origin` 回显 `https://localhost:8083` 与 `https://aixq.shqp.gov.cn`)→ 浏览器里远端确实赢了 → `buildPolicyDetailUrl` 取不到 id → 返回空串 → 「政策名称」纯文本 - 可验证的预测:改之前浏览器里政策事项列表应是 **327 行**(不是 262)——Session 019 问过这个数 - **新增环境变量 `VITE_POLICY_LOCAL_FIRST`**:仅 `.env.development` 为 `true`, `.env.production` / `.env.qingpu` / `.env.test` 显式写 `false`(四份都写,避免占位符不被替换) - `index.html` 改为**按该开关排来源顺序**:dev 本地优先、远端兜底;生产仍远端优先、本地兜底。 错误日志按来源名打印(不再用 `isFallback` 布尔反推) - **运行过的验证**: - `npm run build` 通过 - 生产产物 `dist/index.html`:`var 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.mjs`; `feature_list.json` 的 `policy-library-back-entry`;根 `README.md` 的 20260917 节 - **提交记录**:见下条提交(已按约定提交推送) - **更新过的文件或工件**:`.env.development`、`.env.production`、`.env.qingpu`、`.env.test`、 `index.html`、`harness/tools/check-policy-sources.mjs`(新增)、根 `README.md`、 `harness/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 的开工流程。** 具体漏项: 1. **没跑 `bash harness/init.sh`**(`sops/session-start.md` 第 4 步「跑基础验证」)—— 事后补跑,**exit 0 通过** 2. **没读 `docs/architecture.md`** —— 根 `CLAUDE.md` 写明「第一次进这个仓库必读」, 而本会话是第一次进,我跳过了 3. **一处严重失误**:列 `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.json`、`dms_column_fields.json`、 `create_dms_columns.py`、`fix_field_alias.py`、`RUNBOOK.md`、`SKILL.md`、`apifox-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.md`; `docs/reference/README.md` 的 DMS 索引节 - **提交记录**:**无** —— 本轮改动(`harness/` 下的文档与方案)按 `.gitignore` 约定**不入库**, 故无内容可提交。这是预期行为,不是漏提交 - **更新过的文件或工件**:`docs/reference/DMS_API.md`、`docs/reference/DMS_COLUMNS.md`、 `docs/reference/DMS_MAPPING.md`、`docs/reference/README.md`、 `docs/plans/active/dms-api-replacement.md`(新增)、本文件 - **本轮查证到的两个关键事实**(方案里用到,也值得单独记住): 1. **前端只用了 `EnterpriseInfo` 的 2 个字段**:`name`、`credit_code` (`useEnterpriseAuth.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`](docs/plans/active/dms-api-replacement.md) 的后续落地; 本轮方案与用户拍板记录见本文件末尾「Step-0 结论」。 **用户拍板三条**:①反馈写入 DMS;②会话改名/删除一并切;③访客 id 复用埋点 visitorId (`c_credit_code = 访客_`) ### Step-0 实测结论(DMS content 接口,此前全是「📄 未实测」) 跑法:`node harness/tools/dms-step0-verify.mjs `(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:, state:4}`** | ✅ `state=4`=销毁,返回 200 且行从列表消失。**这条文档里完全没有,是本次试出来的** | | 7 | 长文本(1105 字含 ``/`POLICY_TABLE`/`` 标记)**逐字节往返一致** | ✅ 协议标记可原样存 DMS | | 8 | 中文+下划线的 `c_credit_code` 精确 search 命中 | ✅ 访客 `访客_` 可查 | | 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.mjs`、`dms-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)`; `totalResponse` 补 `answer`;`close`(停止/断流)也补一次(否则中断的问答只有 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` 是共同的)。 `fetchDmsSessions` 的 `c_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_answer`, `c_question` 存首问 - **写入方式改为「以本地 messages 为准整段重写」**,不做读-改-写 → 从根上消除了并发覆盖问题(本地状态就是权威) - `fetchDmsSessionRecords()` 改成从该行 JSON 里**还原问答对** - `writeDmsFeedback()` 改成「读整行 → 只改命中那条消息 → 写回」,与对话写入 **共用 `session:` 顺序链**;定位不到时**不新增行**(避免库里出现只有反馈没有内容的行) - ⚠️ **不存消息级时间戳**:本地消息本就没有时间戳,整段重写时现造 `Date.now()` 会让所有消息的时间都变成最后一次保存的时刻 —— 假数据比没有更糟。顺序由数组顺序表达 - `useBusinessAssistantChat.ts`:4 处调用点(sendMessage / totalResponse / close / submitQuestionAnswers)统一换成 `saveSessionTranscriptToDms(session)` ### ② 去掉开发环境硬编码账号的自动登录 - 根因:`useEnterpriseAuth.ts` 里 `isDevLogin = MODE === 'development'` 时**无条件**用 硬编码的 devLogin token 登录 → 表现为「我没登录也进入登录态」 - 已整段移除该分支;要企业态请显式带 `?access_token=` 或 `?credit_code=`。 开发环境默认即**访客态**,正好方便验证访客记录。 - 影响:dev 下 `globalThis.token` 不再自动注入,依赖它的旧接口(企业信息等) 需要显式登录才可用 —— 本轮涉及的会话/历史/反馈已全部走 DMS(代理注入 token),不受影响 ### ③ harness 结构对齐官方资料 参考: (该站被网络策略挡了,改从 GitHub raw 取到正文与 OpenAI 高级包结构) - **`docs/plans/` → `docs/exec-plans/`**,`done/` → `completed/`, `tech-debt.md` → `tech-debt-tracker.md`(对齐参考资料命名) - **新增硬规则并写进入口**:计划类文件**只能**写在 `harness/docs/exec-plans/active/`。 用户指出的问题属实:本轮的实施计划先前被写到了 Claude Code 自带的 `~/.claude/plans/`(**仓库外**),换个会话接手看不到。已把内容落进仓库 (`exec-plans/active/dms-chat-storage.md`),并在 `CLAUDE.md`、 `exec-plans/README.md`、`sops/session-end.md` 三处写明 - **新增机械约束 `harness/tools/validate-harness.mjs`**:参考资料的「机械约束优先于 口头约定」。校验必备文件、**游离的计划文件**(仓库根/`src/` 下)、 `feature_list.json` 的假 passing / 重复 id / 多个 `in_progress`、 `progress.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.md`; `harness/tools/validate-harness.mjs`;根 `README.md` 的 20260917 节 - **更新过的文件或工件**:`src/network/api/dms/chat-sessions-dms.ts`、 `src/components/business-assistant/useBusinessAssistantChat.ts`、 `src/components/useEnterpriseAuth.ts`、`harness/docs/exec-plans/*`(重构)、 `harness/tools/{validate-harness.mjs,_entry-dms.ts,verify-dms-chat-storage.mjs}`、 `harness/README.md`、`harness/sops/{session-start,session-end}.md`、 `harness/docs/reference/README.md`、`harness/docs/quality.md`、`CLAUDE.md`、根 `README.md` - **已知风险或未解决问题**: - ⚠️ **浏览器端到端仍未人工点过**(沿用 Session 022 的遗留) - ⚠️ **反馈状态刷新后不恢复**:JSON 里存着,但 `BusinessRecord` 的 `localFeedback` 初始恒为 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;`.gitignore`;`harness/tools/README.md`; 根 `README.md` 的 20260917 节 - **更新过的文件或工件**:`.gitignore`、`CLAUDE.md`、`harness/README.md`、 `harness/tools/README.md`、`harness/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. 适配层 `toPolicyTableItem`(`api-chat-coordinator.ts:322-337`)把 `title` 取自 `card.name.text`(**只有政策名**),并把 `declaration_item` 也设成同一个值 —— **每条唯一的 `data.title` 被丢掉了** 3. 于是同一政策的多个申报事项,其 `declaration_item` **完全相同**, 而 `openDetailByItem` 的兜底匹配正是拿它比 → `findIndex` 永远命中第 0 张 - **实证**:用旧逻辑跑同政策两卡片的场景 —— 点第 1 张返回 0、**点第 2 张也返回 0** ✓ 与截图现象吻合 - **修法**:把反查逻辑抽成纯函数 `resolvePolicyDetailIndex()` (`policy-match-utils.ts`),匹配顺序改为: 1. **对象同一性**(卡片区传入的就是 policies 里的那个对象,精确) 2. 字段比对(id / policy_id / declaration_item)**只有唯一命中才采信**; 多命中说明这组数据本就区分不了 → 返回 -1,交给「直接展示被点条目自己」的兜底 —— **宁可走兜底,也不张冠李戴** - `PolicyMatch.vue` 的 `openDetailByItem` 改用它;未改动任何 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.mjs`; `feature_list.json` 的 `policy-detail-index-mismatch` - **更新过的文件或工件**:`src/components/Chat/policy-match-utils.ts`、 `src/components/Chat/PolicyMatch.vue`、`harness/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.ts` 的 `toPolicyTableItem`:`declaration_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.title` 与 `card.name.text` 传成了不同值, 那其实是「有后缀」形态;已修正夹具,**不是代码问题**) ## Session 026 - **日期**:2026-09-17 - **本轮目标**:卡片区「更多」改为打开新侧边栏「政策列表」(用户需求) - **方案**:[`docs/exec-plans/active/policy-card-list-sidebar.md`](docs/exec-plans/active/policy-card-list-sidebar.md) - **用户拍板三条**:①内容 = **本会话全部卡片**(跨消息聚合、去重)②**只换「更多」入口** —— 详情面板的「返回政策事项列表」保留旧列表③热门搜索沿用现有 5 词 ### 已完成 - **新增第 3 种面板模式 `cardList`**(`panelMode: "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)、 `buildPolicyCardDedupKey`、`dedupePolicyItems`(首见优先)、 `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.mjs`;`feature_list.json` 的 `policy-card-list-sidebar` ### 更新过的文件或工件 `src/components/Chat/policy-match-utils.ts`、`src/components/Chat/PolicyMatch.vue`、 `src/components/business-assistant/useBusinessAssistantChat.ts`、 `harness/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_id`,`c_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-storage` 与 `policy-card-list-sidebar` 早已实现 - 已归档两个到 `completed/`,并**更新其状态头**为实际结果(含「浏览器人工点验未做」的显式标注) - `dms-api-replacement.md` 保留在 active,但**加了「落地进度」表**: 1887/1889 相关的 3 读 3 写已实现,**企业信息(1888)未做**——如实标出,不假装全做完 - **给 `validate-harness.mjs` 加了两条检查**(机械约束,不靠人记): 1. `active/` 里的计划若状态写着「✅/已完成」→ 提醒该归档 2. `completed/` 里的计划若状态仍写「🔄/进行中/待拍板」→ 提醒状态与位置不一致 - **实测会响**:把 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.ts`、 `src/components/business-assistant/useBusinessAssistantChat.ts`、 `harness/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_id`, `c_question` 与 `c_answer` 均已写入) - 这组数据同时证实三件事:**一会话一行 + 一问一答一条**的粒度正确、 **访客记录确实落了库**(`访客_` 前缀)、**开发环境确实处于访客态**(没有自动登录) - 更新条目证据(去掉「未做」标注,换成上述实测留痕): `dms-chat-storage`、`policy-card-list-sidebar`、`remove-dev-auto-login` - 两个已归档计划的状态头改为「已完成**并人工验证**」 - **运行过的验证**: - 直接查 DMS 两个栏目,读到用户测试留下的真实行(见上),与代码约定逐条对上 - `node harness/tools/validate-harness.mjs` 通过 - **已记录证据**:本文件 Session 028;`feature_list.json` 三条的 evidence/verification; `completed/` 下两个计划的状态头 - **更新过的文件或工件**:`harness/feature_list.json`、 `harness/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 - **日期**:2026-09-17 - **本轮目标**:修「生成中在另一个会话提交 → 原会话被取消、新会话也出问题」, 按用户拍板改为**多会话并行生成**(ChatGPT 桌面模式) - **方案**:[`docs/exec-plans/active/multi-session-parallel-generation.md`](docs/exec-plans/active/multi-session-parallel-generation.md) ### 根因(两层,均已实证) 1. **按钮语义是全局的**:`InputShell` 的 `isGenerating ? emit('stop') : emit('send')` 用的是全局 `isGenerating` → 切到别的会话后按钮仍是「停止」,用户想发送却触发了停止 2. **停止打错对象**:`handleStopGenerate` 先 `smc.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())`**; - **每轮新建 ApiChatCoordinator 实例**(生命周期 = 任务,mitt 随实例销毁) - 每个任务**自己的 chunkBuffer**、监听器**闭包捕获 sessionId**(不再读「当前会话」) - 新增 `finishTask` / `stopTask` / `stopAllTasks`;补问数据按会话存 `sessionInterruptPayloads` - 删除 `smc` / `messageChunkBuffer` / `activeGenerationSessionId` / `dmsAnswerWrittenFor` / `getActiveGenerationSession` / `finishActiveAiMessage` / `buildAppendHistoryPayload` / `setupCoordinator` - `isGenerating` 改 `computed(() => tasks.size > 0)`;新增 `currentSessionGenerating` 给按钮用 - **`handleStopGenerate`** 改为 `resolveStopTargetTaskId` → `stopTask` —— **旧 bug 自然消失** - **`startNewChat` 不再打断生成**(后台任务继续,与主流产品一致) - **新增 toast 通道**:`toastMessage` + `showToast`;PC/Mobile 各挂 ``; `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.ts`、 `src/components/BusinessAssistantPC.vue`、`src/components/BusinessAssistantMobile.vue`、 `harness/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') === 原始对象` → **false**;`shallowRef` → true。 - 而我在**所有监听器**里写的守卫是 `tasks.value.get(sessionId) !== task` → **永远成立** → 监听器全部提前返回 - 后果:mock 流的 `finishTask` 从不执行 → 任务永驻 → `isGenerating` 恒真 → **按钮卡在"停止"态**(用户看到的);**真实对话的内容追加也被同一个守卫挡掉** (`message` 监听器同样提前返回)—— 这是比用户报告更严重的一面,同轮一并修掉 - **修法**: - `tasks` 改 **`shallowRef(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.ts`、 `src/components/business-assistant/useBusinessAssistantChat.ts`、 `harness/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-generation` → **passing**(用户确认:mock 测试按钮跑完恢复、 真实对话内容正常流式输出);`verification` 里那条「❌ 未做浏览器验证」已替换为实测确认 - 计划 `multi-session-parallel-generation.md` → 从 `active/` **归档到 `completed/`**, 状态头改为「已完成并人工验证」,并注明过程中踩过的响应式陷阱(Session 030) - **运行过的验证**:`validate-harness` 通过(19 项功能,**in_progress 0 项**) - **已记录证据**:本文件 Session 031;`completed/multi-session-parallel-generation.md`; `feature_list.json` 的 evidence - **更新过的文件或工件**:`harness/feature_list.json`、`harness/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.test`、 `vite.config.ts`、`harness/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_dms`、`logs`、`uploads`); `../..` = `/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/yszs` 是 **DMS 服务器上新建的目录,目前没有数据**; 它**不是 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.test`、 `harness/docs/reference/DMS_API.md`、`harness/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.md`、`DMS_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.test`、 `harness/docs/architecture.md`、`harness/docs/reference/DMS_API.md`、根 `README.md`、本文件 - **已知风险或未解决问题**: - ⚠️ 回退掉的东西**仍在 git 历史里**(提交 `0caf1ae`、`7ce6ed3`)—— 包括那次探测得到的「DMS 有 `POST /file/getFiles`、`path` 是相对 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.test` 的 `VITE_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=true`** (`summary` 摘要 / `qpyszx` 是否青浦营商咨询 / `qyzt` 是否识别到企业主体 / `rzqpyx` 是否落户青浦意向)。 **不带它们写入会被 `214 数据错误` 拒绝**(实测:不带 214、带上 200) 2. **DMS 的系统字段 `title` 只在创建那一刻初始化**:改 `c_title` 后 `title` **不跟随** (实测:改完 `c_title=标题B`、`title` 仍是 `标题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.ts`、 `harness/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.ts` 的 `buildAnalyticsFields` 去掉那三个字段,**只保留 `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.ts`、 `harness/tools/verify-dms-chat-storage.mjs`、根 `README.md`、本文件 - **已知风险或未解决问题**: - ⚠️ **需要新的 DMS token** 才能继续验证与联调(放 `.env.development.local` 的 `DMS_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.ts`、 `harness/tools/{verify-dms-payload-fields.mjs(新增),verify-dms-chat-storage.mjs}`、 根 `README.md`、`harness/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:""` + 三个 `false` → **200 成功** ✓ (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.ts`、 `harness/tools/{verify-dms-chat-storage.mjs,verify-dms-payload-fields.mjs}`、 根 `README.md`、`harness/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.ts`、 `harness/tools/verify-dms-payload-fields.mjs`、根 `README.md`、 `harness/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 的 `访客_` 归属在用它**,已搬进 `src/network/api/dms/chat-sessions-dms.ts`; **localStorage 的 key 字符串保持不变**(`assistant_statistics_visitor_id`), 改了会让已存在的访客换新 id、同一浏览器在 DMS 里变成两个访客 - 副作用:**点赞从「双写」变成只写 DMS** 2. **死代码清理**(用户确认直接删): - `src/network/api/card/index.js` **整文件删除**(三个函数都无调用方), 连同 `useBusinessAssistantChat.ts` 里那个未使用的 `import cardAPI` - `assistant-statistics.ts`(同上) - ⚠️ **`PolicyListUpdatedMatch.vue` 有意保留**:它虽对新协议无效, 但**老会话的历史内容里可能还有 `` 块**,删了那些卡片列表渲染不出来 - **运行过的验证**: - `npm run build` 通过;`vue-tsc --noEmit` 本次涉及文件无报错 - `verify-dms-payload-fields.mjs` **12/12**;`verify-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.ts`、`src/components/BusinessAssistant.vue`、 `src/components/Chat/BusinessRecord.vue`、 `src/components/business-assistant/useBusinessAssistantChat.ts`、`.env.qingpu`、 `docs/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-screen`, **exp 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.ts` 的 `fetchToken` / `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:347` 与 `BusinessAssistantMobile.vue:405` 都 `import 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 条(厂商依赖 + 硬编码凭据) - **下一步最佳动作**:无(本轮为记录决定)