| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266 |
- {
- "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": "policy-detail-index-mismatch",
- "priority": 4,
- "area": "policy",
- "title": "修复:同一政策的多个申报事项点「查看详情」内容一样",
- "user_visible_behavior": "两张卡片即使是同一政策下的不同申报事项,点「查看详情」也各自打开对应的那条内容,不再都跳到第一张。",
- "status": "passing",
- "verification": [
- "npm run build 通过",
- "harness/tools/verify-policy-detail-index.mjs —— 10 项断言全通过。关键项:「点第 2 张 → 下标 1(修复前这里会返回 0)」、字段无法区分时返回 -1(不张冠李戴)、唯一命中仍采信、id/policy_id 可用、空值边界",
- "旧逻辑对照实证:同政策两卡片的场景下,点第 1 张与点第 2 张**都返回下标 0** —— 复现了用户看到的现象",
- "真实载荷探针 probe-policy-cards.mjs:确认后端 item 的 id/policy_id 恒为 undefined、card.name.text 两条相同"
- ],
- "evidence": "回归脚本 10/10 通过;旧逻辑对照复现了 bug;构建通过。⚠️ 浏览器未人工点过(见 notes)",
- "notes": "根因链:①后端 item 事件的 id/policy_id 恒为 undefined,唯一标识在 data.title(「政策名 - 申报事项」)与 data.source_id;②适配层 toPolicyTableItem 把 title 取自 card.name.text(只有政策名),并把 declaration_item 也设成同一个值,**丢掉了每条唯一的 data.title**;③于是同政策的多个申报事项 declaration_item 完全相同,而 openDetailByItem 的兜底匹配正是拿它比 → findIndex 永远命中第 0 张。\n\n修法:抽纯函数 resolvePolicyDetailIndex()(policy-match-utils.ts),匹配顺序改为「对象同一性优先 → 字段比对仅在唯一命中时采信 → 否则 -1 交给兜底」。未改任何 UI 文案与样式。\n\n⚠️ **未修的更深问题**:适配层丢掉了 data.title 里的申报事项后缀,因此详情面板标题(declaration_item)对同一政策的所有申报事项都一样——内容修好了,但用户从标题上仍分不出看的是哪个申报事项。要改需动显示文案,属 UI 变更,等用户拍板。"
- },
- {
- "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,把新协议翻译成原界面本来就能渲染的内容标记(<scope>/POLICY_TABLE/<ref_links>/<question-cards>)。旧协调器 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——过滤器遇到 <!-- POLICY_TABLE 会把同批 buffer 里它之前的内容当纯文本输出。"
- },
- {
- "id": "policy-source-badge",
- "priority": 30,
- "area": "policy",
- "title": "惠企政策库来源标识",
- "user_visible_behavior": "来自惠企政策库的推荐卡片,右上角显示橙色「惠企政策」标识。",
- "status": "passing",
- "verification": [
- "对真实后端取回卡片,断言 is_policy_library 字段存在且为布尔值",
- "断言打标条目与来源文件对应(reference/惠企政策.json)"
- ],
- "evidence": "真实后端 8 张卡片,字段均为布尔值,1 张打标(与 file=reference/惠企政策.json 一致)",
- "notes": "只在卡片右上角显示,不同步到「更多」列表与详情面板(用户明确要求)。样式类 .policy-source-badge 与左侧 .policy-badge 同形不同色。"
- },
- {
- "id": "policy-detail-blocks",
- "priority": 40,
- "area": "policy",
- "title": "政策详情面板新增区块(条件核验 / 匹配依据)",
- "user_visible_behavior": "点卡片「查看详情」,面板底部有 条件核验 / 匹配依据 两个区块。",
- "status": "passing",
- "verification": [
- "npm run build 通过",
- "对真实后端断言 conditions / match_basis / citations 随 POLICY_TABLE 传入"
- ],
- "evidence": "真实后端卡片解析出 conditions / match_basis / citations 字段;构建通过",
- "notes": "复用面板原有的 detail-row card-style / record-condition-title / condition-match-card 类,配色用已有的 match-high/medium/low 等级样式,未引入新视觉语言。\n\n⚠️ 2026-09-17:「原文引用」板块按用户要求删除(模板与其计算属性均已注释保留,未直接删,符合项目「注释不删除」的约定)。"
- },
- {
- "id": "company-interrupt",
- "priority": 50,
- "area": "chat",
- "title": "公司补问与候选确认",
- "user_visible_behavior": "问公司相关问题时会弹出选项卡;选候选公司、换关键词、查看更多、取消都能继续对话。",
- "status": "passing",
- "verification": [
- "真实后端触发 company_need 与 company_selection",
- "断言选项可被原 QuestionCard 解析",
- "断言选择结果还原为协议字符串(1→\"1\"、查看更多→/next、重新查询→/retry、取消→/cancel、不需要→不需要)"
- ],
- "evidence": "真实后端候选 5 家正常展示;序号续问、/next 翻页、/cancel 均返回 done=completed/needs_input;往返映射 8 项断言通过",
- "notes": "interrupt 翻译成 <question-cards>,由原 QuestionCard 渲染;提交时经 toChatQuestionAnswer 转成 question 字符串。注意:公司候选提交的是公司名称而非序号,见 questionnaire-submit-text 条目。"
- },
- {
- "id": "policy-library-back-entry",
- "priority": 45,
- "area": "policy",
- "title": "政策事项列表:统一入口,仅惠企政策可返回",
- "user_visible_behavior": "卡片区「更多 >>」与详情的「返回政策事项列表」进入的是同一个列表(惠企政策库 262 条申报事项,每次进入重置筛选),本轮匹配到的惠企政策置顶;非惠企政策的详情里不显示该返回入口。",
- "status": "passing",
- "verification": [
- "npm run build 通过",
- "harness/tools/verify-policy-library.mjs —— 21 项断言:名称归一化、判定(接口字段优先/标题回退/负例)、命中收集、置顶排序(含无命中时顺序不变)、政策详情地址拼接(有 id / 无 id / data 缺失 / 空白 id / 去空格 / 特殊字符编码)+ 真实库覆盖率 261/262",
- "harness/tools/check-policy-sources.mjs —— 对比本地与远端两个来源:本地 262 条 / 261 带 市级政策id,远端 327 条 / 0 带(确认开发环境本地优先仍有必要)",
- "真实库数据:262 条 / 38 个政策名;后端 is_policy_library=true 的那条标题能在库中匹配到",
- "生产产物 dist/index.html 的 localFirst 求值为 false(生产仍远端优先);dev server 提供的本地 json 与仓库文件 sha256 一致"
- ],
- "evidence": "tools 脚本 21/21 通过;check-policy-sources.mjs 输出「本地 262 条 / 261 带 id、远端 327 条 / 0 带 id」;构建通过,dist/index.html 的 localFirst 为 false、dev 为 true。另修掉一个缓存 bug:库数据一度写成 computed 读 globalThis,会把异步加载前的空值缓存住,已改为函数现读。",
- "notes": "⚠️ 政策名称跳转:由库条目的 data.市级政策id 拼出 https://zwdt.sh.gov.cn/qykj/shell_oc_policy_zq/policy/policy-detail?id=<id>(见 policy-library-utils.ts 的 buildPolicyDetailUrl)。库覆盖率 261/262。\n\n⚠️ **261/262 只对「本地那份」成立**(2026-09-17 查明):另一个来源(`VITE_DOWNLOAD_URL` 远端那份,327 条)**0 条**有这个字段 —— 这才是浏览器里「政策名称」显示为纯文本的真因(Session 019 记的「陈旧 HMR 状态」「走了兜底路径」两个猜测都不对)。远端那份的 `policy_id` 是 40 位、与该条 `id` 相同,与 市级政策id(24 位)是**两套 id 体系**,拼不出一网通办的地址。index.html 原本是远端优先,且远端**带 CORS 头**(实测 `Access-Control-Allow-Origin` 回显 `localhost:8083` 与 `aixq.shqp.gov.cn`),所以浏览器里远端确实会赢。\n\n为此新增 `VITE_POLICY_LOCAL_FIRST`(仅 `.env.development` 为 true,其余三份 env 显式 false):dev 本地优先、远端兜底,生产仍远端优先、本地兜底。\n⚠️ 生产环境「政策名称」仍不可点,属**后端数据问题**(需远端那份补 `data.市级政策id`);`check-policy-sources.mjs` 会在「远端也有 id 了」时提醒可以去掉该开关。\n⚠️ 代价:dev 用的这份比远端**少 65 条申报事项**(262 vs 327)。\n\n此前考察过的两个来源都不可用:apply_link 字段 262 条全空;资源申请备注里的 URL 只有 46 条有、其中 25 条是内网登录页,且同名政策下的 URL 是按申报事项分别对应的、不能互借。没有 id 时不给链接(不猜)。\n\n惠企政策 = policies_public.v1.json(262 条申报事项 / 38 个政策,index.html 异步预加载到 globalThis.policiesPublic)。判定两者结合:优先用接口字段 is_policy_library,缺失时回退到「标题能在库里找到」,标题匹配做了归一化(去《》全半角括号、连接符、空白)。\n\n⚠️ 两个入口**必须走同一个列表**(用户明确要求)。曾一度做成两套数据源(卡片「更多」看卡片、详情「返回」看库),是理解错了。两套数据源还有个连带问题:筛选状态跨入口共享,而库里部门是简称、卡片是全称,导致从库切回卡片时几乎筛不出东西(表现为「只剩一条」)。统一后消失。\n\n⚠️ 该值是 index.html **异步 fetch** 写入的,应用**不等待**它——读取务必**现读**,不要用 computed 缓存(globalThis 属性不是响应式依赖)。库未加载完时退回落卡片数据作为兜底。\n\n纯逻辑在 src/components/Chat/policy-library-utils.ts,tools 脚本引用同一份实现。\n\n⚠️ 「每次进入都重置筛选」(用户要求):进入列表时清空部门/分类/关键词并把页码归 1。此前筛选跨入口保留,是「列表只剩一条」的直接成因。重置会连带触发 watch([currentFilterOption, currentDeptFilter, searchKeyword]) 与 watch(currentPage),导致列表被重复构建——两次结果相同,可接受。"
- },
- {
- "id": "company-interrupt-status-matching",
- "priority": 50,
- "area": "chat",
- "title": "按 interrupt.status 判定补问形态(替掉猜测式判定)",
- "user_visible_behavior": "有候选时显示候选列表,没有候选时显示「上方输入框 + 跳过公司查询」。判定稳定,不受后端措辞变化影响。",
- "status": "passing",
- "verification": [
- "npm run build 通过",
- "四种情况逐一断言:company_selection+found / company_need+not_found / company_need+failed / company_need+无status",
- "专项核对:failed 文案不含「未查询到」;not_found 文案为「未查询到匹配的公司」",
- "去重回归:正文已含该段时问题字段仍为空",
- "真实后端:kind=company_need status=not_found → 输入框在上 + [跳过公司查询] + 问题字段已去重"
- ],
- "evidence": "真实后端 status=not_found 判定正确;四种情况断言全通过",
- "notes": "补问用 kind + status 描述,后端确认**四种情况都可能出现**:company_selection+found=有候选、company_need+not_found=未找到、company_need+**failed**=查询服务失败、company_need+**无 status**=初次询问是否需要公司信息。此前**完全没用这个字段**,而是靠 kind、candidates.length、以及用 input_help 中文文本做正则 /跳过|skip/i 猜按钮文案——最后一条尤其脆,后端改一个字就失效。现已改为 status 为权威信号,删掉正则嗅探。\n\n本轮修正两处错误:①failed 曾落进 not_found 分支,问题文案被填成「未查询到匹配的公司」,**把失败说成了查无**,违反接口文档「空候选与查询失败需分别展示」;现独立分支,说明失败原因(result.error 经 describeCompanyError 翻译)。②无 status(初次询问)曾给「跳过公司查询」,现改为「不需要公司信息」(→不需要),与该变体后端 input_help 一致。\n\n⚠️ 曾引入回归又修复:去重把 question 置空后,被后续的 `question || '未查询到匹配的公司。'` 兜底复活,等于去重失效;已抽出 fallbackQuestion() 统一处理。\n⚠️ failed 与「无 status」两种情况只有静态断言,未在真实后端遇到过(上游持续 provider_failure)。"
- },
- {
- "id": "company-need-input-on-top",
- "priority": 51,
- "area": "chat",
- "title": "公司未找到时:上方输入框补充信息 + 下方跳过公司查询",
- "user_visible_behavior": "公司未检索到时,卡片上方是一个常驻输入框(placeholder:请输入工商注册全名或统一社会信用代码),下方只有一个跳过选项;填入公司信息即可重新查询,或点跳过先按原问题回答。",
- "status": "passing",
- "verification": [
- "npm run build 通过",
- "真实后端两个 company_need 变体均验证:未检索到企业 → [跳过公司查询];询问是否需要公司信息 → [不需要公司信息]",
- "回归:有候选的 company_selection 行为不变(候选列表 + 查看更多 + 取消公司查询)",
- "提交映射:跳过公司查询→/skip、不需要公司信息→不需要、自由输入→原文"
- ],
- "evidence": "真实后端整链验证:卡片问题字段为空(已去重)、输入框在上、选项 [跳过公司查询]、正文确认已含该段;三项修复静态断言全通过(含反向用例)",
- "notes": "QuestionCard 新增两个可选字段:freeformInputOnTop(输入框常驻并排在最上)、freeformPlaceholder。原自由输入是选项之后的「其他」行、需点击才展开,无法满足「输入框在上」。改动同步修正 4 处自由输入判定(isFreeformSelected / isQuestionAnswered / canSubmit / handleSubmit),使常驻输入框无需点击激活即可被计入已作答与提交内容。\n\n后续修掉三个问题:①序号错乱——输入框在最上却拿 B、选项拿 A;新增 getCardOptionLabel(card, optIdx) 在 freeformInputOnTop 时整体后移一位,输入框取 A。②同一段文字重复——后端把同一段话既放进 answer.text 又放进 interrupt.question(实测完全相同);改为 buildQuestionCardsContent(interrupt, shownText),正文已含则不重复,模板问题行加 v-if 避免只剩“(单选)”。③跳过公司查询原来发 /skip,改为发选项文本「跳过公司查询」(后端 input_help 明确两种都收)。\n\n⚠️ 保留一处不一致:不需要公司信息 仍发 \"不需要\",因该变体后端 input_help 写的是“不需要则输入'不需要'”;若统一改发选项文本需先确认后端认哪个。\n⚠️ Question.message 字段从未被模板渲染,此前写的提示文案一直不可见(未改)。"
- },
- {
- "id": "company-query-error-states",
- "priority": 52,
- "area": "chat",
- "title": "公司补问区分「查询失败」与「查无公司」",
- "user_visible_behavior": "上游公司数据服务失败时,卡片明说「公司数据服务返回失败(provider_failure),暂时无法列出候选公司」并给出重试/取消;查无结果时说「未查询到匹配的公司」;正常时列出候选。",
- "status": "passing",
- "verification": [
- "npm run build 通过",
- "三种卡片状态的静态断言(查询失败 / 查无公司 / 正常有候选)",
- "真实后端端到端:用「上海星溯算力集团有限公司,有什么优惠政策」复现 provider_failure,确认修复后的卡片文案"
- ],
- "evidence": "真实后端返回「公司数据服务返回失败(provider_failure),暂时无法列出候选公司。」+ [重新查询][取消公司查询]",
- "notes": "🚨 修复的是公司补问**死循环**的真正根因。此前两次误判为「序号 vs 文本」问题,实际是:后端返回 candidates:[] + error:provider_failure(上游企查查查询失败),而卡片仍写「请选择您所指的公司」、不显示错误、允许自由输入并提示「输入新关键词」→ 用户只能手打公司名 → refine → 上游又失败 → 又弹同一张卡。接口文档明确要求「空候选与查询失败需分别展示」「有此错误时,不把空候选说成查无公司」,原实现两条都违反。新增 describeCompanyError() 翻译错误码(含 search:/basic:/honors: 阶段前缀,保留原码便于排查)。⚠️ 另有教训:排查必须读原始 SSE 载荷,不能只看封装的中间事件——适配层已把 interrupt/done 并入 message 通道,前两次测试脚本查的是不存在的事件,导致误判「后端不返回补问」。"
- },
- {
- "id": "questionnaire-submit-text",
- "priority": 55,
- "area": "chat",
- "title": "公司候选提交选项全部内容拼接成的文本",
- "user_visible_behavior": "公司候选卡片选项显示公司名称(详情为信用代码·状态·负责人);点击提交后接口收到的是选项全部内容拼接成的文本。",
- "status": "passing",
- "verification": [
- "npm run build 通过",
- "单元验证 toChatQuestionAnswer 的 10 项断言:按 label 反查候选重建完整文本、固定动作映射控制串、自由输入原样发送、多选与多题拼接、无补问数据时降级不崩"
- ],
- "evidence": "转换逻辑 10/10 断言通过;构建通过",
- "notes": "🚨 该条目历史:第一版提交序号(符合协议),被要求改为提交文字后**产生死循环**——提交公司名落进协议的“换关键词”路径,后端重新检索又返回同一批候选。根因:协议(api-chat.md)明确‘API 由序号映射到原候选’,文本这种输入形式被保留给 refine,语义上无法表达“我选这一个”。现按用户要求改为提交选项全部内容拼接文本。实现要点:QuestionCard 只回传 label 不回传 description,故协调器新增 lastInterruptPayload 留存补问数据,提交时按 label 反查候选重建文本。⚠️ 若后端仍按‘序号选中/文本则重新检索’处理,死循环会复现;届时的两条出路:①改回提交序号(序号取自当前显示顺序);②后端扩语义识别全字段文本。另修掉三个缺陷:原先只取 answers[0](多题丢失)、多选只取第一项、/^(\\d+)\\.\\s/ 规则误伤其它问卷。端到端未验证(排查时后端已完全不返回补问)。"
- },
- {
- "id": "dept-filter-restored",
- "priority": 60,
- "area": "policy",
- "title": "政策「更多」面板筛选还原为最初实现",
- "user_visible_behavior": "部门下拉仍是原来那 15 个固定部门,筛选按字面匹配。",
- "status": "passing",
- "verification": [
- "读 PolicyMatch.vue 断言 deptOptions 为固定的 15 个部门",
- "断言筛选比较为 String(item.source) === dept 的字面相等",
- "grep 确认无 matchDept / DEPT_KEYWORDS / normalizeDeptLabel 残留"
- ],
- "evidence": "已确认无残留,npm run build 通过",
- "notes": "曾先后尝试①从接口数据动态生成选项、②保留固定选项但加关键词归一化匹配,均被用户否掉。用户明确表态:暂时不做匹配,恢复成最初的。新接口返回的部门是全称(如青浦区科学技术委员会),与这 15 个简称字面不相等,因此选中部门会筛出空列表——这是按要求保持原逻辑的必然结果,不要自作主张去修。"
- },
- {
- "id": "file-upload",
- "priority": 70,
- "area": "upload",
- "title": "文件与图片上传",
- "user_visible_behavior": "用户能通过按钮 / 拖拽 / 粘贴上传图片与附件,上传后随提问一起发给模型。",
- "status": "not_started",
- "verification": [],
- "evidence": "",
- "notes": "阻塞原因(接口契约未定):新接口 /api/chat 请求体只接受 {thread_id, question},多字段返回 422,因此旧协议的 transmission.files / file_pos 发不出去。备选方案:①后端扩展请求体接受 files/file_pos;②前端把 OSS 地址拼进 question 文本;③后端提供文件登记接口返回 file_id。需用户先确认后端方案。原实现与两个接口的完整参数见 git 历史与 useBusinessAssistantUpload.ts(原代码已保留,入口按钮被注释)。"
- },
- {
- "id": "chat-markdown-format",
- "priority": 80,
- "area": "chat",
- "title": "回答的 markdown 分段",
- "user_visible_behavior": "多段内容正常分段,列表后面的说明行不被并进上一条列表项。",
- "status": "blocked",
- "verification": [],
- "evidence": "",
- "notes": "阻塞原因(方案未拍板):新后端 summary 文本混用单换行与空行分隔段落,而正文块是 marked + white-space: normal,markdown 下单个换行会被折叠,导致列表后的说明行被并进上一条列表项。前端可加归一化(非列表项之间的单换行提升为空行、连续列表项保持单换行),但需用户确认是否接受在适配层改动正文格式。前端渲染链路本身正常,证据:TextContent.vue 用 marked.parse,未做格式化转换。"
- }
- ]
- }
|