# 进度日志
> 通用的仓库内会话进度日志。文件名沿用课程历史约定,不绑定 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` |
## 会话记录
## 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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` 开始实现
### 还没写进 feature_list 的待办
- 适配层映射了新字段 `knowledge_type` 的类型但未渲染,用户未要求展示
- 惠企政策标识目前只在卡片右上角,用户明确要求不同步到「更多」列表与详情面板