通用的仓库内会话进度日志。文件名沿用课程历史约定,不绑定 Claude Code—— Codex、OpenHands 等 agent 同样可用,前提是仓库指令要求它在开工时读取、收尾时更新。 agent 不会自动维护这个文件。
f:\yysk\AI_zhaoshang\zhaoshang-llmnpm run dev → https://localhost:8083(dev server 为 HTTPS,带自签证书告警属正常)npm run build(生产构建,必过);可选 npm run test(jest)file-upload(文件与图片上传)/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 |
item 事件里:id / policy_id 恒为 undefined;每条唯一的信息在
data.title(形如「政策名 - 申报事项」,如 …通知 - 创业开办费补贴)与 data.source_idtoPolicyTableItem(api-chat-coordinator.ts:322-337)把 title 取自
card.name.text(只有政策名),并把 declaration_item 也设成同一个值 ——
每条唯一的 data.title 被丢掉了declaration_item 完全相同,
而 openDetailByItem 的兜底匹配正是拿它比 → findIndex 永远命中第 0 张resolvePolicyDetailIndex()
(policy-match-utils.ts),匹配顺序改为:
PolicyMatch.vue 的 openDetailByItem 改用它;未改动任何 UI 文案或样式npm run build 通过harness/tools/verify-policy-detail-index.mjs —— 10/10 通过,
关键断言「点第 2 张 → 下标 1(修复前这里会返回 0)」与「字段无法区分时返回 -1」harness/tools/probe-policy-cards.mjs(打 item 原始事件,
确认 id/policy_id 为 undefined、card.name.text 两条相同)harness/tools/verify-policy-detail-index.mjs;
feature_list.json 的 policy-detail-index-mismatchsrc/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 变更,需用户拍板(已在回复里提出)用户选择「详情标题显示具体申报事项」。改动:
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 传成了不同值,
那其实是「有后缀」形态;已修正夹具,不是代码问题)
harness/ 排除在 git 之外).gitignore 去掉 /harness、/CLAUDE.md、/agents.md 三条排除,
改为入库;并在原位置留了说明注释(为什么改、何时改).env.*.local、.claude/settings.local.jsonharness/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)CLAUDE.md 的提交约定、
harness/README.md 的关系表legacy/API.md 里两处带 ... 的截断示例,无真实凭据git check-ignore 逐项核对:.env.development.local(含 DMS token)已排除 ✓vite.config.ts、.env.* 属同一类,不是新增暴露面harness/tools/_*.mjs → 按 README 命令重新生成
三个产物 → 四个验证脚本全绿verify-empty-content-agreement.mjs 30/30verify-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 通过.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、本文件progress.md),是入库必然的progress.md 会持续变大(已 1000+ 行),后续可考虑按会话分文件或定期归档,
但那是另一个改动,本轮不做用户原话:「每次一问一答就会在 DMS 存一条消息,我希望一条消息同时包含同一个会话窗口的所有消息」。 用户三个决策:一行一个会话(1889);反馈存进该行 JSON 里对应那条消息;dev 自动登录去掉。
chat-sessions-dms.ts:
saveDmsTranscript() 取代 upsertDmsRecord() —— 幂等键从 c_record_id 改为
c_session_id,整段消息序列化成 {version:1, messages:[…]} 存进 c_answer,
c_question 存首问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=。
开发环境默认即访客态,正好方便验证访客记录。globalThis.token 不再自动注入,依赖它的旧接口(企业信息等)
需要显式登录才可用 —— 本轮涉及的会话/历史/反馈已全部走 DMS(代理注入 token),不受影响参考:https://walkinglabs.github.io/learn-harness-engineering/zh/resources/ (该站被网络策略挡了,改从 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)已记录证据:本文件 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
已知风险或未解决问题:
BusinessRecord 的 localFeedback
初始恒为 None,没做「从 DMS 读回反馈态」的回填。要做得另接一条线.gitignore 有意排除,Session 009/013 用户确认过)。
参考资料的原则是「计划、质量、技术债和代码一起版本化」,本仓库与之有张力:
团队 clone 看不到 harness。若哪天想改,是一行 .gitignore 的事 —— 但那是用户的决定,没动下一步最佳动作:浏览器实测(发消息 → DMS 一行 → 刷新 → 列表/历史 → 点赞 → 改名/删除); 另需用户提供新 token(旧的今晚过期)
docs/plans/active/dms-api-replacement.md 的后续落地;
本轮方案与用户拍板记录见本文件末尾「Step-0 结论」。
用户拍板三条:①反馈写入 DMS;②会话改名/删除一并切;③访客 id 复用埋点 visitorId
(c_credit_code = 访客_<assistant_statistics_visitor_id>)跑法:node harness/tools/dms-step0-verify.mjs <TOKEN>(14/17 通过)+ dms-delete-probe.mjs。
三个 FAIL 里一个是我判据写错,两个是真发现:
| # | 结论 | 影响 |
|---|---|---|
| 1 | addContent 不需要 c_id(自动填 0) |
✅ 最大阻塞风险解除(模型里 c_id 是 must=true,实测不强校验) |
| 2 | addContent 返回记录 uuid,形态是纯字符串:{"code":200,"content":"056dbae8-…"} |
✅ 新增行后不必再反查一次;不是 content.id 对象 |
| 3 | c_created_at 不自动填,必须自己写 |
⚠️ 写入时要带时间戳 |
| 4 | 时间戳格式:"YYYY-MM-DD HH:mm:ss" 与 ISO 串都接受;读回是 epoch 毫秒(数字) |
写入用前者;读取按毫秒处理 |
| 5 | 删除不是 delContentById(各种传法都 code=-1/POST 405) |
⚠️ 见 #6 |
| 6 | 删除的正确姿势:POST /content/updateAudit,form {columnId, id:<uuid>, state:4} |
✅ state=4=销毁,返回 200 且行从列表消失。这条文档里完全没有,是本次试出来的 |
| 7 | 长文本(1105 字含 <scope>/POLICY_TABLE/<question-cards> 标记)逐字节往返一致 |
✅ 协议标记可原样存 DMS |
| 8 | 中文+下划线的 c_credit_code 精确 search 命中 |
✅ 访客 访客_<uuid> 可查 |
| 9 | 反馈字段(c_feedback_status/option/remark/at)经 updateContent 写入生效 |
✅ 反馈方案可行 |
| 10 | pageSize=200 被接受;写后不带 states 也能查回 |
— |
⚠️ 测试数据已全部清理(1887/1889 复查均 0 行)。清理过程中发现探测脚本把 1887 的 columnId 用到了 1889 的行上,导致一度残留 1 行,已手工补删。
记录:本文件 Session 022;脚本 harness/tools/dms-step0-verify.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_idsrc/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_TOKENnpm run build 通过(改动后重跑)harness/tools/verify-dms-chat-storage.mjs,打的是真实 DMS,
经 vite 代理、与浏览器同一条路径):会话增/改不重复、问答 question→answer 补写不重复、
长文本含协议标记往返一致、反馈三种状态、删除连带清理、时间戳解析、访客归属https://localhost:8084/dms-api/... 查询 → 202(token 已注入);
直连 DMS 不带 token → 208 无token → 证明 token 确实只在代理侧202 空(测试数据已清理干净)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 验证,但「发一条消息 → 库里出现两行」这一步
仍需在浏览器点一遍确认。.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)。fetchRemoteSessions 对空 credit_code 直接返回 [](保持原行为),
符合用户「访客也存」的要求;若将来要「访客也能看自己的历史」,需要另开一条读路径。浏览器实测:发一条消息 → 查 DMS 两栏目应各出现一行(c_session_id=localStorage
ba_current_session_id);刷新后列表出现该会话;改名/删除/点赞各验一次。
bash harness/init.sh(sops/session-start.md 第 4 步「跑基础验证」)——
事后补跑,exit 0 通过docs/architecture.md —— 根 CLAUDE.md 写明「第一次进这个仓库必读」,
而本会话是第一次进,我跳过了harness/docs/reference/ 时,ls 的输出里没有 DMS 三份文档,
我据此说「缺失」,随后 find 证明它们存在(mtime 9/16 17:17,不是新拷入的)。
原因不明(疑似输出被截断),但教训明确:不要用单次 ls 的输出当「文件不存在」的证据,
要用 find / test -e 复核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(该目录此前一直空着):selectContentList 语义吻合delContentById 参数未文档化)code=202),数据迁移(F4-*)没做完不能切bash harness/init.sh 通过(exit 0)—— 事后补跑的开工验证grep 确认三份 DMS 文档已无残留的 ../artifacts / ../tools / (RUNBOOK.md) 相对引用test -e 确认存在(含带空格的 2. DMS-Manage 路径)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(新增)、本文件EnterpriseInfo 的 2 个字段:name、credit_code
(useEnterpriseAuth.ts:58-59;另外 19 个字段在 src/ 里零引用)
→ 用 DMS 的企业信息替换不需要字段映射层,比预想的简单c_feedback_status / c_feedback_option / c_feedback_remark / c_feedback_at
→ 前端 POST /chat/feedback 理论上可由 1889 承担feature_list.json 也未新增条目
(避免在没有决定前把未定的事写成待办)selectContentList 等)在文档里标的是 📄未实测,
本项目从未调用过 —— 方案第 0 步就是先小样本验证ls 漏文件那件事没有查到根因。若再遇到「列目录看不到文件」,先 test -e 复核再下结论feature_list.json 条目逐项做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 → 返回空串 → 「政策名称」纯文本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" → 生产行为未变"true" === "true"
(Vite 检测到 .env 变更会自行重启,故 8083 也已生效,无需手动重启)/merchant-agent/policies_public.v1.json 与仓库本地文件
sha256 一致(1a5127c0…),即 262 条、261 条带 idharness/tools/verify-policy-library.mjs 21/21 通过harness/tools/check-policy-sources.mjs:对比两个来源,输出
本地 262 / 261 带 id、远端 327 / 0 带 id,退出码 0(与预期一致)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、本文件data.市级政策id(或让远端也带 id)。
开关的移除条件已写进 check-policy-sources.mjs 的输出——
远端哪天有 id 了,脚本会提醒可以去掉这个开关ScopeContent 按卡片宽度切样式(isMaxWidthReached = cardWidth >= parentWidth - ε):
没撑满容器时标题 nowrap + 省略号,撑满时整块切成 pre-wrap。
原协议标题是固定的短句"正在思考中...",永不撑满、从不翻转;
新协议我把整条阶段文案放进了标题(长得多),撑满即翻 → 突然换行、卡片高度跳动。
改法:把原本同时管标题与正文的那条规则拆开 —— 正文保持 pre-wrap(它是为多行设计的),
标题恒定 nowrap + 省略号。用户选的就是「一律不换行」。buildPolicyDetailUrl,不是没编译进去apply_link 的编译结果正确:item.apply_link || item['政策原文地址'] || item.data['政策原文地址'] || buildPolicyDetailUrl(item) || ""v-if="selectedPolicyItem?.apply_link" → <a>,否则纯文本).policy-link 样式正确(紫色 + 下划线 + 手型)pointer-events 阻挡buildPolicyDetailUrl 对真实库数据覆盖 261/262(脚本断言过)
剩下的两个可能:
① 页面/内存状态陈旧 —— HMR 更新了组件代码,但已构建好的 policyListItems
(旧 apply_link 为空)仍在内存里;硬刷新或关掉列表重新进才会用新逻辑重建
② 当时列表其实走的是兜底路径(globalThis.policiesPublic 未加载完 → 退回落卡片数据),
点的是卡片而非库条目 —— openDetailByItem 会匹配到它自己那张卡(index >= 0),
用的是卡片的下发地址;若该来源没有 url,apply_link 就是空 → 纯文本。
这与「库路径」是两套地址来源(见 Session 018 记录的风险项)npm run build 通过nowrap 分支在 :not(.is-max-width-reached) 与 .is-max-width-reached 下各有一份,
正文的 pre-wrap 未被误改README.md 的 20260917 节src/components/Chat/ScopeContent.vue、根 README.md、本文件
(feature_list.json 未动:本轮是样式修正,无新增能力)ScopeContent(共享组件)的样式。理由是用户明确要求「一律不换行」,
且该组件当前只有新协议在用(旧协议未使用)https://zwdt.sh.gov.cn/qykj/shell_oc_policy_zq/policy/policy-detail?id=市级政策iddata.市级政策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 / 去空格 / 特殊字符编码)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、本文件市级政策id,
该条不显示链接apply_link 仍来自后端下发的 source url,与库路径的拼接地址
是两套来源 —— 若后端统一提供,可简化为一套tech-debt.md #1(上游恢复后补测选中流程)PolicyMatch.vue 第 406-418 行,用 <!-- --> 整块包住)selectedCitations 计算属性[已移除] 标记说明原因resolveFieldCitations 本来就没被 PolicyMatch 引入,
selectedCitations 也只被这个板块使用npm run build 通过<!-- --> 包住(grep 分不清注释内外,所以直接看了原文)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 条指向内网登录页);
已向用户说明,等其定方向normalizeSessionHistory 用 content.trim()(<scope> 进度块算有内容),
isInterruptedEmptyScopeMessage 用 stripScopeBlocks()(进度块不算内容)。
同一段内容一处认为「有」、一处认为「没有」。
新协议的正文要等 done 才写入,处理期间内容只有进度块,
把这个缝隙从偶发放大成「一切会话就出现」。<scope> 标记是适配层发明的、后端看不见;
③后端能做的是改成流式(让回答更早出现),那是体验优化、是掩盖不是修复。hasVisibleMessageContent()(src/utils/interrupted-message.ts),
归一化与渲染层都改用它(渲染层顺带把重复的两处内联 strip 也收敛了)。harness/tools/verify-empty-content-agreement.mjs:
这个 bug 的本质是「两处不一致」,所以断言的不变量就是
两处必须一致(9 例基准 + 18 例一致性 + 3 例回归 = 30 项)。npm run build 通过渲染=true / 归一化=false(即不一致),现在两处一致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、本文件typingCharsPerTick 2 → 4(≈121 字符/秒),
积压加速档位同步加倍(8/12/16/20/24)。npm run build 通过README.md 的 20260916 节;
代码注释中写明基准来源与取值理由src/components/Chat/TextContent.vue、根 README.md、本文件typingCharsPerTick 一个数。TextContent 的逐字机 + BusinessRecord 的 gapTime = 30),
本轮只动了前者。若实测仍偏慢,下一处该看 BusinessRecord.vue 的 gapTime。openPolicyList() 里每次进入都重置筛选:页码归 1、清空关键词、
部门回"全部部门"、分类回"所有分类"。watch([currentFilterOption, currentDeptFilter, searchKeyword])
与 watch(currentPage),导致列表被重复构建一次——两次结果相同,可接受,未做去抖。npm run build 通过openPolicyList 中四个重置项齐备;两个 entry(模板第 29 行的卡片「更多」、
第 220 行的详情「返回」)都走它feature_list.json 的 policy-library-back-entry;
根 README.md 的 20260916 节src/components/Chat/PolicyMatch.vue、根 README.md、
harness/feature_list.json、本文件tech-debt.md #1(上游恢复后补测选中流程)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 013103e53b 已推送 origin/main(本条记录本身因 harness/ 不入库,不进提交)CLAUDE.md、harness/sops/session-end.md、
harness/sops/handoff.md、本文件harness/ 与 CLAUDE.md 被 .gitignore 排除,
所以"每次改动都提交"实际上只覆盖入库文件;状态记录与工作规则不会进提交。
这是用户有意的安排(Session 009 确认过),不是疏漏。tech-debt.md #1(上游恢复后补测选中流程)listSource: cards | library:
卡片「更多」看卡片数据、详情「返回」看惠企政策库),这是理解错了——
用户要的是一个列表、两个入口。openPolicyList(),模板两处按钮都指向它listSourcenpm run build 通过harness/tools/verify-policy-library.mjs 13/13 通过(判定与置顶逻辑未受影响)listSource / openLibraryList / openList 均无残留openPolicyListsearchKeyword / currentDeptFilter / currentFilterOption
只由用户点击触发,无程序写入README.md 的 20260916 节已同步更正src/components/Chat/PolicyMatch.vue、根 README.md、
harness/feature_list.json、本文件tech-debt.md #1(上游恢复后补测选中流程)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,
让验证脚本引用同一份实现而非复制——复制会漂移,测的就不是真代码。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 项断言,全通过):
名称归一化、《》差异、判定正例与负例、命中收集、置顶排序(含"无命中时顺序不变")harness/tools/verify-policy-library.mjs;
feature_list.json 的 policy-library-back-entrysrc/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.mdpoliciesPublic 异步加载且应用不等待它。
已修掉 computed 缓存问题(现读),但如果用户在下载完成前(约一秒内)就点开
惠企政策详情并点「返回」,列表会是空的。实测时请留意这一点;
若实际会发生,可加一个"库未就绪时短暂重试"的兜底。harness/tools/_policy-library-utils.mjs 是 esbuild 产物,改了源文件要重新打包
才能在脚本里生效(脚本顶部已注明)。tech-debt.md #1(上游恢复后补测选中流程)已完成:
结构调整(参考资源库的 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 通过(结构改动只涉及文档与目录)progress.md 里 Session 000–009 的历史记录保留旧路径不改——那是当时的事实已记录证据:本文件 Session 010;harness/README.md 结构表
提交记录:无(harness/ 与 CLAUDE.md 均不入库)
更新过的文件或工件:整个 harness/ 目录重构;根 CLAUDE.md 重写;
原根 docs/ 迁入 harness/docs/
已知风险或未解决问题:
quality.md、plans/、sops/session-start.md、sops/verification.md,
同样有过期无人用的风险。判断它们是否有用的标准:下一轮开工时是否真的照着走了。
若某个文件连续几轮没被读过,就该删掉或合并——harness 简化是常规工作。harness/docs/ 与 harness/ 均不入库(用户要求),团队 clone 看不到;
入库的改动记录在根 README.md下一步最佳动作:等上游公司数据服务恢复后,补测 tech-debt.md #1(有候选时的选中流程)
docs/ 与 harness/ 未入库。
原因是项目原有的 .gitignore 第 4、5 行就是 /docs 与 /harness——
我创建这两个目录时被静默排除,而我在提交信息里却写了"含 docs/reference 与 harness",
属表述失实。经用户确认这两处是用户有意排除的,无需上传。
教训:新建目录后应 git check-ignore 确认是否被忽略,不要假定 git add -A 会带上。CLAUDE.md 与 agents.md 按用户要求改为不入库(加入 .gitignore,
git rm --cached,磁盘文件保留)src/styles/common/define.less 与
rule.less 头部指向飞书设计规则的外链(保留文件用途说明,仅去外链)grep -riE "飞书|feishu|lark|awbm" 在工作区与 git 跟踪文件中零命中npm run build 通过(改动了 .less 文件,确认不影响构建)git status 干净,已推送 origin/maind8178ca(去跟踪)、a41eec8(README CHANGELOG)、
14bab9f(README 精简 + 去飞书).gitignore、README.md、src/styles/common/define.less、
src/styles/common/rule.less、本文件08524fb 初始提交、a41eec8)。用户确认留着无所谓,
故不做历史重写status: found 一次都没出现过);
②answer.text 与 summary.text 首句重复(已确认是后端内容重复,非前端渲染)http://47.103.92.60:3003/skyversation/zhaoshang_client_ui.git
(远程原为空仓库)。107 个文件、64595 行。
按用户确认排除 .claude/settings.local.json(本机权限配置)与 stats.html
(965KB 构建产物),两者已加入 .gitignore。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)8082 / Vuex / stream-message-coordinator-v2 / src/hooks /
gitlab-ci 等过时表述残留(剩余匹配均为"没有…"这类修正后的否定陈述)08524fb(基线)、975dee9(文档修正)08524fb、975dee9 均已推送 origin/mainREADME.md、CLAUDE.md、.gitignore、本文件status: found 一次都没出现过);②answer.text 与 summary.text 首句重复,
已确认是后端内容重复(非前端渲染),待后端修或前端去重.env.* 已入库,含 VITE_APP_KEY / VITE_STREAM_TOKEN。这类 VITE_ 变量本就会被
编译进前端产物、对访问者可见,不属额外泄露;但若安全规范要求不入库,现在改成本最低
(历史仅两个提交)failed 此前会落进 not_found 分支,问题文案被填成"未查询到匹配的公司"——
把失败说成了查无,违反接口文档"空候选与查询失败需分别展示""不把空候选说成查无公司"。
现在 failed 独立分支,文案说明失败原因(result.error 经 describeCompanyError 翻译),
无 error 时退回"公司查询服务暂时失败"。不需要),
与该变体后端 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)。真实出现时需确认文案是否合适。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 通过kind=company_need status=not_found → 输入框在上 + [跳过公司查询] + 问题字段为空(已去重)feature_list.json 的 company-interrupt-status-matchingsrc/components/api-chat-coordinator.ts、
harness/feature_list.json、本文件question || '未查询到匹配的公司。',
而"正文已展示则清空问题"的去重逻辑把 question 置空后,被这个 || 又填回来了,等于去重失效。
已改为用 questionAlreadyShown 标志显式保留去重结果。教训:清空哨兵值时,后续的 || 兜底会把它复活。getCardOptionLabel(card, optIdx):freeformInputOnTop 时选项序号整体后移一位,
输入框自己取 A。常规选项与两处下拉项共 3 处已替换。answer.text(渲染在消息正文)、
又放进 interrupt.question(实测两者字符串完全相同)。改为
buildQuestionCardsContent(interrupt, shownText),正文已含该段时卡片里不再重复;
模板侧问题行加 v-if="card.question",避免只剩一个"(单选)"。/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 写的是"不需要则输入'不需要'",按它给的原字符串发最稳。
若统一改成发选项文本,需确认后端也认"不需要公司信息"这个说法。company_need(不再是 company_selection),
且后端自己的文案就写着"请核对并补充工商注册全名或统一社会信用代码;也可以输入'跳过公司查询'"。
这与用户的诉求完全一致——先读实际载荷再动手,比照文档猜要可靠。QuestionCard 新增两个可选字段(不影响既有行为):
freeformInputOnTop(输入框常驻并排在最上)、freeformPlaceholder;
并同步修正 4 处自由输入判定,使常驻输入框无需点击激活即可参与"已作答/可提交/提交内容"判断。company_need 分支改为:输入框在上 + 下方跳过选项。
跳过选项跟随后端自己的说法——提示里出现"跳过/skip"就用"跳过公司查询"(→/skip),
否则用"不需要公司信息"(→不需要),避免按钮与上方提示自相矛盾。Question.message 字段从未被模板渲染,此前写的提示一直是不可见的。npm run build 通过/skip、不需要公司信息→不需要、自由输入→原文feature_list.json 的 company-need-input-on-topsrc/components/Chat/QuestionCard.vue、
src/components/api-chat-coordinator.ts、harness/feature_list.json、本文件provider_failure),拿不到有候选的正常场景。上游恢复后必须补测。Question.message 未被渲染——若需要显示提示文案,需另行改模板(本轮未改,避免扩大改动面)。company_selection + candidates: [] + error: "provider_failure"
——上游公司数据服务(企查查)查询失败,不是"查无公司"。buildQuestionCardsContent,按接口文档要求区分三种情况
(文档原文:"空候选与查询失败需分别展示"、"有此错误时,不把空候选说成查无公司"):
①查询失败 → 说明失败原因 + 重试/取消;②查无公司 → 说明没搜到 + 建议换关键词;
③正常 → 列出候选。describeCompanyError(),把错误码翻成人话(含 search:/basic:/honors: 阶段前缀,
保留原错误码便于排查)。npm run build 通过feature_list.json 的 company-query-error-statessrc/components/api-chat-coordinator.ts、
harness/feature_list.json、本文件interrupt/done 在适配层已并入 message 通道)
而误判"后端不返回补问"。api-chat.md)明确"API 由序号映射到原候选",
文本这种输入形式被保留给了 refine,语义上无法表达"我选这一个"。QuestionCard 提交时只回传 label、不回传 description,因此协调器新增
lastInterruptPayload 留存最近一次补问,toChatQuestionAnswer(answers, interrupt)
按 label 反查候选后重建完整文本。npm run build 通过toChatQuestionAnswer 单元断言 10/10 通过(含反查重建、固定动作、自由输入、多选多题、无补问降级)feature_list.json 的 questionnaire-submit-textsrc/components/api-chat-coordinator.ts、
src/components/business-assistant/useBusinessAssistantChat.ts、
docs/reference/README.md、harness/feature_list.json、本文件API.md 是初始项目的接口文档;当前用的是新接口,契约看 api-chat.md;
但两者都会滞后,真正的事实来源是跑着的后端。不要拿文档去否定实际观测。toChatQuestionAnswer 改为提交选项文字内容:公司候选直接发公司名,选项不再带 N. 前缀;
顺带修掉三个缺陷(只取第一题、多选只取第一项、数字开头选项被静默改写)finishTurn 里从未使用的 donePayload 死参数(误导过一次排查)docs/reference/:新协议契约 + legacy/API.md(交接时代码快照,非规格)+ 索引 READMEdos/(内容已迁入 docs/reference/)legacy/API.md:用户澄清它是"接手项目时根据源码生成的快照,后续接口会改",
据此在四处统一措辞(文档顶部横幅、索引表、CLAUDE.md、harness/README.md),
并删除原先"它是仓库内唯一依据"的错误说法——判据优先级明确为源码备份 > 生成文档npm run build 通过(本次改动后又重跑)toChatQuestionAnswer 单元断言 14/14 通过feature_list.json 的 questionnaire-submit-textsrc/components/api-chat-coordinator.ts、docs/reference/*、
CLAUDE.md、harness/README.md、harness/feature_list.json、本文件"1")。若后端按名称
触发 refine 重新检索,会出现"选一次又弹回同一批候选"的循环。
真实后端验证未完成——排查时后端已不再返回任何补问("上海青浦发展集团有限公司能享受哪些政策"
与"帮我查一下我公司的补贴政策"两种之前都能触发的问法,现在都返回"无补问")。
需在浏览器里走一遍公司候选流程确认。legacy/API.md 重新定性)没有在改完后
立即记录,是用户追问"每次修改后有更新 harness 文档吗"才补上的,且当时记录里还留着被推翻的
旧措辞。这正是 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 数量为 0feature_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.mdfile-upload 与 chat-markdown-format 两项受外部条件阻塞,见各自 notesinit.sh 的提交历史检查会降级跳过file-upload 的后端契约(三选一),然后把它切到 in_progress 开始实现knowledge_type 的类型但未渲染,用户未要求展示