tech-debt-tracker.md 19 KB

技术债

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

未还

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

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

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

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

4.(2026-09-24 已还)部门筛选在新数据下筛不出结果

还清了:用户 2026-09-24 明确要求「部门筛选只包含 JSON 里面存在的所有部门」—— 前端不再用那份硬编码的部门列表,改为从惠企政策库(政策所属部门)现算 (打开部门菜单/面板时算一次;选中项若不在新清单里自动退回「全部部门」)。 于是"固定列表 vs 数据取值口径不一致"这个根因消失了:清单里不会再有库里没有的部门, 也不会漏掉库里新增的部门。验证见 verify-policy-library.mjs 第 7 节(35/35)。

⚠️ 早先"不要当 bug 修"的告诫已被这条用户指示取代 —— 别再按老口径改回固定列表。

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

5. Question.message 字段未被渲染

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

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 只放行前端真正用到的端点且强制按调用者身份改写查询条件 —— 实践上很难做对,不建议。
  • 现状:未处理,生产接入前必须解决。

9. 生产 / 青浦构建没配 DMS:附件传不上去、DMS 落库静默跳过

  • 是什么:.env.development 里有 VITE_DMS_API="/dms-api" + VITE_DMS_TARGET, 而 .env.production 也没有(2026-09-24 起只剩开发/生产两份 env,见 progress.md 续 38)。 这些环境构建出的包里 getDmsApiBaseUrl() 返回空串,后果两条: ①uploadDmsAttachment 直接抛「未配置 DMS 接口地址,无法上传附件」→ 附件功能在这些环境整体不可用; ②DMS 会话/问答/反馈请求走 client.ts 的「未配置就跳过」,静默不落库。 (构建产物可自证:dist/index.html 里 globalThis.VITE_DMS_API = "%VITE_DMS_API%" 原样留着。)
  • 为什么先欠着:这两处是 2026-09-23 才接进来的(附件换 DMS、气泡图片修复), 当时只在开发环境验证过;生产怎么接需要部署侧的决定,不是改一行 env 就完。
  • 欠着会怎样:部署到青浦后,用户传图片会弹「未配置 DMS 接口地址」, 且所有会话/问答记录不落 DMS(看起来像"功能没了")。
  • 该还的条件 / 要先拍板的两件事: ①DMS 地址(http://121.43.55.7:10081)从部署环境是否可达; ②token 谁注入 —— 开发环境是 vite 代理在转发时加 token 头(浏览器不持有 token, 见 vite.config.ts 的 /dms-api/),生产要由 nginx(或后端网关)做同样的事, 否则 DMS 会因鉴权失败而拒;顺带 /dms-api 还要把 /dms 前缀补回去。 ③index.html 的 %VITE_DMS_API% 支持运行时注入(globalThis.VITE_DMS_API 优先), 所以也可以不改构建、只改部署产物里的这一行——但同样绕不开 ②。
  • 当前状态:仅登记,未处理。需要用户决定生产怎么接。

10.(2026-09-24 已大半还清)生产的 NLS token 端点

进展:用户 2026-09-24 给了后端网关的 token 端点 GET http://121.43.55.7:10011/proxy/verify/getALYToken → {code:200, content:"<token>"}, 前端已改为调它(同源 /aly-api/…,dev 走 vite 代理)。前端侧不再需要 AK/SK, vite 里那段自签中间件已删除。 剩下的(部署侧,仍未做): ① nginx 把 /aly-api/ 转发到 http://121.43.55.7:10011/(去掉前缀,与 /ip-api/ 同法); ② 生产 env 要配 VITE_NLS_APPKEY(实测:.env.production 都没有 → 生产构建打出来的包里 appkey 为空 → 识别会失败)。 详见 ../reference/asr-nls.md。

原来的记录(2026-09-23) - **是什么**:语音识别要换成阿里云 NLS,而 NLS 的 token 必须用 **AccessKeySecret** 签名去换 (`CreateToken`,24 小时过期)。secret **不能进前端产物**(bundle 是公开的), 所以 `vite.config.ts` 里加了一个 **dev-only** 的端点 `GET /nls-token/` (`apply: 'serve'`,返回 `{token, expireAt, appkey}`)。 - **为什么先欠着**:开发期够用了(前端零凭据,凭据在 `.env.development.local`), 但生产没这个端点 —— 打包产物里没有、也不该有任何换 token 的能力。 - **欠着会怎样**:**生产环境语音识别拿不到 token,功能不可用**(前端会拿到 404/503)。 - **该还的条件**:后端/网关提供**同样形态**的端点:前端仍是同源 `GET /nls-token/` → `{token, expireAt, appkey}`,secret 留在服务端。 这样前端的代码一行都不用改,只是 dev 与生产的实现者不同。 - **当前状态**:dev 已通(端点 200、token 能真调通识别、构建产物零凭据泄漏); 生产未做,**需要用户/后端拍板由谁实现**。

已还

语音识别已换阿里云 NLS(2026-09-24 已还)

  • 换了什么:语音识别从厂商 metamaker 的 asr_hub 中转,换成阿里云 NLS 直连 (协议与实现见 ../reference/asr-nls.md)。
  • 当初欠的两点都解决了:① 厂商 token 硬编码(exp 2034-12-29)已随旧实现删除; ② token 改由后端网关 /proxy/verify/getALYToken 签发,前端不持有任何云凭据。
  • 旧实现已按用户要求删除(2026-09-24),要看它原来怎么工作查 git 历史。
当初的欠账记录 ### 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`](../reference/current-api-call-sites.md) 的「ASR 协议契约」, 实现方照着就能写自建网关,**前端一行都不用改**(只把 `VITE_ASR` 指向新地址)。 - **一个低成本的前置动作(尚未做)**:把 `index.html` 里写死的 `globalThis.isXF = false` **改成环境变量开关** —— 这样万一厂商出问题,改配置就能切到讯飞分支,不必改代码重新发版。

访客行会被企业看到(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 可改造成此用途)。
  • 若要根治:得后端在流式阶段就带上最终链接、或减少降级 —— 属后端侧决定。