tech-debt-tracker.md 13 KB

技术债

记在这里的债都是有意欠的,不是忘了。每条写清:是什么、为什么先欠着、 什么条件下该还。

未还

1. 公司候选提交的是文本而非序号(协议标注的做法)

  • 是什么:接口文档写的是"选择第 1 家公司提交 question="1",API 由序号映射到原候选", 而当前前端提交的是选项全部内容拼接的文本(公司名 + 信用代码 + 状态 + 负责人)。
  • 为什么先欠着:这是产品明确要求的形态。文本在语义上无法表达"我选这一个", 后端可能把它当成"换关键词"走 refine 路径重新检索。
  • 欠着会怎样:可能出现"选一次又弹回同一批候选"的死循环。
  • 该还的条件:上游公司数据服务恢复后必须补测 —— 走一遍有候选(status: found)的流程, 选中一家,确认返回的是选定公司后的结果而不是又弹候选列表。 若循环,两条路:①改回提交序号(序号取自当前显示顺序);②后端扩语义识别全字段文本。
  • 当前状态:上游持续 provider_failure,status: found 一次都没出现过,因此从未验证。

2. answer.text 与 summary.text 首句重复

  • 是什么:后端把同一段话既放进 answer.text(渲染在消息正文)又放进 summary.text 的开头, 实测两者首句逐字相同,用户会看到同一段话出现两次。
  • 为什么先欠着:已确认是后端内容重复,不是前端渲染问题(前端对两个字段各渲染一次)。
  • 欠着会怎样:回答开头重复一段,观感差。
  • 该还的条件:后端修(推荐,最干净),或前端加去重(权宜:比对首句并剥离, 但属启发式判断,可能误删)。需用户拍板走哪条。

4. 部门筛选在新数据下筛不出结果

  • 是什么:政策「更多」面板的部门下拉是固定的最初 15 个部门, 按字面相等匹配;而接口返回的部门是全称(如"青浦区科学技术委员会"),字面不相等。
  • 为什么先欠着:用户明确要求保持最初的实现,暂时不做匹配。
  • ⚠️ 不要当 bug 修:曾试过①按接口数据动态生成选项、②保留固定选项但加关键词归一化匹配, 两个方案都被否掉。
  • 该还的条件:用户明确要求时再动。

5. Question.message 字段未被渲染

  • 是什么:QuestionCard 的 Question 接口里定义了 message 字段, 但模板从未渲染它 —— 适配层写的提示文案一直不可见。
  • 为什么先欠着:只影响观感,不影响功能;改模板会扩大改动面。
  • 该还的条件:需要展示这类提示时。

6. 语音识别仍跑在厂商(metamaker)的服务上

  • 是什么:语音输入走 wss://qingpu-data-api.metamaker.cn/common/asr_hub (引擎 aliyun_dashscope),且鉴权 token 与 appKey 都硬编码在源码里 (src/three-libs/asr/index.ts 的 token:iss: human-large-screen,exp 2034-12-29,入库可见)。
  • 为什么先欠着:用户 2026-09-18 明确「先不动,先用原来的语音识别功能」—— 现在能用、无成本、前端零维护;独立的成本主要在「要有人长期维护一个 ASR 网关 + 引擎账号」。
  • 欠着会怎样:⚠️ 厂商若失效那条 token、或停掉那个网关,语音功能会突然不可用,且没有任何预警; 另外用户语音音频流经厂商服务器(政务场景下涉及数据合规评估)。
  • 该还的条件:出现以下任一情况就该动 —— ①厂商停服/改价/要求换凭据;②合规要求音频不出自己的服务器; ③要换引擎。退路已经准备好:协议契约已存档在 ../reference/current-api-call-sites.md 的「ASR 协议契约」, 实现方照着就能写自建网关,前端一行都不用改(只把 VITE_ASR 指向新地址)。
  • 一个低成本的前置动作(尚未做):把 index.html 里写死的 globalThis.isXF = false 改成环境变量开关 —— 这样万一厂商出问题,改配置就能切到讯飞分支,不必改代码重新发版。

7. 移动端真机样式问题:没有兼容基线,且布局依赖 JS 测量

  • 是什么:用户反馈真机上(F12 模拟正常)①首页展示不全 ②思考过程文案超出屏幕 ③不同浏览器问题还不一样。排查后确认三个相互独立的根因:
    1. height: 100dvh 没有 100vh 兜底(BusinessAssistantMobile.vue)—— dvh 是较新的单位(Chrome 108+ / Safari 15.4+),国产浏览器内核多基于更老的版本 → 该声明被整条丢弃 → 容器高度塌陷,而外层是 overflow: hidden → 内容被裁掉
    2. 没有 text-size-adjust: 100% —— 移动端浏览器会按各自策略自动放大文字, 各家不同 → 同一份文案在不同手机上长度不同(也是"不同浏览器不一样"的直接来源)
    3. 思考过程卡片的样式靠 JS 测量 + width: max-content 决定「单行省略 vs 换行」: updateMaxWidthReached() 用 getBoundingClientRect() 比卡片与父容器宽度。 而字体首选 "HarmonyOS Sans SC"(手机多无此字体 → 回退到系统字体)→ 字宽不同 → 测量结果不同 → 该换行时判成"没撑满" → 单行 nowrap 撑出屏幕
  • 为什么先欠着:需用户提供信息/确认改法(见下),且其中一条会改变观感
  • 欠着会怎样:移动端(本项目的主要使用场景)体验不稳定,且换台手机就可能变样
  • 该还的条件:
    1. 用户提供具体浏览器与版本、以及能否真机远程调试(Android 用 chrome://inspect 最省事)—— 没有这个只能猜
    2. 方案 A(低风险、可立刻做):dvh 加 vh 兜底、全局 text-size-adjust: 100%、 给 flex 链补 min-width: 0
    3. 方案 B(建议做,但要用户确认观感变化):去掉「按宽度切样式」这套测量, 改为纯 CSS —— 标题恒定单行省略、正文恒定可换行,删掉 is-max-width-reached 与 width: max-content。现在这套先天不稳定(字体/时机/内核都会影响测量)
    4. 方案 C(机制):定浏览器兼容基线 + 加 browserslist; 项目当前没有任何 browserslist / postcss 降级配置,而代码里用了 dvh(4) / gap(151) / inset(22) / backdrop-filter(24) 等较新特性
  • 当前进度(2026-09-18):已按用户自己的方案给首页布局"瘦身" (顶栏 64→52px、logo 130×88→96×65、标题 28→22px 且恒定单行、建议词区间距收窄, 随后再加两侧留白、标题 22→20px、介绍文案 14→13px),用户真机确认"不遮了"。 ⚠️ 但这只是让内容放得下——上面三个根因一个都没动(dvh 仍无 vh 兜底、 仍无 text-size-adjust、思考卡片仍靠 JS 测量切样式,padding-bottom: 180px 也仍是写死的)。 换台手机、或用户调大系统字号,仍可能变样;方案 A/B/C 依然有效,等用户决定要不要做。

8. DMS 代理注入全权限 token:站点可访问 = DMS 数据可读(部署前必须处理)

  • 是什么:前端所有 DMS 请求都走 /dms-api/ 代理,token 由代理服务端注入 (dev 是 vite.config.ts,生产计划是 nginx 同款转发)。这个 token 不区分调用者 —— 任何能打开站点的人,在浏览器 devtools 里对 /dms-api/content/selectContentList 发任意查询, 就能读到 DMS 的全部数据:1887 会话、1888 企业工商信息、1889 问答记录, 包括别家企业的。代理只做「补 /dms 前缀 + 注入 token」,不做任何鉴权。
  • 为什么先欠着:还在联调阶段、生产尚未接入(.env.production 没配 VITE_DMS_API)。 dev 环境无所谓(本机、只有自己)。
  • 欠着会怎样:生产一旦按现方案接上,DMS 数据等同公开 —— 含企业工商信息与全部问答记录。 这与「访客 / 登录」无关,是部署层面的暴露,登录态也挡不住(token 是共享的)。
  • 该还的条件(两条路):
    1. 推荐:后端提供 DMS 转发接口,按调用者身份(登录企业 / 访客 id)做行级授权, 前端不再直连 DMS —— 与 docs/plans/active/dms-api-replacement.md 里"前端直连还是后端转发" 那道待拍板的问题是同一件事,这里给出了必须选后端的理由;
    2. 临时缓解:nginx 只放行前端真正用到的端点且强制按调用者身份改写查询条件 —— 实践上很难做对,不建议。
  • 现状:未处理,生产接入前必须解决。

已还

访客行会被企业看到(2026-09-22 已还)

  • 当时的问题:1887 口径改为「c_credit_code 只放真实信用代码」后,访客行也带着 对话提到的那家公司的代码;而 fetchDmsSessions(读「我的会话」)按 c_credit_code 查、 且只在登录后调用 → 那家公司登录后会看到陌生访客的会话。
  • 怎么还的:fetchDmsSessions 增加一道过滤 —— 排除「明确是访客」的行 (c_is_login === "0")。只排除显式访客行(c_is_login 缺失的历史行照旧返回), 因此可见范围与改造前完全一致。
  • 同时修掉一个更隐蔽的:登录认领过的行(c_is_login === "1")不再允许访客对话改写 它的公司字段 —— 否则退登后再问到别的公司,会把这行改到那家公司名下、而 c_is_login 仍是 "1",既串台又会被读侧当登录行返回。
  • 验证:verify-dms-chat-storage.mjs 打真实 DMS,两个方向都断言(访客行查不到 / 登录行查得到),且实测「拿掉过滤该断言立刻失败」——是真守卫。

文件上传 / 多模态输入(2026-09-23 已还)

  • 当时的问题:接口请求体只接受 {thread_id, question},多一个字段 422, 旧协议的 transmission.files / file_pos 发不出去 —— 上传入口虽然活着, 但附件只能把 OSS 地址拼进 question 文本(方案②),模型会把 URL 当正文读, 本地消息、会话标题、DMS 记录也都跟着被 URL 污染。
  • 怎么还的:后端扩展了请求体(方案①),新增两个多模态字段:
    • pictures: string[] —— 图片 URL 列表(后端自己去取图,须公网可达)
    • files: string[] —— 非图片文件 URL 列表
    • 实测:元素必须是 URL 字符串(对象数组 [{url,name}] 会被 422 invalid_request 拒掉); 带上 pictures 问「这张图片里是什么」,模型确实描述出了图的内容(不是收下就忽略); 空数组与不传都接受。
  • 前端改动:toChatAttachmentUrls() 按 isImage 拆成两个列表随请求体发出; 地址不再拼进 question。旧函数 buildQuestionWithAttachments 按约定注释保留。
  • 不变量(有断言守着):没有附件时请求体只有 thread_id + question —— 既不多传无意义字段,也让「后端若回滚」只影响带附件的那几轮。
  • 验证:harness/tools/verify-attachment-question.mjs 19/19(拆分纯逻辑 + 真正发出的 请求体 + 「旧函数无活跃调用点」的源码级断言);字段生效是打真实后端探针实测的。

已知且已接受的行为(不是债,勿当 bug 修)

A1. 流式草稿会被最终文本整段替换

  • 现象(用户 2026-09-21 报):卡片下方先渲染出一大段正文,随后整段消失, 变成另一段(可能是更短的降级答复)。
  • 为什么会这样(契约要求,不是渲染 bug):
    • token-streaming.md:「完整校验后绑定真实链接,随最终替换出现」; 「answer_end 无论 status 是 completed 还是 fallback,均用 text 覆盖已有内容…… 不能只停止光标而保留草稿」
    • 实测复现(同一问法):草稿 1494 字 → 最终 1943 字,公共前缀只有 175 字(11.7%)—— 最终版给标题补了序号(### 高新技术企业认定… → ### 一、高新技术企业认定…)、 把纯文本渠道换成了 markdown 链接(- 国家高新技术企业认定管理工作网先行注册… → - [查看办事指南](https://zwdt.sh.gov.cn/…))。链接只在最终校验后才绑定,草稿必然没有。
    • status: fallback 那一类更直接:草稿被判不可用 → 换成降级答复 (「已查到相关资料,但这次未能完成答复整理。您可以查看下方原文资料,或稍后重试。」)
  • 客户端能做的只有缓解、改不了「内容会变」:曾评估过 ①加提示 ②渲染层保留公共前缀行只重建 分歧点之后 —— 实测公共前缀仅 11.7%,②收益很小。用户 2026-09-21 决定接受现状。
  • 复核方式:抓一轮 /api/chat 原始载荷,比对 concat(answer_delta) 与 answer_end.text 的公共前缀长度(harness/tools/probe-answer-tail-timing.mjs 可改造成此用途)。
  • 若要根治:得后端在流式阶段就带上最终链接、或减少降级 —— 属后端侧决定。