通用的仓库内会话进度日志。文件名沿用课程历史约定,不绑定 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 |
knowledge_type 的类型但未渲染,用户未要求展示顺序:按时间正序(旧 → 新),与根
README.md的日期节一致—— 最新的记录在最下面。加新记录请追加到文件末尾,不要插到开头。
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 开始实现
日期:2026-09-15
本轮目标:修问卷/补问提交序号的问题;建立接口参考文档目录
已完成:
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-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 要防的失效模式——文件不会自动维护,每轮收尾必须即时更新,不要攒。下一步最佳动作:在浏览器验证公司候选提交后的行为;若出现循环,与后端确认候选确认按什么取值
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;
但两者都会滞后,真正的事实来源是跑着的后端。不要拿文档去否定实际观测。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 通道)
而误判"后端不返回补问"。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 未被渲染——若需要显示提示文案,需另行改模板(本轮未改,避免扩大改动面)。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 写的是"不需要则输入'不需要'",按它给的原字符串发最稳。
若统一改成发选项文本,需确认后端也认"不需要公司信息"这个说法。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 标志显式保留去重结果。教训:清空哨兵值时,后续的 || 兜底会把它复活。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)。真实出现时需确认文案是否合适。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_ 变量本就会被
编译进前端产物、对访问者可见,不属额外泄露;但若安全规范要求不入库,现在改成本最低
(历史仅两个提交)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 首句重复(已确认是后端内容重复,非前端渲染)已完成:
结构调整(参考资源库的 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(有候选时的选中流程)
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(上游恢复后补测选中流程)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(上游恢复后补测选中流程)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(上游恢复后补测选中流程)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(上游恢复后补测选中流程)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。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、本文件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 条指向内网登录页);
已向用户说明,等其定方向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(上游恢复后补测选中流程)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(共享组件)的样式。理由是用户明确要求「一律不换行」,
且该组件当前只有新协议在用(旧协议未使用)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 了,脚本会提醒可以去掉这个开关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 条目逐项做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);刷新后列表出现该会话;改名/删除/点赞各验一次。
用户原话:「每次一问一答就会在 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(旧的今晚过期)
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+ 行),后续可考虑按会话分文件或定期归档,
但那是另一个改动,本轮不做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 传成了不同值,
那其实是「有后缀」形态;已修正夹具,不是代码问题)
docs/exec-plans/active/policy-card-list-sidebar.mdcardList(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)、matchesPolicyCardKeywordmergeWithPublicPolicies/normalizePolicy);
inject 拿不到 provider 时回退本组件卡片,不崩不空白declaration_item(2026-09-17 起 =「政策名 - 申报事项」,每条唯一);
精简格式的占位符「查看政策详情」会退化为「标题|匹配度|匹配理由|id」组合键,
避免把不同卡片错并成一条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 而非后来的)、
降序+同分稳定、六个检索面+大小写+空词、端到端组合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
active/,没移进 completed/)chat-sessions-dms.ts:撤掉「一会话一行 + 整段对话 JSON」的实现,恢复
upsertDmsRecord()(幂等键 c_record_id,c_question / c_answer 各存一半);
删除 DmsTranscriptMessage / serializeTranscript / parseTranscriptfetchDmsSessionRecords() 改成按 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 加了两条检查(机械约束,不靠人记):
active/ 里的计划若状态写着「✅/已完成」→ 提醒该归档completed/ 里的计划若状态仍写「🔄/进行中/待拍板」→ 提醒状态与位置不一致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、本文件
已知风险或未解决问题:
exec-plans/README.md 里早就写了
「做完移入 completed/」,我没做。现在有了机械检查,但仍需要我每次收尾时真的跑它8f60119 里,未丢失)下一步最佳动作:浏览器验收 DMS 与政策列表两条;以及从本轮起,收尾必跑
validate-harness.mjs 并当场归档计划(已加进 sops/session-end.md 的清单)
1887 助手会话:该会话 1 行,c_title = 首问「猫和老鼠讲述了什么故事」,
c_credit_code = 访客_68e82b71-a3ea-4332-b616-d174c4e9e4d41889 助手问答记录:2 轮 = 2 行(同一 c_session_id、不同 c_record_id,
c_question 与 c_answer 均已写入)访客_ 前缀)、开发环境确实处于访客态(没有自动登录)dms-chat-storage、policy-card-list-sidebar、remove-dev-auto-loginnode harness/tools/validate-harness.mjs 通过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 的验收场景是「同一政策的两个申报事项
分别点开、内容不同」,属于特定构造场景。保守起见仍保留「浏览器未人工点过」的标注,
等用户确认过再改——不靠推断把状态改好看.env.development.local 并重启 dev serverdocs/exec-plans/active/multi-session-parallel-generation.mdInputShell 的 isGenerating ? emit('stop') : emit('send')
用的是全局 isGenerating → 切到别的会话后按钮仍是「停止」,用户想发送却触发了停止handleStopGenerate 先 smc.stopGenerate()(mitt 同步 emit close
→ 监听器把 activeGenerationSessionId 清空),再取目标时
getActiveGenerationSession() || currentSession 必然 fallback 到当前会话 →
停止打在用户正在看的会话上;stopAiMessage 还会 ensurePendingAiMessage
凭空造出一条空的"请求已取消"消息chat-generation-task.ts(类型 + 4 个纯决策函数,无 vue/网络依赖,可被 harness 打包):
isSessionGenerating / resolveSendAction / resolveStopTargetTaskId / shouldNormalizeSessionHistorytasks = ref(new Map<sessionId, ChatGenerationTask>());
finishTask / stopTask / stopAllTasks;补问数据按会话存 sessionInterruptPayloadssmc / messageChunkBuffer / activeGenerationSessionId / dmsAnswerWrittenFor /
getActiveGenerationSession / finishActiveAiMessage / buildAppendHistoryPayload / setupCoordinatorisGenerating 改 computed(() => tasks.size > 0);新增 currentSessionGenerating 给按钮用handleStopGenerate 改为 resolveStopTargetTaskId → stopTask —— 旧 bug 自然消失startNewChat 不再打断生成(后台任务继续,与主流产品一致)toastMessage + showToast;PC/Mobile 各挂 <Toast>;
info 事件终于有人监听了(此前 409/错误提示用户完全无感)
stopAllTasks()(在 loadHistory 之前,避免旧账户任务写到新身份下):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 零事件、
各自计数独立)—— 这是并行化的地基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、本文件
typewriterTestSession,已同步改为 per-session 判断浏览器双会话实测(上述清单),特别是 mock 测试按钮回归
ref(new Map())。ref 会把 Map 变成响应式代理,
tasks.value.get(id) 返回的是代理、不是存进去的那个对象。
实测:ref(new Map()).get('A') === 原始对象 → false;shallowRef → true。tasks.value.get(sessionId) !== task →
永远成立 → 监听器全部提前返回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 项):
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 通过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、本文件multi-session-parallel-generation → passing(用户确认:mock 测试按钮跑完恢复、
真实对话内容正常流式输出);verification 里那条「❌ 未做浏览器验证」已替换为实测确认multi-session-parallel-generation.md → 从 active/ 归档到 completed/,
状态头改为「已完成并人工验证」,并注明过程中踩过的响应式陷阱(Session 030)validate-harness 通过(19 项功能,in_progress 0 项)completed/multi-session-parallel-generation.md;
feature_list.json 的 evidenceharness/feature_list.json、harness/docs/exec-plans/(归档)、
根 README.md、本文件dms-api-replacement 里剩下的企业信息(1888) —— 计划仍在 active/,
1887/1889 相关的 3 读 3 写已实现,企业信息那条没做(前端只用到 name/credit_code
两个字段,替换成本很低)。需要用户拍板要不要做。policy-detail-index-mismatch 的浏览器验证仍未单独确认 —— 那个 bug 的验收场景是
「同一政策的两个申报事项分别点开、内容不同」,属特定构造场景(Session 025 修,
自动化证据齐备;用户尚未针对该场景单独确认过)。file-upload(not_started)/ chat-markdown-format(blocked) —— 都卡在等待决策
(后端契约 / 是否接受在适配层做格式归一化),见各自条目。.env.development.local 并重启 dev server。/data/dms/dms_upload/yszs,并说明后端地址是 http://192.168.2.23:8000dms_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 里;现改为读 envVITE_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 通过npm run dev(用户已在跑的 8083,vite 检测到 config 变更自动重启)
→ 首页 200;经 /chat-api/api/chat 打真实后端 → http=200(不是 502).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 的功能),
只作约定记录——已在变量注释与架构文档里写明,避免以后有人以为它在起作用VITE_CHAT_SESSION_JSON_DIR 取,不要另写死地址/data/dms/dms_upload/yszs 到底哪种形态有数据
(承接 Session 032 我按字面把 URL 假设写进 env 的那处不确定)| 探测 | 结果 |
|---|---|
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 |
POST /dms/file/getFilespath(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、本文件
已知风险或未解决问题:
dms_upload/ 根下确实有 7 个 uuid 命名的 .json,
但我没有读文件内容的接口,无法确认它们是不是会话 JSON)192.168.2.23:8000,而该路径在 121.43.55.7 上(两台机器)——
若后端要写这个目录,是跨机器写(可能经 DMS 的文件接口),这点需用户/后端确认下一步最佳动作:请用户确认两件事 ——①后端是否已经开始往这个目录写会话 JSON; ②如果已有文件而我看不到,请给一个文件名/完整示例,我按它验证读取路径
/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 通过.env.development、.env.production、.env.qingpu、.env.test、
harness/docs/architecture.md、harness/docs/reference/DMS_API.md、根 README.md、本文件0caf1ae、7ce6ed3)——
包括那次探测得到的「DMS 有 POST /file/getFiles、path 是相对 Tomcat 工作目录的路径」
这条通用知识(不止对 yszs 有用)。如果哪天要浏览 DMS 服务器上的文件,
可以 git show 7ce6ed3 取回,或让我把不含 yszs 的部分重新写进参考文档下一步最佳动作:无(本轮为回退);功能清单里仍无 in_progress,可做的事见 Session 031 列的优先级
日期:2026-09-17
本轮目标:用户指出「进度日志顺序反了」——修正 progress.md 的会话记录顺序
已完成:
README.md 的日期节一致(README 的正序是 Session 016 时用户明确要求的)运行过的验证:
Session 000(基线) → … → Session 034,升序正确已记录证据:本文件 Session 035;上面的逐块校验输出
更新过的文件或工件:harness/progress.md
已知风险或未解决问题:
validate-harness.mjs 目前只查会话编号重复,
不查顺序)。若将来有人又插到开头,需要靠这条约定挡;要更强的话可以加一条校验下一步最佳动作:无(本轮为文档整理)
补充:同轮给 validate-harness.mjs 加了正序校验(会话编号必须升序),并实测能抓到乱序——故意把最新一条插到开头,校验器立刻报出「Session 035 之后跟了 Session 000」。测试后已恢复正序
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 前面两档
(惠企政策库 / 本轮卡片兜底)几乎总能命中openid_to_token(四份 env 里只有 .env.test 的
VITE_USE_KNOWLEDGE_API=false)src/network/api/card/index.js 的三个函数
(getMediaList / giveFeedback / appendHistory)都没有调用方——
反馈切 DMS 后最后一个调用点消失;目前只剩 useBusinessAssistantChat.ts:2
一个未使用的 importPolicyListUpdatedMatch.vue 仍挂在 BusinessRecord 上,但新协议不再产出
policy-list-updated 块,只有旧历史内容可能触发node harness/tools/validate-harness.mjs 通过docs/reference/current-api-call-sites.md(含可重跑的复核命令)harness/docs/reference/current-api-call-sites.md(新增)、
harness/docs/reference/README.md、本文件docs/reference/current-api-call-sites.md
c_credit_code 归属 —— 共 5 处依赖c_feedback_*(业务),又发统计接口(运营)VITE_ASSISTANT_STATISTICS_BASE_URL 只有 .env.qingpu 配了,
其余环境回退 VITE_APIuseBusinessAssistantUpload.ts 里的appendHistory 的来历(早就注释,历史改由接口 enable_history 控制)fetchPolicyList
前两档 return 才能判断几乎走不到);validate-harness 通过docs/reference/current-api-call-sites.mdharness/docs/reference/current-api-call-sites.md、本文件title 同步 ②存问答记录时把问题文本写进 titleharness/tools/probe-dms-title.mjs,用完即清理),查出三件原本不知道的事:
must=true
(summary 摘要 / qpyszx 是否青浦营商咨询 / qyzt 是否识别到企业主体 / rzqpyx 是否落户青浦意向)。
不带它们写入会被 214 数据错误 拒绝(实测:不带 214、带上 200)title 只在创建那一刻初始化:改 c_title 后 title 不跟随
(实测:改完 c_title=标题B、title 仍是 标题A)——这正是用户说"改名后 title 不同步"的真因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 字段、多发被忽略」这条差异npm run build 通过;其余脚本全绿(33/11/31);validate-harness 通过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)也请确认是否符合业务口径qpyszx / qyzt / rzqpyx 三个字段不要传值,
后续要传哪些他会告知chat-sessions-dms.ts 的 buildAnalyticsFields 去掉那三个字段,只保留 summary
(会话用标题、记录用问题文本)must=true,若再出 214 先看这里」212 无效token(32 项失败全部由此引起,与本次改动无关)must=true,
按 Step-0/上一轮的实测规律,缺 must 字段很可能再次被 214 数据错误 拒绝node harness/tools/verify-dms-chat-storage.mjs → 因 token 过期无法验证(已如实记录,
未把失败算到改动头上)summary(否则会误报)verify-dms-chat-storage.mjs 第 7 节的注释src/network/api/dms/chat-sessions-dms.ts、
harness/tools/verify-dms-chat-storage.mjs、根 README.md、本文件.env.development.local 的 DMS_TOKEN
并重启 dev server)verify-dms-chat-storage.mjs,确认写入是否仍通chat-sessions-dms.ts 删掉 buildAnalyticsFields 及其两处调用;
写入只发业务字段 + title:c_credit_code c_session_id c_title title c_source c_created_at c_updated_atc_credit_code c_session_id c_record_id c_question title c_source c_created_atharness/tools/verify-dms-payload-fields.mjs —— 打桩 fetch 抓真实请求体,
解析 form 里的 content 键集合,断言「不多不少」:9/9 通过title,不含 summary/qpyszx/qyzt/rzqpyx)title(原先不带 → DMS 里标题不跟随改名)title 的值就是问题文本globalThis.fetch 打桩即可断言发出的内容npm run build 通过;validate-harness 通过harness/tools/verify-dms-payload-fields.mjssrc/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、本文件verify-dms-chat-storage.mjs 确认真实写入是否通过.env.development.local,按约定不入库;
vite 代理已自动重启拾取,实测经代理查询返回 200)title = 问题文本 已确认生效 |
| 1887 助手会话 | ❌ 被 214 数据错误 拒绝 || 载荷 | 结果 | |---|---| | 全不带(= 当前前端) | 214 | | +summary | 214 | | +summary+qpyszx | 214 | | +summary+qpyszx+qyzt | 214 | | 四个都带 | 200 成功 | | 只带 qpyszx(不带 summary) | 214 |
must=true。二者不能同时满足,必须有一方让步。verify-dms-chat-storage.mjs(打真实 DMS)——
1889 相关全通过、1887 相关因 214 失败(如实记录,未粉饰).env.development.local(不入库)、本文件must=false)→ 前端就能只传 title(最贴合用户要求)
②前端仍传这四个 → 取值口径要定;也可以先传空值/默认值(summary:"" 等)
让写入先通,后续再定口径 —— 但这是"为了通过校验而填",需用户认可
③维持现状 → 1887 会话写入持续失败summary:"" + 三个 false → 200 成功 ✓
(must 只校验"字段在不在",不校验非空)chat-sessions-dms.ts):
buildRequiredSessionFields(),返回 {summary:"", qpyszx:false, qyzt:false, rzqpyx:false}verify-dms-chat-storage.mjs 43/43 通过 —— 1887 写入已恢复
关键项:新建会话带 title ✓、改名后 c_title 与系统 title 都跟着变 ✓、
四个必填字段确实写进去了且为空/默认 ✓、记录 title = 问题文本 ✓verify-dms-payload-fields.mjs 10/10 通过(打桩抓请求体,不需要 token):
会话新增字段集合「不多不少」、更新不带那四个字段、记录不带npm run build 通过verify-dms-chat-storage.mjs 里「摘要应为会话标题」——已过期,改成
「写入且为空(不猜业务值)」+「三个布尔为默认 false」verify-dms-payload-fields.mjs 头部说明还写着旧的「不要传 summary」,已更正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 为空。
若业务口径定了(或该由后端回填),需要再改这里[已停用] 与原因):
sendMessage:去掉 upsertDmsSession(会话行不再新建/更新)submitQuestionAnswers:同上renameSessionAt):不再同步到服务端,只改本地syncSessionTitleToServer 函数整体注释;upsertDmsSession / updateSessionInfo
两个 import 一并去掉(注释说明去处)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 通过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、本文件mergeRemoteSessions 拿到空数组是无害的,行为与接 DMS 前一致)docs/reference/current-api-call-sites.md: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/policiesopenid_to_tokenvalidate-harness 通过;每条与代码核对过docs/reference/current-api-call-sites.mdharness/docs/reference/current-api-call-sites.md、本文件useBusinessAssistantChat.ts:2 那个未使用的 import cardAPI 还在(是死模块 card/index.js
的唯一引用)——要清理另说,属独立的小清理BusinessAssistant.vue(页面浏览)、BusinessRecord.vue(反馈统计)src/network/api/assistant-statistics.ts 删除VITE_ASSISTANT_STATISTICS_BASE_URL 从 .env.qingpu 删除getOrCreateAssistantVisitorId)——DMS 的
访客_<id> 归属在用它,已搬进 src/network/api/dms/chat-sessions-dms.ts;
localStorage 的 key 字符串保持不变(assistant_statistics_visitor_id),
改了会让已存在的访客换新 id、同一浏览器在 DMS 里变成两个访客src/network/api/card/index.js 整文件删除(三个函数都无调用方),
连同 useBusinessAssistantChat.ts 里那个未使用的 import cardAPIassistant-statistics.ts(同上)PolicyListUpdatedMatch.vue 有意保留:它虽对新协议无效,
但老会话的历史内容里可能还有 <policy-list-updated> 块,删了那些卡片列表渲染不出来npm run build 通过;vue-tsc --noEmit 本次涉及文件无报错verify-dms-payload-fields.mjs 12/12;verify-dms-chat-storage.mjs(真实 DMS)43/43assistant-statistics / network/api/card 在 src 下零引用;validate-harness 通过docs/reference/current-api-call-sites.mdsrc/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 log --diff-filter=D --name-only)查证结果:只依赖 1 个 WebSocket 接口
wss://qingpu-data-api.metamaker.cn/common/asr_hub
?project=qingpu&engine=aliyun_dashscope&is_long_connection=true&heartbeat=true
VITE_ASR;引擎是 aliyun_dashscope(阿里云百炼),不是讯飞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、本文件
已知风险或未解决问题:
VITE_XF_ASR / 讯飞分支整条是死代码(isXF 恒 false),但没删(不在本轮范围)下一步最佳动作:无(本轮为查证 + 改文档)
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 通过architecture.md 的更正harness/docs/architecture.md、本文件three-libs/asr 不能删docs/exec-plans/tech-debt-tracker.md 第 6 条,
免得下次会话又从头讨论:wss://qingpu-data-api.metamaker.cn/common/asr_hub(引擎 aliyun_dashscope),
token 与 appKey 硬编码在源码里(token exp 2034,入库可见)reference/current-api-call-sites.md)→ 自建网关时前端零改动isXF 写死改成 env 开关,便于改配置切讯飞分支git status 干净;validate-harness 通过tech-debt-tracker.md 第 6 条harness/docs/exec-plans/tech-debt-tracker.md、本文件buildQuestionWithAttachments(question, fileUrls)(shared.ts):
原文 + 换行 + 每条 URL 一行;不加「附件:」之类自定义措辞(不凭空发明后端可能不认的写法);
无有效 URL 时原样返回(不能凭空多换行,否则影响所有普通提问)sendMessage 拆开两个变量(关键):
questionForBackend = 拼了附件地址 → 只发给后端text = 用户原文 → 本地消息内容、会话标题、DMS 记录
这样用户气泡里不会出现一长串 URL,DMS 记录也保持可读BusinessAssistantMobileInputShell.vue 里原先整块注释):
上传图片 / 上传附件。拖拽与粘贴本来就是通的,不是本轮才恢复OSS_SIGNATURE_URL 改为可配置:新增 env VITE_UPLOAD_SIGN_URL(四份 env 都写,
值仍是厂商地址)→ 将来换自建签名服务只改配置、不改代码POST //qingpu-data-api.metamaker.cn/common/qp_signed_url body {ext} → 返回
file_url(https://prod.heijingai.com/qingpu/<uuid>.<ext>)、host
(https://heijing-products.oss-cn-hangzhou.aliyuncs.com)、key、policy 等heijing-products)里,不是我们的 ——
政务场景下这点值得评估(数据在第三方)harness/tools/verify-attachment-question.mjs —— 10/10 通过:
无附件原样返回(undefined/null/空数组/全空串)、有附件时原文在前且 URL 各一行、
不添加自定义措辞、脏数据过滤、URL 去空格npm run build 通过verify-attachment-question.mjs;签名接口实测响应src/components/business-assistant/shared.ts、
src/components/business-assistant/useBusinessAssistantChat.ts、
src/components/business-assistant/useBusinessAssistantUpload.ts、
src/components/BusinessAssistantMobileInputShell.vue、四份 .env.*、
harness/tools/{_entry-attachments.ts,verify-attachment-question.mjs}(新增)、
根 README.md、harness/feature_list.json、本文件.env.development 里 4 个变量定义多次VITE_STREAM_SERVER、VITE_STREAM_TOKEN(含两个凭据)、
VITE_HUMAN_IDLE_MP4_URL、VITE_PLUGIN_URL、VITE_PLUGIN_ASR
—— 都是数字人/插件那条从未接入本项目前端的链路的遗留.env.development 的重复定义:VITE_ASR、VITE_XF_ASR 各定义两次,合并成一份/stream/ 代理(无代码使用);/asr/ 代理保留(讯飞退路的一部分)public/images、public/pencil_exportsstats.html(968K,rollup visualizer 的分析产物,npm run build 会重新生成).env 的语义是后定义的生效 —— 于是 VITE_ASR 被改成了原本的死值
(wss://human-screen-v3...),会弄坏语音识别。核对 diff 时发现,已把保留的那两行
改回正确的生效值(https://qingpu-data-api.metamaker.cn 与 /asr)。
教训:合并重复配置前要确认哪一份生效(.env 是后者覆盖前者)。src/three-libs/asr/ —— 语音识别,在用(用户明确「先用原来的」)VITE_XF_ASR + vite /asr/ 代理 —— 切引擎的退路(与 ASR 那个决定配套)src/components/stream-message-coordinator.ts —— 架构文档明确「保留未用,勿改」PolicyListUpdatedMatch.vue —— 老会话历史里可能仍有 <policy-list-updated> 块dist/(3.1M)—— 是部署产物,不是垃圾;要删另说npm run build 通过VITE_ASR 生效值正确、VITE_XF_ASR=/asr 与代理保留、
上传签名地址在、政策库 json 在vite.config.ts 里那 3 处是我写的说明注释)/chat-api 打真实后端 200(env 缩减后没坏)validate-harness 通过.env.*、vite.config.ts、根 README.md、本文件
(删除:stats.html、public/images/、public/pencil_exports/)VITE_STREAM_TOKEN)仍在 git 历史里;它们是数字人流的 token,
现无代码使用,但若安全要求彻底清除需重写历史dist/ 未删(部署产物),要腾空间可自行删harness/tools/probe-answer-latency.mjs:打真实后端,记录每个 SSE 事件的时间戳,
并把前端打字机的展开时间按 TextContent.vue 的真实参数模拟出来,拆成两段| 问题 | 后端耗时 | 回答字数 | 前端打字机 | 用户感知 |
|---|---|---|---|---|
| 「你好」 | 1.3s | 81 | 0.5s | 1.7s |
| 「高新技术企业能享受什么优惠」 | 49.8s | 767 | 1.9s | 51.7s |
| 「奶茶店有什么扶持政策」 | 65.0s | 896 | 2.1s | 67.1s |
summary(正文)在 48.4s 整段到达,
在此之前界面上没有任何正文,只有「思考中」卡片 + 心跳提示"还在为您准备答复"recommend 阶段(26.5 秒,期间只有 heartbeat);
30.8s 才出 source + summary第一版脚本只统计 answer 事件的字数,而这个后端把正文放在 summary 里
(result.response 是同样的快照)→ 一开始得出「回答总字数 0」,误以为是"后端零输出"。
已改为统计 answer + summary(不含 result 快照,因为适配层也只展示一次)后重测,
上面的表是修正后的数据。
即便再调快,收益是秒级;而后端是几十秒级 → 优先治后端
运行过的验证:三个问题各实测一次(原始事件时间戳为证);validate-harness 通过
已记录证据:本文件 Session 051;harness/tools/probe-answer-latency.mjs(可重跑)
更新过的文件或工件:harness/tools/probe-answer-latency.mjs(新增)、本文件
已知风险或未解决问题:
recommend 阶段内部发生了什么,前端看不到)下一步最佳动作:把这份数据给后端;最有效的一步是让后端改成流式(边生成边推
answer/summary),用户等待感会立刻不同;其次是查 recommend 阶段为何要 26 秒
PolicyMatch.vue 的 rebuildCardList():数据源改回本组件自己的卡片
(props.policyData / props.policyContent,与 syncPolicies 同源),
不再 inject('collectSessionPolicyCards') 往上层取会话级聚合useBusinessAssistantChat.ts:删掉 provide('collectSessionPolicyCards', …) 与对应的 importpolicy-match-utils.ts:删掉 collectPolicyCardsFromMessages() 及其
parsePolicyTableContent import(该函数是它唯一的使用者)harness/tools/_entry-policy-cards.ts:去掉该导出verify-policy-card-list.mjs:删掉第 1 节(跨消息聚合的断言)、
第 6 节改用直接传入卡片数组;头部两段说明也改了(有一段还写着
「侧边栏要的是整个会话的聚合」,与新行为自相矛盾,留着会误导)dedupePolicyItems、排序
sortPolicyCardsByMatchScore、关键词过滤 matchesPolicyCardKeyword
—— 同一回答里若出现重复条目,仍会去重verify-policy-card-list.mjs 24/24 通过(原 31 项中去掉 7 项聚合断言)npm run build 通过collectSessionPolicyCards / collectPolicyCardsFromMessages
在源码与入口里零命中(只剩我写的"已移除"说明注释)validate-harness 通过verify-policy-card-list.mjssrc/components/Chat/PolicyMatch.vue、
src/components/Chat/policy-match-utils.ts、
src/components/business-assistant/useBusinessAssistantChat.ts、
harness/tools/{_entry-policy-cards.ts,verify-policy-card-list.mjs}、
根 README.md、harness/feature_list.json、本文件policy-card-list-sidebar 的 verification 去掉「❌ 未做浏览器实测」,
换成用户的实际确认;evidence 同步更新validate-harness 通过;git status 干净feature_list.json 的 policy-card-list-sidebarharness/feature_list.json、本文件file-upload 仍是 in_progress:它的「浏览器端到端」还没确认过
(本轮用户的「检查无误」是指「更多」侧边栏那条)。上传那条要确认的是:
选文件 → 上传成功 → 提问 → 后端确实收到带 URL 的 questionfile-upload 的 verification 去掉「❌ 未做浏览器端到端」,换成用户确认;
状态 in_progress → passingvalidate-harness 通过(19 项功能,in_progress 0 项);git status 干净feature_list.json 的 file-uploadharness/feature_list.json、本文件heijing-products,杭州),
链接为 prod.heijingai.com——数据在第三方,政务场景下需评估(用户知悉)VITE_UPLOAD_SIGN_URL,
换自建服务时只改配置;替换需要提供的清单已在本会话给出(OSS 凭据 + 一个签名服务 + 文件访问前缀)BusinessAssistantMobile.vue 首页):| 元素 | 改前 | 改后 |
|---|---|---|
| 顶栏 .mobile-header | 64px | 52px |
| logo .overlay-icon(含下边距) | 130×88 + 41 | 96×65 + 20 |
| 大标题 .heading | 28px(13 字放不下会折两行) | 22px + clamp(18px,5.6vw,22px) + nowrap(恒定单行) |
| 欢迎区 gap | 7px | 6px |
| 介绍文案行高 | 1.6 | 1.5 |
| 建议词区 | padding 28 / gap 12 | padding 18 / gap 10 |
clamp 覆盖」的写法:老内核拿到 22px(窄屏也放得下),
新内核按屏宽继续收 —— 目的是保证恒定单行.home-fixed-input 仍是 position: fixed
悬浮、不占文档流,而给它预留的是写死的 padding-bottom: 180px,与输入框实际高度无关
—— 字体一放大、输入框一换行,180px 就不够npm run build 通过;逐项核对新值已落进源码src/components/BusinessAssistantMobile.vue、本文件、技术债清单position: fixed,改成
.ba-mobile-container 的 flex 子项 + .mobile-main { min-height: 0 })——
那样剩余高度是浏览器算出来的,与字体/内核无关,数学上不会再遮BusinessAssistantMobile.vue 首页):
.welcome-section 新增两侧留白 padding: 0 28px(连 .mobile-main 的 8px,
视觉留白约 36px)—— 标题与介绍文案都不再贴屏幕边.heading 字号 22px → 20px(clamp 同步收为 clamp(17px, 5.2vw, 20px)).desc 字号 14px → 13px:加留白后可用宽度变窄,字号同步收一点以避免多折一行npm run build 通过;排版用脚本实算(非目测)src/components/BusinessAssistantMobile.vue、本文件9ccf536 提交里;本次是在其基础上继续调字号与留白BusinessAssistantPC.vue:127 与 BusinessAssistantMobile.vue:39 的标题
你好,有什么我可以帮您的? → 有什么我可以帮您的?(两端一致,只动文字)查过的项与结果(全部为实际执行输出,非推断):
| 检查 | 结果 |
|---|---|
git status / HEAD 与 origin/main |
✅ 干净、一致——没有未提交或未推送的改动 |
最后一次提交之后有无源文件被改(find -newermt,排除忽略项) |
✅ 无(只有我本轮改的两个文件) |
bash harness/init.sh |
✅ exit 0(含 npm install + npm run build) |
node harness/tools/validate-harness.mjs |
✅ 通过(19 项功能,in_progress 0 项) |
| 10 个验证脚本 | ✅ 全绿:33/11/24/8/10/21/30/10/12 + 打真实 DMS 的 43/43 |
exec-plans/ |
✅ active 只剩 dms-api-replacement.md(在办),completed 3 个 |
| dev server 8083 | ✅ 在跑,首页 200 |
→ 上一段会话(Session 056)在提交 f1951af 之后结束,文件夹层面没有半成品。
查到 5 处异常,全是「记录与事实不符」,已当场修掉:
CLAUDE.md 末尾自相矛盾:底部仍写着「harness/ 与 harness/docs/ 按 .gitignore
约定不入库」,而上方「提交约定」写着 2026-09-17 起已入库
(Session 024 声称改过三处,漏了这一处)→ 已改为如实说明。README.md 从未有过 ## 20260918 节:09-18 的改动(DMS 系列、上传恢复、
统计埋点删除、清理…)一直被追加在 ## 20260917 标题下面(从 09-18 第一个提交
061bcfc 起就这样,不是本次才漂的)→ 已在正确位置补上 ## 20260918。dvh 无 vh 兜底、无 text-size-adjust、padding-bottom: 180px 写死)。feature_list.json 的 dms-chat-storage 条目内部打架:同一段 notes 里
同时存在「1887 写入已恢复(43/43)」与「🚨 1887 会话写入当前是坏的」、
已推翻的「一个会话一行」粒度、以及「浏览器人工点验始终没做」
(而同条 verification 里写着用户 09-17 已确认);meta.last_updated 也停在 09-17。
→ 已把这些按当前事实重写成一段(在写的/已停用的/规矩/踩过的坑/已知缺口),
并去掉与 verification 冲突的表述;payload 脚本断言数由 9 更正为 12。npm run build 通过(改动后重跑)bash harness/init.sh exit 0;validate-harness.mjs 通过verify-dms-chat-storage.mjs 43/43,
token 有效期至今晚 21:33)README.md 的 20260918 节;
feature_list.json 的 dms-chat-storage 重写;技术债第 7 条的「当前进度」src/components/BusinessAssistantPC.vue、
src/components/BusinessAssistantMobile.vue、CLAUDE.md、README.md、
harness/feature_list.json、harness/docs/exec-plans/tech-debt-tracker.md、本文件nowrap + 字号没变,去掉 3 个字只会更宽松,理论无风险)feature_list 的 notes
都是"只往上加、不回头改",越积越互相矛盾。下一轮起,改完当场回写——
这次靠人工发现,validate-harness.mjs 目前查不出这类内容矛盾。若要加机械约束,
可考虑"README 的日期节必须覆盖最近提交日期""notes 里出现互相矛盾的结论"这类检查。policy-detail-index-mismatch 仍保留「浏览器未人工点过」的保守标注(Session 028 的决定),
用户若顺手验过「同政策两个申报事项点开内容不同」,即可去掉该标注你好,有什么我可以帮您的?.heading | 32px | 28px |
| 移动端 .heading | 20px + clamp(17px,5.2vw,20px) | 16px + clamp(13px,4.2vw,16px) |@media (max-width: 360px) 里有一条 .heading { font-size: 28px }.heading 基础值就是 28px),
但 2026-09-18 基础值改小后,它在窄屏上反而把标题放大回 28pxwhite-space: nowrap,13 个字 × 28px ≈ 356px,
而 360px 屏可用宽度只有 272px → 标题被顶出屏幕约 84px(正是用户此前抱怨的那类现象).heading 自己的 clampnpm run build 通过.heading 只剩 PC 与移动端两处定义(窄屏覆盖已注释),无其他覆盖源README.md 20260918 节
(上一轮那条「去掉你好」的记录已改写为「一度去掉、随即加回」——不留错误的旧记录)src/components/BusinessAssistantPC.vue、
src/components/BusinessAssistantMobile.vue、README.md、本文件
(feature_list.json 未动:纯样式/文案调整,无新增能力)clamp(13px,4.2vw,16px) → clamp(17px,5.2vw,20px)font-size: 20px,但紧跟着的
font-size: clamp(13px, 4.2vw, 16px) 在后面、会覆盖它(同优先级后者胜)
→ 现代浏览器上实际渲染仍是 ≤16px,手改等于没改。
已在代码注释里写明「两行必须同进同退」,避免下次再踩。npm run build 通过src/components/BusinessAssistantPC.vue、
src/components/BusinessAssistantMobile.vue、README.md、本文件font-size 写两行做降级时,只改基础值不生效;
这类"同值覆盖"在改基础值后会变成有效覆盖(本轮 Session 058 已经踩过一处 28px 的).heading:clamp(17px, 5.2vw, 20px) → clamp(17px, 5.7vw, 22px)
(基础值同步改 22px;两行一起改,见 Session 059 的坑)5.7vw 这个系数是按上限等比推的(原 5.2vw 对应上限 20px):
≥386px 的屏恒为 22px,更窄的屏按比例收缩npm run build 通过src/components/BusinessAssistantMobile.vue、README.md、本文件clamp(17px,5.7vw,22px) 即可nowrap,各屏能容纳的上限是
320px→18.5px / 360px→21.6px / 375px→23.9px(脚本实算)。再往上调就得
先收窄两侧留白或允许折行