{ "meta": { "project": "zhaoshang-llm", "description": "青浦区营商智能助手(Vue 3 + TypeScript + Vite),含 AI 对话、政策卡片、文件上传、虚拟人/语音能力", "repo_root": "f:\\yysk\\AI_zhaoshang\\zhaoshang-llm", "last_updated": "2026-09-17", "status_legend": { "not_started": "还没碰", "in_progress": "当前正在做的那个(同一时间只能有一个)", "blocked": "有记录的阻塞问题,推不动", "passing": "验证通过,证据已记录" } }, "features": [ { "id": "remove-dev-auto-login", "priority": 6, "area": "auth", "title": "去掉开发环境硬编码账号的自动登录", "user_visible_behavior": "开发环境不再自动进入登录态——没登录就是访客态;要企业态需显式带 ?access_token= 或 ?credit_code=。", "status": "passing", "verification": [ "npm run build 通过", "grep 确认 src/ 与 index.html 里 devLogin / isDevLogin 零残留", "代码路径核对:initAuth() 现在只剩「有 access_token 走登录」「有 credit_code 直接拉企业信息」「都没有则保持未登录」三条分支,无环境判断" ], "evidence": "分支已整段移除,构建通过,grep 零残留。⚠️ 浏览器观感未人工确认(见 notes)", "notes": "根因:useEnterpriseAuth.ts 里 `const isDevLogin = import.meta.env.MODE == 'development'` 在开发环境**无条件**用硬编码的 devLogin token 登录,所以「我没登录也进入登录态」。\n\n影响与取舍:dev 下 globalThis.token 不再自动注入。依赖它的旧接口(企业信息 /fta_ent_policy/enterprise_info 等)需要显式登录才可用;本轮涉及的会话/历史/反馈已全部走 DMS(token 由 vite 代理注入),不受影响。\n\n⚠️ 待人工确认:打开页面应停留在访客态(无企业名、历史用 ba_temp_chat_history)。" }, { "id": "dms-chat-storage", "priority": 5, "area": "chat", "title": "会话、问答记录与反馈落地到 DMS", "user_visible_behavior": "用户提交问题时会话与该会话的整段对话写入 DMS(一个会话一行,不是一问一答一行);会话列表、历史记录从 DMS 读取;会话改名/删除同步到 DMS;点赞/点踩/取消记进该会话行的 JSON 里对应那条消息;未登录的访客记录以「访客_<访客id>」归属写入;DMS 不可用时聊天与本地历史不受影响;开发环境不再自动登录(默认访客态)。", "status": "in_progress", "verification": [ "npm run build 通过", "harness/tools/verify-dms-chat-storage.mjs —— 36 项断言,打真实 DMS(经 vite 代理,与浏览器同一路径)。关键断言:第二轮后仍是同一行(不是一封问答一行)、消息顺序与 id 保留、长文本含协议标记往返一致、反馈只改命中那条消息且另一条未被误改、定位不到时不新增行、删除连带清理、时间戳解析、访客归属", "代理链路实测:经 /dms-api/ 查询返回 202(token 已由代理注入);直连 DMS 不带 token 返回 208 无token", "DMS 残留复查:测试数据已清理(残留 0 行)" ], "evidence": "验证脚本 36/36 通过;构建通过;代理注入 token 实证。⚠️ 浏览器端到端未人工点过(见 notes)。", "notes": "存储粒度(用户 2026-09-17 定的):**一个会话一行**。1889 的幂等键是 c_session_id(不是 c_record_id),整段消息序列化成 {version:1,messages:[…]} 存进 c_answer,c_question 存首问。写入方式是**以本地 messages 为准整段重写**,不做读-改-写 —— 本地状态是权威,从根上避免并发覆盖。\n\n会话标识用前端 session.id(UUID,恒等于聊天协议 thread_id,跨刷新稳定)→ c_session_id;消息 id 沿用 AI 消息 id,它与反馈组件的 record.id 是同一个值,反馈才定位得到。\n\n反馈:读整行 → 只改命中那条消息的 feedback 字段 → 写回;与对话写入共用 session: 顺序链。定位不到时**不新增行**。\n\n⚠️ **不存消息级时间戳**:本地消息本就没有时间戳,整段重写时现造 Date.now() 会让所有消息时间都变成最后一次保存的时刻——假数据比没有更糟。顺序由数组顺序表达,会话起始时间看该行 c_created_at。\n\n⚠️ **踩过的坑:findRowBy 不要加 orderBy**。1889 没有 c_updated_at 字段,按它排序会让 DMS 报错、反查恒空 → 每次 upsert 都新增 → 同一会话写成多行。跨栏目复用查询函数时,排序字段必须两个栏目都有。\n\n⚠️ **DMS 的删除不是 delContentById**(各种传法都 code=-1 / POST 405),正确姿势是 `POST /content/updateAudit` form {columnId, id, state: 4}(state=4 销毁)。\n\n⚠️ **addContent 返回记录 uuid,形态是纯字符串**(content 字段直接是 uuid),不需要再反查。c_id(must=true)实测不强校验,自动填 0。\n\n⚠️ token 由 **vite 代理注入**(.env.development.local 的 DMS_TOKEN,不入库),前端产物里没有 token;生产需 nginx 等价转发,长效凭据待确认。\n\n⚠️ **反馈状态刷新后不恢复显示**:JSON 里存着,但 BusinessRecord 的 localFeedback 初始恒为 None,未做从 DMS 回填。要做得另接一条线。\n\n访客只写不读(fetchRemoteSessions 对空 credit_code 返回 [],保持原行为)。" }, { "id": "empty-content-agreement", "priority": 15, "area": "chat", "title": "统一「内容是否为空」的判空口径", "user_visible_behavior": "切会话或刷新后,未产生正文的消息不再误报「请求已取消」;只有真正被用户停止的消息才显示该提示。", "status": "passing", "verification": [ "npm run build 通过", "harness/tools/verify-empty-content-agreement.mjs —— 30 项断言:基准判定 9 例 + 不变量(两处必须一致)18 例 + 回归 3 例", "覆盖用例:空串 / 空白 / 只有 1 个进度块 / 多个进度块 / 只有 silence / 进度块+正文 / 纯正文 / 正文+进度块 / 只有 POLICY_TABLE 标记" ], "evidence": "验证脚本 30/30 通过;构建通过", "notes": "根因:两处判空口径不一致——normalizeSessionHistory 用 content.trim()(scope 进度块算有内容),isInterruptedEmptyScopeMessage 用 stripScopeBlocks()(进度块不算内容)。同一段内容一处认为有、一处认为没有。\n\n新协议把正文推迟到 done 才写入(适配层 flushContent),处理期间内容只有进度块,把这个缝隙从偶发放大成「一切会话就出现」:归一化认为有内容→不替换文案却把状态翻成 Finish;渲染层认为没内容→显示请求已取消,而请求其实还在跑。\n\n修法:抽出唯一的 hasVisibleMessageContent()(src/utils/interrupted-message.ts),归一化与渲染层都改用。\n\n⚠️ 修复后的观感变化:原显示请求已取消的场景,现在可能显示「会话已经取消」(CANCELLED_SESSION_TEXT)——这是代码库里「消息结束但无内容」的既有措辞,且只在会话**未在生成中**时触发(switchSession 对正在生成的会话会跳过归一化)。若该措辞也不合适,需另定文案或改为不落文案。" }, { "id": "chat-api-migration", "priority": 10, "area": "chat", "title": "AI 对话迁移到新接口 POST /api/chat", "user_visible_behavior": "用户提问后能正常收到回答;政策类问题能出卡片;公司类问题能出补问候选。界面表现与原版一致。", "status": "passing", "verification": [ "npm run build 通过", "起本地服务端跑协议适配层契约测试(SSE 分片、CRLF、多行 data、409、断流、坏 JSON、过期 request_id、主动停止)", "对真实后端 192.168.2.23:8000 跑端到端:普通问答 / 政策推荐 / 公司补问 / 候选确认 / 翻页 / 取消" ], "evidence": "适配层契约测试 44/44 通过;真实后端多场景验证通过,行顺序 text→policy-table→text→ref-links,result.response 未被重复追加", "notes": "适配层在 src/components/api-chat-coordinator.ts,把新协议翻译成原界面本来就能渲染的内容标记(/POLICY_TABLE//)。旧协调器 src/components/stream-message-coordinator.ts 未改动、保留未用。请求体严格只发 {thread_id, question},多字段会 422。" }, { "id": "chat-rendering-order", "priority": 20, "area": "chat", "title": "回答内容顺序、换行与复制", "user_visible_behavior": "顺序为 正文 → 政策卡片 → 综合说明 → 参考资料;段落正常分段;复制出来的是纯可读文本,不含协议标记。", "status": "passing", "verification": [ "对真实后端取回内容,按帧喂给真实的 StreamXMLFilter,断言行顺序", "对含 scope/POLICY_TABLE/ref_links 的内容跑复制提取,断言标记全部被剔除" ], "evidence": "真实后端非 scope 行顺序 text→policy-table→text→ref-links(参考资料在最后);复制提取测试 7/7 断言通过", "notes": "复制用 copyableContent 正则剥离标记,不能用 getStreamPlainText——过滤器遇到