# zhaoshang-llm 改动记录 > 本文件当前只记录**按日期的改动**。项目概述、技术栈、目录结构、接口说明等内容, > 等项目结束时再统一总结。 ## 20260914 - AI 对话迁移到接口 `POST /api/chat`(SSE) - 新增协议适配层 `src/components/api-chat-coordinator.ts`,把 SSE 事件翻译成既有渲染组件 可识别的内容标记,渲染层无需感知新协议 - 请求体严格只发 `{thread_id, question}`;`answer` 与 `result.response` 只展示一次; 未收到 `done`/`error` 即断流时按连接中断处理,不自动重试 - 旧协议实现 `src/components/stream-message-coordinator.ts` 保留未用 - 新增环境变量 `VITE_CHAT_API`,新增 dev 代理 `/chat-api` → `http://192.168.2.23:8000` - 政策卡片、政策详情、参考资料、公司补问与候选确认接入新协议 - 错误码翻译:把 HTTP 与 SSE 错误码转成中文提示(如 `thread_busy` → 该问题还在处理中) ## 20260915 - 修复回答内容顺序:正文 → 政策卡片 → 综合说明 → **参考资料**(参考资料移到最下方) - 修复换行丢失:块级 markdown 下单个换行会被折叠,导致列表后紧跟的说明行被并进上一条列表项 - 修复复制功能:复制结果剔除协议标记,不再把 ``、``、 `` 等原始标记一起复制出来 - 修复卡片顺序与「更多」列表不一致:卡片改为按接口 `priority.rank` 排序 - 「更多」面板的列表改为复用卡片数据,不再拉取与卡片无关的公共政策库 - 部门筛选还原为最初的 15 个固定部门与字面匹配(此前一度改为按接口数据动态生成) - 新增惠企政策库来源标识(来自惠企政策库的卡片在右上角打标) - 政策详情面板新增「条件核验 / 匹配依据 / 原文引用」三个区块 - 建立本机开发工作流文件与接口参考目录(按 `.gitignore` 约定不入库) ## 20260916 - 打字机速度加快:每次打出字数 2 → 4(61 → **121 字符/秒**),积压加速档位同步上调。 基准取主流 AI 的实测输出速度(Claude 3.5 约 60 字符/秒、DeepSeek-R1 约 75, 主流区间 50~120),取区间上沿——因为本项目后端一次返回整段,打字机是纯额外延迟, 同样速度会比主流慢一整段 - 「政策事项列表」统一为**一个列表、一个入口**: - 卡片区「更多 >>」与详情的「返回政策事项列表」**走同一条路径、内容相同** - 列表数据统一来自 `public/merchant-agent/policies_public.v1.json` (262 条申报事项 / 38 个政策),**本轮匹配到的惠企政策置顶** - 「政策事项列表」**每次进入都重置筛选**(部门 / 分类 / 关键词 / 页码), 避免上次的筛选残留导致「条目少」或「筛不出东西」 - 政策详情面板的「返回政策事项列表」按惠企政策区分:**只有惠企政策才显示该入口**; 惠企政策判定优先用接口字段 `is_policy_library`,缺失时回退到按标题在库中匹配 (已做归一化,容忍《》全半角括号等差异) - 公司补问改为按 `status` 字段判定形态(`found` / `not_found` / `failed` / 无 `status`), 替掉原先靠"候选数是否为 0"和 `input_help` 文案正则的猜测式判断 - `failed`(查询服务失败)此前会落进 `not_found` 分支,文案被填成"未查询到匹配的公司", 等于把失败说成查无公司,已改为独立分支并说明失败原因 - 无 `status`(初次询问是否需要公司信息)此前给"跳过公司查询",改为"不需要公司信息" - 补问卡片:输入框移到选项上方并常驻(原来在选项之后、需点击才展开) - 补问卡片:修复选项序号错乱——输入框在最上却拿 B、选项在下面却拿 A,现输入框取 A、选项顺延 - 补问卡片:正文里已展示过的同一段文字不再在卡片内重复 - 「跳过公司查询」由发送 `/skip` 改为发送选项文本「跳过公司查询」 - 问卷提交:修复只取第一题(多题丢失)、多选只取第一项、以及数字开头选项被静默改写成纯数字 ## 20260917 - 修复切会话时误报「请求已取消」: - 根因是**两处「内容是否为空」的判空口径不一致**——归一化用 `content.trim()` (`` 进度块算作有内容),渲染层用 `stripScopeBlocks()` (进度块不算内容)。同一段内容,一处认为「有」、一处认为「没有」 - 新协议的正文要等 `done` 才写入,处理期间内容**只有进度块**,把这个缝隙 从偶发放大成「一切会话就出现」:归一化认为有内容 → 不替换文案,却把状态翻成 Finish;渲染层认为没内容 → 显示「请求已取消」,而请求其实还在跑 - 修法:抽出**唯一**的判空口径 `hasVisibleMessageContent()`(`utils/interrupted-message.ts`), 归一化与渲染层都改用它 - 新增回归脚本 `harness/tools/verify-empty-content-agreement.mjs`, 断言的不变量就是「两处必须一致」,30 项断言 - 删除政策详情面板的「原文引用」板块(条件核验、匹配依据保留) - 思考中卡片的阶段文案一律不换行: - 原因:`ScopeContent` 按卡片宽度切样式 —— 没撑满容器时标题 `nowrap` + 省略号, 撑满时切成 `pre-wrap`。而阶段文案长度不一(新协议的文案比原来的"正在思考中..."长得多), 切换时表现为「突然换行」且卡片高度跳动 - 改法:拆开原本同时管标题与正文的规则 —— **正文保持可换行**, 标题恒定 `nowrap` + 省略号 - 政策详情面板的「政策名称」支持点击跳转:由库条目的 `data.市级政策id` 拼出 `https://zwdt.sh.gov.cn/qykj/shell_oc_policy_zq/policy/policy-detail?id=` - 背景:库里 `apply_link` 字段 262 条**全为空**;`资源申请备注` 里的 URL 是按申报事项 零散对应、且混有内网地址(25 条指向 `http://10.235.238.34:7202/imanage/login`), 不能作为来源。`市级政策id` 有 261/262 条,可靠 - 没有 id 时不给链接(**不猜**),宁可没有也不指向打不开的地址 - 查明「政策名称」显示为纯文本的真因,并调整政策库数据源(**开发环境改用本地 json**): - 政策库其实有**两个来源**,字段并不一致: - 本地 `public/merchant-agent/policies_public.v1.json`:262 条,`data.市级政策id` 有 **261** 条 - 远端 `VITE_DOWNLOAD_URL`:327 条,该字段**一条都没有**(它有的 `policy_id` 是 40 位、 与条目 `id` 相同,与 24 位的 市级政策id 是**两套 id 体系**) - 而 `index.html` 原本是**远端优先**(远端带 CORS 头,浏览器里确实会赢), 于是取不到 id → 拼不出详情地址 → 「政策名称」是纯文本。 此前怀疑的「HMR 状态陈旧」「走了兜底路径」均**排除** - 新增环境变量 `VITE_POLICY_LOCAL_FIRST`:**仅开发环境**打开(本地优先、远端兜底), **生产仍远端优先**——生产该问题属后端数据缺失,需远端那份补上 `data.市级政策id`, 前端无法修 - 新增 `harness/tools/check-policy-sources.mjs`:对比两个来源的条数与字段覆盖率, 并在「远端也带上 id 了」时提醒可以移除该开关 - 代价:开发环境用的这份比远端少 65 条申报事项(262 vs 327) - 会话、问答记录与反馈**落地到 DMS 数据管理服务**(栏目 1887 助手会话 / 1889 助手问答记录) - 提交问题时写入会话(按 `c_session_id` upsert:没有就新增、有就更新)与问答记录(先写问题); 回答完成补写回答;停止/断流也补写已生成的内容,避免中断的问答只有问题没有回答 - 会话列表、历史记录改为**从 DMS 读取**;会话改名、删除同步到 DMS - 点赞/点踩/取消写入 DMS 的问答记录(按 `c_record_id` 定位那一行) - **未登录的访客也写入**,以 `访客_<访客id>` 归属(访客 id 复用埋点的那个) - 会话标识用前端的 `session.id`(它恒等于聊天协议的 `thread_id`);问答记录标识用 AI 消息 id(同时是反馈组件用的 `record.id`,因此从 DMS 读回的历史消息点赞仍能命中原始行) - **token 只存在于 vite 代理侧**(`.env.development.local`,按约定不入库), 浏览器产物里没有 token;DMS 不可用时静默降级,聊天与本地历史不受影响 - 实测踩坑:DMS 的「删除」不是 `delContentById`(各种传法都返回参数错误), 而是 `POST /content/updateAudit` 把状态改成 `4`(销毁);`addContent` 会返回记录 uuid - **存储粒度:一个会话一行**(不是一问一答一行)——整段对话的消息数组以 JSON 存进 该行,每次以本地消息列表为准整段重写;反馈记在 JSON 里对应那条消息上 - 去掉**开发环境硬编码账号的自动登录**:此前开发环境下会无条件用内置账号登录, 表现为「没有登录也进入登录态」。现在默认即访客态,要企业态需显式带 `?access_token=` 或 `?credit_code=` - 本机工作流文件(`harness/`)结构对齐 [Learn Harness Engineering 的 OpenAI 高级包](https://walkinglabs.github.io/learn-harness-engineering/zh/resources/): `docs/plans/` → `docs/exec-plans/`(`completed/` + `tech-debt-tracker.md`), 并新增结构校验脚本 `harness/tools/validate-harness.mjs`(机械约束优先于口头约定: 可检出假 passing、多个 in_progress、游离的计划文件等) - **`harness/`、`CLAUDE.md`、`agents.md` 纳入版本控制**(此前排除) - 依据:参考资料的原则是「计划、质量、技术债**和代码一起版本化**」—— 团队 clone 应当能看到状态、计划与契约文档,而不是只在本机 - 仍然排除的是带凭据的本机文件(`.env.*.local`、`.claude/settings.local.json`), 以及 esbuild 生成的 `harness/tools/_*.mjs`(构建产物,入库会与源码漂移); 产物重新生成的命令写在 `harness/tools/README.md` - 入库前做了凭据扫描(无真实 token),并**模拟 fresh clone**(删光产物 → 按文档重新生成 → 四个验证脚本全绿)确认别人拿到仓库能跑通 - 修复「同一政策的多个申报事项,点『查看详情』内容一样」: - 根因:同一条政策下的多个申报事项会各出一张卡片,但适配层把卡片的 `title` 与 `declaration_item` 都取自 `card.name.text`(**只有政策名**),而后端不下发 `id`/`policy_id`(实测恒为 undefined)——这些卡片在字段上完全无法区分, 原来的反查只要任一条件命中就返回,于是点第二张也跳到第一张 - 修法:反查逻辑抽成纯函数 `resolvePolicyDetailIndex()`,**对象同一性优先**, 退化的字段比对**只在唯一命中时采信**,多命中时交给「直接展示被点条目」的兜底 —— 宁可走兜底,也不张冠李戴 - 新增回归脚本 `harness/tools/verify-policy-detail-index.mjs`(10 项断言, 含「点第 2 张 → 下标 1」这条针对本 bug 的断言) - 详情面板的标题改为显示**具体申报事项**(如「…的通知 - 创业开办费补贴」): 适配层的 `declaration_item` 改用后端每条唯一的 `data.title`,不再与卡片标题同值; **卡片区标题仍是政策名,观感不变**(含一处连带的 `panelData.title` 取值顺序修正—— 否则改详情标题会连带改掉卡片标题) - 新增 `harness/tools/verify-policy-card-fields.mjs`(8 项断言:同政策两条必须可区分、 卡片标题不变、无后缀时退化为政策名) - 卡片区「更多 >>」改为打开新的**「政策列表」侧边栏**: - 标题「政策列表」,保留搜索框与热门搜索,**移除部门/分类筛选** - 内容为**本会话内所有消息出现过的政策卡片**(跨消息聚合、去重、按匹配度排序)—— 此前会话级聚合并不存在(每条消息的卡片各自渲染),本轮新增 - 详情面板的「返回政策事项列表」**仍打开原来的惠企政策库列表**(两套列表并存, 按要求只换「更多」入口) - 面板新增第三种模式 `cardList`,与旧列表**共用同一套模板与样式类,未新增 CSS**; 旧列表逻辑靠既有守卫自然短路,`openPolicyList` 一行未动 - 新增 `harness/tools/verify-policy-card-list.mjs`(31 项断言:聚合并集保序、 坏 JSON/空输入不抛错、首见优先去重、降序+同分稳定、六面关键词过滤) - DMS 的问答记录存储粒度**改回「一问一答一条」**(幂等键 `c_record_id`, 问题与回答各存一半)。曾一度改成「一个会话一行、整段对话 JSON 塞进 `c_answer`」, 现按需求改回——更贴近库表原本语义(`chat_record` 即一行一轮问答),也便于按轮次检索统计。 反馈随之改为写在该轮的 `c_feedback_*` 列上;会话删除连带清理其名下所有问答行 - 计划归档:`harness/docs/exec-plans/completed/` 开始启用, 已实现的两个计划(DMS 落地、政策列表侧边栏)移入并更新状态; 结构校验脚本新增两条检查(`active/` 里状态写着已完成、`completed/` 里状态仍写进行中) - **按会话并行生成**(改为 ChatGPT 桌面那种模型): - 此前全局只有一个协调器实例 + 一个全局 `isGenerating`,于是「同一时间只能一个会话在生成」, 并衍生出两个 bug:生成中切到别的会话,发送按钮仍显示「停止」(想发送却停了生成); 停止时目标会话解析错误(同步事件把它提前清空 → 回落到「当前会话」), 会把停止打在看的那条会话上、并凭空造出一条空的「请求已取消」消息 - 现在生成状态按会话隔离(`tasks` Map + 每轮独立协调器实例 + 监听器闭包捕获会话 id), 各会话并行、互不干扰;停止只停自己那一轮;新建会话不再打断正在生成的会话 - 顺带修好了两处静默失败:后端 409 等提示此前**无人监听**(现接到全局 Toast)、 同会话重复发送此前静默无反应(现提示「该会话已有回答在生成中」) - 新增回归脚本:`verify-chat-task-utils.mjs`(33 项,含把「响应式容器破坏对象同一性」 这个陷阱钉死的断言)、`verify-coordinator-multi-instance.mjs` (11 项,验证多实例事件隔离——并行化的地基) - 删除统计埋点的老接口(用户要求): - 删除调用点(`BusinessAssistant.vue` 的页面浏览、`BusinessRecord.vue` 的反馈统计) 与整个 `src/network/api/assistant-statistics.ts`,以及已无人使用的 `VITE_ASSISTANT_STATISTICS_BASE_URL` - **点赞由「双写」变为只写 DMS**;运营侧的埋点数据从此不再上报 - 只保留 `getOrCreateAssistantVisitorId`(DMS 的 `访客_` 归属在用),搬进 DMS 模块, **localStorage key 保持不变**(改了会让已存在的访客换新 id) - 清理死代码(用户确认直接删除,非注释): - `src/network/api/card/index.js` 整文件删除(三个函数均无调用方) 及其未使用的 import - `PolicyListUpdatedMatch.vue` **有意保留**——老会话历史里可能仍有 `` 块,删了那些卡片列表会渲染不出来 - DMS 写入范围收窄为**只写问答记录**: - **1887 助手会话不再写入**(用户指示「只传具体的会话记录,另一个接口不传了」)—— 新建会话、改名都不再同步;会话列表退化为纯本地(与接 DMS 之前一致) - 1889 问答记录照常写入(含 `title` = 问题文本);删除会话仍清理其名下的 1889 记录 - 调用点按约定**注释保留**并标 `[已停用]`,同时在 `verify-dms-payload-fields.mjs` 里加了**源码级断言**(不得出现未注释的 `upsertDmsSession` 调用),防止被顺手改回 - DMS 写入补齐 `title` 与运营分析字段: - 会话(1887):每次 upsert 都显式带 `title` = 会话标题 —— DMS 的系统字段 `title` 原本只在创建时初始化,**改 `c_title` 不会带动它**,所以前端改名后 DMS 里看到的标题不变 - 问答记录(1889):`title` 用**问题文本**(原先库里是字符串 "null", 在 DMS 列表里看不出是哪一轮) - 写入带 `title`(用户需求);1887 另有四个 `must=true` 的运营字段 (`summary` / `qpyszx` / `qyzt` / `rzqpyx`)——**不带就 `214 数据错误`,四个必须全带**, 但**只传空值/默认值**(取值属业务口径,前端不猜;用户拍板「先传空值」)。 ⚠️ **只在新增时带、更新时一律不带**——否则会把后端将来回填的值冲成空 - 新增 `harness/tools/verify-dms-payload-fields.mjs`(9 项断言):**打桩 fetch 抓真实请求体**, 断言字段集合「不多不少」且 `title` 的值正确 —— 这条**不需要 DMS token** 就能跑 - 环境变量整理:**地址类配置收进 env,不再写死在代码里** - 新增 `VITE_CHAT_TARGET`(聊天后端真实地址)——此前它硬编码在 `vite.config.ts` 的 代理配置里;现由 `vite.config.ts` 从 env 读取(保留同值兜底)