Няма описание

gongtianxiao c96e6ae43e fix(html): 修「界面卡在正在初始化」——上一轮删出的括号不配对 + 补内联脚本语法检查 преди 1 месец
build 08524fb44a chore: 初始提交——项目基线 преди 1 месец
harness c96e6ae43e fix(html): 修「界面卡在正在初始化」——上一轮删出的括号不配对 + 补内联脚本语法检查 преди 1 месец
public 08524fb44a chore: 初始提交——项目基线 преди 1 месец
src 446b03a8f8 chore: 删除走不到的卡片引导链;asr 移出 three-libs(用户要求) преди 1 месец
.editorconfig 08524fb44a chore: 初始提交——项目基线 преди 1 месец
.env.development 446b03a8f8 chore: 删除走不到的卡片引导链;asr 移出 three-libs(用户要求) преди 1 месец
.env.production 446b03a8f8 chore: 删除走不到的卡片引导链;asr 移出 three-libs(用户要求) преди 1 месец
.eslintignore 08524fb44a chore: 初始提交——项目基线 преди 1 месец
.eslintrc.js 08524fb44a chore: 初始提交——项目基线 преди 1 месец
.gitignore c5016ab820 feat(nls): 语音识别换阿里云 NLS —— 第 3 步:协议层 + 实现 + 真实网关验证 преди 1 месец
.prettierrc.cjs 08524fb44a chore: 初始提交——项目基线 преди 1 месец
CLAUDE.md 3e89dfef7a change(ui): 首页标题去掉「你好」;修 5 处记录与事实不符 преди 1 месец
README.md c96e6ae43e fix(html): 修「界面卡在正在初始化」——上一轮删出的括号不配对 + 补内联脚本语法检查 преди 1 месец
agents.md 3b8533df9e chore(harness): harness、CLAUDE.md、agents.md 纳入版本控制 преди 1 месец
index.html c96e6ae43e fix(html): 修「界面卡在正在初始化」——上一轮删出的括号不配对 + 补内联脚本语法检查 преди 1 месец
jest.config.js 08524fb44a chore: 初始提交——项目基线 преди 1 месец
package-lock.json 6783c6957b chore: 删除接口清点里的全部死代码(用户:「B 全部删掉」) преди 1 месец
package.json 6f15af69f4 chore(env): 环境只保留开发与生产(用户要求),删除 qingpu / test 两个模式 преди 1 месец
redirect.html 08524fb44a chore: 初始提交——项目基线 преди 1 месец
tsconfig.json 08524fb44a chore: 初始提交——项目基线 преди 1 месец
tsconfig.node.json 08524fb44a chore: 初始提交——项目基线 преди 1 месец
vite.config.ts 6783c6957b chore: 删除接口清点里的全部死代码(用户:「B 全部删掉」) преди 1 месец

README.md

zhaoshang-llm 改动记录

本文件当前只记录按日期的改动。项目概述、技术栈、目录结构、接口说明等内容, 等项目结束时再统一总结。

20260914

  • AI 对话迁移到接口 POST /api/chat(SSE)
    • 新增协议适配层 src/components/api-chat-coordinator.ts,把 SSE 事件翻译成既有渲染组件 可识别的内容标记,渲染层无需感知新协议
    • 请求体严格只发 {thread_id, question};answer 与 result.response 只展示一次; 未收到 done/error 即断流时按连接中断处理,不自动重试
    • 旧协议实现 src/components/stream-message-coordinator.ts 保留未用
  • 新增环境变量 VITE_CHAT_API,新增 dev 代理 /chat-api → http://192.168.2.23:8000
  • 政策卡片、政策详情、参考资料、公司补问与候选确认接入新协议
  • 错误码翻译:把 HTTP 与 SSE 错误码转成中文提示(如 thread_busy → 该问题还在处理中)

20260915

  • 修复回答内容顺序:正文 → 政策卡片 → 综合说明 → 参考资料(参考资料移到最下方)
  • 修复换行丢失:块级 markdown 下单个换行会被折叠,导致列表后紧跟的说明行被并进上一条列表项
  • 修复复制功能:复制结果剔除协议标记,不再把 <scope>、<!-- POLICY_TABLE -->、 <ref_links> 等原始标记一起复制出来
  • 修复卡片顺序与「更多」列表不一致:卡片改为按接口 priority.rank 排序
  • 「更多」面板的列表改为复用卡片数据,不再拉取与卡片无关的公共政策库
  • 部门筛选还原为最初的 15 个固定部门与字面匹配(此前一度改为按接口数据动态生成)
  • 新增惠企政策库来源标识(来自惠企政策库的卡片在右上角打标)
  • 政策详情面板新增「条件核验 / 匹配依据 / 原文引用」三个区块
  • 建立本机开发工作流文件与接口参考目录(按 .gitignore 约定不入库)

20260916

  • 打字机速度加快:每次打出字数 2 → 4(61 → 121 字符/秒),积压加速档位同步上调。 基准取主流 AI 的实测输出速度(Claude 3.5 约 60 字符/秒、DeepSeek-R1 约 75, 主流区间 50~120),取区间上沿——因为本项目后端一次返回整段,打字机是纯额外延迟, 同样速度会比主流慢一整段
  • 「政策事项列表」统一为一个列表、一个入口:
    • 卡片区「更多 >>」与详情的「返回政策事项列表」走同一条路径、内容相同
    • 列表数据统一来自 public/merchant-agent/policies_public.v1.json (262 条申报事项 / 38 个政策),本轮匹配到的惠企政策置顶
  • 「政策事项列表」每次进入都重置筛选(部门 / 分类 / 关键词 / 页码), 避免上次的筛选残留导致「条目少」或「筛不出东西」
  • 政策详情面板的「返回政策事项列表」按惠企政策区分:只有惠企政策才显示该入口; 惠企政策判定优先用接口字段 is_policy_library,缺失时回退到按标题在库中匹配 (已做归一化,容忍《》全半角括号等差异)
  • 公司补问改为按 status 字段判定形态(found / not_found / failed / 无 status), 替掉原先靠"候选数是否为 0"和 input_help 文案正则的猜测式判断
    • failed(查询服务失败)此前会落进 not_found 分支,文案被填成"未查询到匹配的公司", 等于把失败说成查无公司,已改为独立分支并说明失败原因
    • 无 status(初次询问是否需要公司信息)此前给"跳过公司查询",改为"不需要公司信息"
  • 补问卡片:输入框移到选项上方并常驻(原来在选项之后、需点击才展开)
  • 补问卡片:修复选项序号错乱——输入框在最上却拿 B、选项在下面却拿 A,现输入框取 A、选项顺延
  • 补问卡片:正文里已展示过的同一段文字不再在卡片内重复
  • 「跳过公司查询」由发送 /skip 改为发送选项文本「跳过公司查询」
  • 问卷提交:修复只取第一题(多题丢失)、多选只取第一项、以及数字开头选项被静默改写成纯数字

20260917

  • 修复切会话时误报「请求已取消」:
    • 根因是两处「内容是否为空」的判空口径不一致——归一化用 content.trim() (<scope> 进度块算作有内容),渲染层用 stripScopeBlocks() (进度块不算内容)。同一段内容,一处认为「有」、一处认为「没有」
    • 新协议的正文要等 done 才写入,处理期间内容只有进度块,把这个缝隙 从偶发放大成「一切会话就出现」:归一化认为有内容 → 不替换文案,却把状态翻成 Finish;渲染层认为没内容 → 显示「请求已取消」,而请求其实还在跑
    • 修法:抽出唯一的判空口径 hasVisibleMessageContent()(utils/interrupted-message.ts), 归一化与渲染层都改用它
    • 新增回归脚本 harness/tools/verify-empty-content-agreement.mjs, 断言的不变量就是「两处必须一致」,30 项断言
  • 删除政策详情面板的「原文引用」板块(条件核验、匹配依据保留)
  • 思考中卡片的阶段文案一律不换行:
    • 原因:ScopeContent 按卡片宽度切样式 —— 没撑满容器时标题 nowrap + 省略号, 撑满时切成 pre-wrap。而阶段文案长度不一(新协议的文案比原来的"正在思考中..."长得多), 切换时表现为「突然换行」且卡片高度跳动
    • 改法:拆开原本同时管标题与正文的规则 —— 正文保持可换行, 标题恒定 nowrap + 省略号
  • 政策详情面板的「政策名称」支持点击跳转:由库条目的 data.市级政策id 拼出 https://zwdt.sh.gov.cn/qykj/shell_oc_policy_zq/policy/policy-detail?id=<id>
    • 背景:库里 apply_link 字段 262 条全为空;资源申请备注 里的 URL 是按申报事项 零散对应、且混有内网地址(25 条指向 http://10.235.238.34:7202/imanage/login), 不能作为来源。市级政策id 有 261/262 条,可靠
    • 没有 id 时不给链接(不猜),宁可没有也不指向打不开的地址
  • 查明「政策名称」显示为纯文本的真因,并调整政策库数据源(开发环境改用本地 json):
    • 政策库其实有两个来源,字段并不一致:
    • 本地 public/merchant-agent/policies_public.v1.json:262 条,data.市级政策id 有 261 条
    • 远端 VITE_DOWNLOAD_URL:327 条,该字段一条都没有(它有的 policy_id 是 40 位、 与条目 id 相同,与 24 位的 市级政策id 是两套 id 体系)
    • 而 index.html 原本是远端优先(远端带 CORS 头,浏览器里确实会赢), 于是取不到 id → 拼不出详情地址 → 「政策名称」是纯文本。 此前怀疑的「HMR 状态陈旧」「走了兜底路径」均排除
    • 新增环境变量 VITE_POLICY_LOCAL_FIRST:仅开发环境打开(本地优先、远端兜底), 生产仍远端优先——生产该问题属后端数据缺失,需远端那份补上 data.市级政策id, 前端无法修
    • 新增 harness/tools/check-policy-sources.mjs:对比两个来源的条数与字段覆盖率, 并在「远端也带上 id 了」时提醒可以移除该开关
    • 代价:开发环境用的这份比远端少 65 条申报事项(262 vs 327)
  • 会话、问答记录与反馈落地到 DMS 数据管理服务(栏目 1887 助手会话 / 1889 助手问答记录)
    • 提交问题时写入会话(按 c_session_id upsert:没有就新增、有就更新)与问答记录(先写问题); 回答完成补写回答;停止/断流也补写已生成的内容,避免中断的问答只有问题没有回答
    • 会话列表、历史记录改为从 DMS 读取;会话改名、删除同步到 DMS
    • 点赞/点踩/取消写入 DMS 的问答记录(按 c_record_id 定位那一行)
    • 未登录的访客也写入,以 访客_<访客id> 归属(访客 id 复用埋点的那个)
    • 会话标识用前端的 session.id(它恒等于聊天协议的 thread_id);问答记录标识用 AI 消息 id(同时是反馈组件用的 record.id,因此从 DMS 读回的历史消息点赞仍能命中原始行)
    • token 只存在于 vite 代理侧(.env.development.local,按约定不入库), 浏览器产物里没有 token;DMS 不可用时静默降级,聊天与本地历史不受影响
    • 实测踩坑:DMS 的「删除」不是 delContentById(各种传法都返回参数错误), 而是 POST /content/updateAudit 把状态改成 4(销毁);addContent 会返回记录 uuid
    • 存储粒度:一个会话一行(不是一问一答一行)——整段对话的消息数组以 JSON 存进 该行,每次以本地消息列表为准整段重写;反馈记在 JSON 里对应那条消息上
  • 去掉开发环境硬编码账号的自动登录:此前开发环境下会无条件用内置账号登录, 表现为「没有登录也进入登录态」。现在默认即访客态,要企业态需显式带 ?access_token= 或 ?credit_code=
  • 本机工作流文件(harness/)结构对齐 Learn Harness Engineering 的 OpenAI 高级包: docs/plans/ → docs/exec-plans/(completed/ + tech-debt-tracker.md), 并新增结构校验脚本 harness/tools/validate-harness.mjs(机械约束优先于口头约定: 可检出假 passing、多个 in_progress、游离的计划文件等)
  • harness/、CLAUDE.md、agents.md 纳入版本控制(此前排除)
    • 依据:参考资料的原则是「计划、质量、技术债和代码一起版本化」—— 团队 clone 应当能看到状态、计划与契约文档,而不是只在本机
    • 仍然排除的是带凭据的本机文件(.env.*.local、.claude/settings.local.json), 以及 esbuild 生成的 harness/tools/_*.mjs(构建产物,入库会与源码漂移); 产物重新生成的命令写在 harness/tools/README.md
    • 入库前做了凭据扫描(无真实 token),并模拟 fresh clone(删光产物 → 按文档重新生成 → 四个验证脚本全绿)确认别人拿到仓库能跑通
  • 修复「同一政策的多个申报事项,点『查看详情』内容一样」:
    • 根因:同一条政策下的多个申报事项会各出一张卡片,但适配层把卡片的 title 与 declaration_item 都取自 card.name.text(只有政策名),而后端不下发 id/policy_id(实测恒为 undefined)——这些卡片在字段上完全无法区分, 原来的反查只要任一条件命中就返回,于是点第二张也跳到第一张
    • 修法:反查逻辑抽成纯函数 resolvePolicyDetailIndex(),对象同一性优先, 退化的字段比对只在唯一命中时采信,多命中时交给「直接展示被点条目」的兜底 —— 宁可走兜底,也不张冠李戴
    • 新增回归脚本 harness/tools/verify-policy-detail-index.mjs(10 项断言, 含「点第 2 张 → 下标 1」这条针对本 bug 的断言)
    • 详情面板的标题改为显示具体申报事项(如「…的通知 - 创业开办费补贴」): 适配层的 declaration_item 改用后端每条唯一的 data.title,不再与卡片标题同值; 卡片区标题仍是政策名,观感不变(含一处连带的 panelData.title 取值顺序修正—— 否则改详情标题会连带改掉卡片标题)
    • 新增 harness/tools/verify-policy-card-fields.mjs(8 项断言:同政策两条必须可区分、 卡片标题不变、无后缀时退化为政策名)
  • 卡片区「更多 >>」改为打开新的「政策列表」侧边栏:
    • 标题「政策列表」,保留搜索框与热门搜索,移除部门/分类筛选
    • 内容为本条回答里出现的政策卡片(去重、按匹配度排序) —— 注:2026-09-18 按需求调整,此前一度做成「本会话内所有消息出现过的卡片」 (跨消息聚合),那套聚合与它的采集函数已随本次调整删除
    • 详情面板的「返回政策事项列表」仍打开原来的惠企政策库列表(两套列表并存, 按要求只换「更多」入口)
    • 面板新增第三种模式 cardList,与旧列表共用同一套模板与样式类,未新增 CSS; 旧列表逻辑靠既有守卫自然短路,openPolicyList 一行未动
    • 新增 harness/tools/verify-policy-card-list.mjs(24 项断言:坏 JSON/空输入不抛错、 首见优先去重、降序+同分稳定、六面关键词过滤) —— 断言数由 31 降为 24:随「收回会话级聚合」删掉了那 7 项聚合断言
  • DMS 的问答记录存储粒度改回「一问一答一条」(幂等键 c_record_id, 问题与回答各存一半)。曾一度改成「一个会话一行、整段对话 JSON 塞进 c_answer」, 现按需求改回——更贴近库表原本语义(chat_record 即一行一轮问答),也便于按轮次检索统计。 反馈随之改为写在该轮的 c_feedback_* 列上;会话删除连带清理其名下所有问答行
  • 计划归档:harness/docs/exec-plans/completed/ 开始启用, 已实现的两个计划(DMS 落地、政策列表侧边栏)移入并更新状态; 结构校验脚本新增两条检查(active/ 里状态写着已完成、completed/ 里状态仍写进行中)
  • 按会话并行生成(改为 ChatGPT 桌面那种模型):
    • 此前全局只有一个协调器实例 + 一个全局 isGenerating,于是「同一时间只能一个会话在生成」, 并衍生出两个 bug:生成中切到别的会话,发送按钮仍显示「停止」(想发送却停了生成); 停止时目标会话解析错误(同步事件把它提前清空 → 回落到「当前会话」), 会把停止打在看的那条会话上、并凭空造出一条空的「请求已取消」消息
    • 现在生成状态按会话隔离(tasks Map + 每轮独立协调器实例 + 监听器闭包捕获会话 id), 各会话并行、互不干扰;停止只停自己那一轮;新建会话不再打断正在生成的会话
    • 顺带修好了两处静默失败:后端 409 等提示此前无人监听(现接到全局 Toast)、 同会话重复发送此前静默无反应(现提示「该会话已有回答在生成中」)
    • 新增回归脚本:verify-chat-task-utils.mjs(33 项,含把「响应式容器破坏对象同一性」 这个陷阱钉死的断言)、verify-coordinator-multi-instance.mjs (11 项,验证多实例事件隔离——并行化的地基)
  • 清理项目(用户要求):
    • 删掉 5 个无人读取的 env 变量(VITE_STREAM_SERVER / VITE_STREAM_TOKEN / VITE_HUMAN_IDLE_MP4_URL / VITE_PLUGIN_URL / VITE_PLUGIN_ASR)——数字人与插件那条 从未接入本项目前端的链路的遗留;其中两个还是入库的凭据
    • 合并 .env.development 里重复定义的 VITE_ASR / VITE_XF_ASR(后定义的才生效)
    • 删掉无代码使用的 vite /stream/ 代理(/asr/ 保留,是切讯飞 ASR 的退路)
    • 删掉空目录 public/images、public/pencil_exports 与构建分析产物 stats.html
  • 恢复上传图片/附件功能(此前因新协议只收两个字段而挂起):
    • 采用「把 OSS 地址拼进 question 文本」的方案(用户拍板,不需要后端配合)
    • 关键:sendMessage 拆成两个变量——发给后端的 questionForBackend(含附件地址)与 本地用的原文 text(气泡、会话标题、DMS 记录都不显示 URL)
    • 恢复「上传图片 / 上传附件」两个按钮(原先整块注释);拖拽与粘贴本来就是通的
    • 取签名的地址做成 env VITE_UPLOAD_SIGN_URL(四份 env 均写,值仍为厂商地址), 将来换自建签名服务只改配置
    • 实测:厂商签名接口 //qingpu-data-api.metamaker.cn/common/qp_signed_url 无需鉴权即可取签名; 文件实际存放在厂商的 OSS bucket heijing-products(杭州), 外链为 https://prod.heijingai.com/qingpu/<uuid>.<ext>
    • 新增回归脚本 harness/tools/verify-attachment-question.mjs(10 项断言)
  • 删除统计埋点的老接口(用户要求):
    • 删除调用点(BusinessAssistant.vue 的页面浏览、BusinessRecord.vue 的反馈统计) 与整个 src/network/api/assistant-statistics.ts,以及已无人使用的 VITE_ASSISTANT_STATISTICS_BASE_URL
    • 点赞由「双写」变为只写 DMS;运营侧的埋点数据从此不再上报
    • 只保留 getOrCreateAssistantVisitorId(DMS 的 访客_<id> 归属在用),搬进 DMS 模块, localStorage key 保持不变(改了会让已存在的访客换新 id)
  • 清理死代码(用户确认直接删除,非注释):
    • src/network/api/card/index.js 整文件删除(三个函数均无调用方) 及其未使用的 import
    • PolicyListUpdatedMatch.vue 有意保留——老会话历史里可能仍有 <policy-list-updated> 块,删了那些卡片列表会渲染不出来

20260918

  • DMS 写入范围收窄为只写问答记录:
    • 1887 助手会话不再写入(用户指示「只传具体的会话记录,另一个接口不传了」)—— 新建会话、改名都不再同步;会话列表退化为纯本地(与接 DMS 之前一致)
    • 1889 问答记录照常写入(含 title = 问题文本);删除会话仍清理其名下的 1889 记录
    • 调用点按约定注释保留并标 [已停用],同时在 verify-dms-payload-fields.mjs 里加了源码级断言(不得出现未注释的 upsertDmsSession 调用),防止被顺手改回
  • DMS 写入补齐 title 与运营分析字段:
    • 会话(1887):每次 upsert 都显式带 title = 会话标题 —— DMS 的系统字段 title 原本只在创建时初始化,改 c_title 不会带动它,所以前端改名后 DMS 里看到的标题不变
    • 问答记录(1889):title 用问题文本(原先库里是字符串 "null", 在 DMS 列表里看不出是哪一轮)
    • 写入带 title(用户需求);1887 另有四个 must=true 的运营字段 (summary / qpyszx / qyzt / rzqpyx)——不带就 214 数据错误,四个必须全带, 但只传空值/默认值(取值属业务口径,前端不猜;用户拍板「先传空值」)。 ⚠️ 只在新增时带、更新时一律不带——否则会把后端将来回填的值冲成空
    • 新增 harness/tools/verify-dms-payload-fields.mjs(9 项断言):打桩 fetch 抓真实请求体, 断言字段集合「不多不少」且 title 的值正确 —— 这条不需要 DMS token 就能跑
  • 环境变量整理:地址类配置收进 env,不再写死在代码里
    • 新增 VITE_CHAT_TARGET(聊天后端真实地址)——此前它硬编码在 vite.config.ts 的 代理配置里;现由 vite.config.ts 从 env 读取(保留同值兜底)
  • 移动端真机样式修复(用户反馈:夸克上第 3 段建议词被输入框遮住、vivo 上后两段被遮):
    • 按用户给的方案收紧首页元素:顶栏 64px → 52px、logo 130×88 → 96×65、 标题 28px → 22px 且恒定单行、建议词区内边距与间距收窄 —— 估算内容总高 −17%
    • 再按用户反馈调观感:标题与介绍文案两侧各留 28px、标题 22px → 20px、介绍文案 14px → 13px
    • ⚠️ 根因没动,只是让内容放得下:.home-fixed-input 仍是 position: fixed 悬浮, 给它预留的是写死的 padding-bottom: 180px(与输入框实际高度无关); 另有 height: 100dvh 无 100vh 兜底、页面无 text-size-adjust: 100% —— 见技术债第 7 条,换台手机仍可能变样
  • 首页标题文案:一度按指示去掉「你好」,随即按要求加回(仍为 你好,有什么我可以帮您的?)
  • 首页标题字号:几经反复,当前值以这里为准 —— PC 保持 32px 不变;移动端 22px(clamp(17px,5.7vw,22px),≥386px 的屏恒为 22px, 更窄的屏按比例收缩以免标题被顶出屏幕)。过程:一度整体 −4px(PC 32→28、移动端 20→16) → 用户反馈移动端「太小了」→ 全部还原 → 用户要求移动端调到 22px
  • 企业信息分类同步(对话带回 company_info → 分类接口 → DMS 企业两栏目):
    • 对话 result 事件里的 company_info 非空时,回答完成后自动同步到 DMS 两栏目: 1888 企业基础信息 ← 分类接口(POST /api/company/classify)的六个角度标签, 一行一家企业(幂等键 c_credit_code,字段级 diff,无差异零写入); 1886 企业荣誉信息 ← company_info.honors.records(逐条真实荣誉),一条荣誉一行
    • 1886 与库里该企业的行按「荣誉名+级别+来源」全量对齐(补新增、更新差异、删多余), 仅当 honors.complete === true 才删;荣誉名写 c_honor 与系统 title。 —— 数据源曾用分类接口的「资质荣誉」标签,实测宇树科技只给 2 个标签而真实荣誉有 31 条, 按用户要求改为以 honors.records 为准(分类标签仍写进 1888 的 c_tag_honor)
    • 全程 fire-and-forget:任何失败只 warn,不影响聊天;同企业并发走串行链
    • 适配层新增捕获 company_info(原先 result 分支是空的、数据被丢弃)
    • 回归脚本:verify-company-classify-sync.mjs(53 项纯逻辑)、 verify-company-info-passthrough.mjs(10 项,假 SSE 流验证适配层把 company_info 原样带出)、 verify-company-classify-dms.mjs(23 项打真实 DMS)、 verify-company-classify-e2e.mjs(16 项端到端:真实分类接口 + 真实 DMS)
    • ⚠️ 分类接口对「精简/合成」的请求体一律 422(连接口文档里自己的示例片段也被拒), 换成真实完整的 company_info 就返回 200 —— 是对载荷完整性的校验,不是接口坏了; 已在 api-chat.md 补上 company_info 的契约章节
    • DMS 侧字段已就绪(1888 扩到 47 字段、1886 扩到 12 字段,含 c_tag_* 分类标签字段); 两栏目都已有数据(DMS 侧 2026-09-17 灌入),所以同步多为「更新已存在的行」, 只动身份三字段与标签字段;1886 靠 c_source 标记所有权,绝不碰灌入的荣誉行
  • 移除移动端 @media (max-width: 360px) 里那条 .heading { font-size: 28px }: 它最初与基础值同值(空操作),09-18 基础值改小后变成窄屏把标题放大回 28px 的覆盖, 而标题是 white-space: nowrap —— 360px 屏下标题宽 356px、可用仅 272px,会溢出屏幕 84px; 窄屏收缩已由 .heading 自己的 clamp 负责(已注释保留原文并写明原因)

20260920

  • 企业信息同步扩充:company_info 里 DMS 有列的字段现在全部同步
    • 1888 企业基础信息:补上 36 个工商信息字段(企业名/经营状态/注册地址/注册资本/ 经营范围/成立日期/曾用名/行政区划…,逐字段映射、字段级 diff、无差异零写入)
    • 1886 企业荣誉信息:按后端 honors.records 逐条荣誉对齐(一条一行,c_honor 存荣誉名)
    • 三条实现要点:空值一律不写(不清存量)、JSON 列按语义比较(库里是 Python/jsonb 序列化,带空格且键按字母序,直接比字符串会每轮误判成"有差异"而反复写)、 工商与荣誉不依赖分类接口(分类接口慢且偶发失败,拆开后互不牵连)
    • 字段覆盖审计工具 harness/tools/audit-company-info-coverage.mjs:1888 写 43/47 列、 1886 写 12/13 列;未覆盖的 4 列(c_scope_brief/c_phone_number/c_email/c_industry) 后端接口本就没返回
  • 政策详情面板的「政策名称 / 政策所属部门」一行拉满整行宽度(与下方两张卡片右对齐): 原先名称列 max-width: 462px + 部门列写死 110px,整行只有约 520px 宽、右侧空一大块, 部门写全称时还被挤成三行;改为名称列占满剩余空间、部门列可伸展(下限仍 110px)
  • CIPA / 出海类问答展示企业微信二维码:后端在 result.data.cipa 给出布尔值, true(问的是 CIPA / 海外服务平台 / 出海服务)时在参考资料之前追加一张二维码图片 (src/assets/qiyeweixin.png,点击可放大)
    • 走适配层翻译成既有的 <image-scope> 内容标记,渲染层零改动(该标记本来就有对应行类型)
    • 复制按钮的剥离规则同步补上 <image-scope>,避免把图片地址复制进剪贴板
    • 适配层 15 项新断言(含 false/缺字段/非布尔值不显示、跨轮不串、补问轮不展示)
    • 位置:参考资料之前;补问轮(等用户补充公司信息/确认候选)不展示——那一轮还没有正文回答
  • 回答正文改为真实增量流式输出(后端 F073:answer_start / answer_delta / answer_end / answer_abort,契约见 harness/docs/reference/token-streaming.md)
    • 正文逐片段实时上屏,不再是"整段到达后逐字打字";思考中卡片在第一个字到达时即退场
    • 为此在适配层加了增量 markdown 规整器(src/components/answer-stream.ts): 正文块用的规整函数是插入空行的,若不做增量规整,answer_end 的"整块替换"会与已流文本 分歧 → 整段重建 → 思考中卡片每轮闪现重播。规整器保证「已上屏内容恒等于最终全文的逐步前缀」, 正常路径因此退化为纯尾缀追加(零抖动)
    • 实测(真实后端 505 片 delta / 876 字):concat(deltas) === answer_end.text 成立; answer 事件是正文开头的压缩版,流式轮跳过不重复渲染(仅当与正文公共前缀 < 90% 才保留)
    • 用户主动停止保留已流出文字;后端出错 / 断流撤掉未定稿的草稿
    • 旧后端(不下发流式事件)与 mock 测试按钮仍走原来的打字机(回归重点)
    • 新增 verify-answer-stream.mjs(78 项:纯逻辑 + 协调器级假流)、probe-answer-stream.mjs(真后端探针)

20260921

  • 卡片改为「正文开流前」上屏(对齐后端契约 F079):有入选政策时后端先发完完整的 source/item 才开始 answer_start,而前端此前一直等 done 才显示卡片 —— 只能靠整块重写把卡片插到正文之上, 界面上表现为「正文先渲一遍、又从头再渲一遍」。现在开流前就把卡片上屏,收尾只做纯追加
  • 修「同一段回答内容出现两次」:真载荷实测 answer_end.text(1610 字)比增量拼接(1309 字) 多 301 字,导致应用侧记录的"流式正文"不再是上屏内容的后缀 → 重排落兜底分支 → 正文上屏两份。 修法:answer_end 后同步"流式正文",并加固兜底(对不上时绝不重复插入正文)
  • 参考资料改为「正文定稿就上屏」:实测 done 要等 result(375~390KB 的完整快照), 比正文定稿晚 3~10 秒,而参考资料的数据(source 事件)在正文开流前就发完了 —— 等于白等。 现在正文一定稿就发;二维码(来自 result.cipa、只能等)走定向重排,保住「在参考资料之前」的版式
  • 修「文字快渲染完时视图被拽回回答顶部」:流式期间正文是即时渲染的,但最后一行一直被标成 "打字未完成",收尾时整套打字机机制被重新启用,其中一环会把视图滚到最后一条 AI 消息的顶部。 修法:收尾时把已在屏幕上的行补记为「打字完成」
  • 大块内容改为对齐它的顶部(不再滚过它):卡片/参考资料是一整块插入的(300~900px), 此前贴底滚动会一步滚过整块 —— 用户刚拿到卡片却看不到卡片
  • 「流式草稿被最终文本整段替换」记为已接受行为:契约要求「完整校验后绑定真实链接, 随最终替换出现」「不能用 text 覆盖…不能只保留草稿」。实测草稿与最终文本的公共前缀只有 11.7% (最终版给标题补了序号、把纯文本换成 markdown 链接)
  • DMS token 不用再每次手填:token 约 24 小时过期,以前要「改 .env + 重启 dev server」。 现在放在本机单行文件 .dms-token,代理每个请求现读(换 token 不用重启): npm run dms:token(从剪贴板读)、npm run dms:login(账号密码自动登录)、 npm run dev 前自动续期。顺带发现原来 .env.development.local 里的 token 写在了下一行, dotenv 读出来是空串 —— 等于一直没生效
  • 注释掉输入框右侧的测试用 MOCK 按钮组(资料/问卷/流测/回放/打字/Scope/测试输入)

20260922

  • 1887 助手会话写入企业相关字段(未登录也写):企业名称、统一社会信用代码、企业图片、是否登录
    • 规则:登录态的值优先级最高(库里已有值也覆盖);对话带出的 company_info 值与库里不同就更新; 但已被登录态认领过的行(c_is_login="1")不许被后来的访客对话改写 —— 否则会把这行改到别的公司名下
    • 企业看不到陌生访客的会话:读「我的会话」时排除访客行(改造前访客标识是随机的、本来也匹配不上, 这里只是保持一致的可见范围)
    • c_title 不写(标题由运营侧生成);DMS 系统字段 title 只在建行那一刻带上
  • 访客归属改用本机 IP:新增取 IP 接口(返回纯文本 IP;走同源代理 —— 它是 HTTP 而页面是 HTTPS, 直连会被浏览器按混合内容拦掉),IP 存进 c_ip,访客身份为 访客_<IP>; 取不到 IP 时退回浏览器随机 id,不影响任何写入
  • 复制回答去掉 markdown 语法符号:###、**、反引号、[文字](链接) 的括号语法、无序列表的 - 都不再复制出去;有序列表序号保留(顺序是内容)、链接地址保留(写成「文字(地址)」)
  • 排查中撞到一个 DMS 协议坑并记录:payload 里含模型里不存在的列时,addContent 会忽略该列照写, 而 updateContent 会静默丢弃整个更新(返回仍是 {"code":200,"message":"成功"})—— 新增列时必须先确认 DMS 侧已加

20260923

  • 多模态输入:新增 pictures / files 两个请求字段(后端扩展了 /api/chat 的请求体)
    • pictures:图片 URL 列表;files:非图片文件 URL 列表。两者都是字符串数组
    • 截图、文档上传后不再把 OSS 地址拼进问题文本,而是随请求体发出 —— 地址不再被模型当正文读,本地消息、会话标题、DMS 记录也不再被 URL 污染
    • 实测(打真实后端):元素必须是 URL 字符串,传对象数组会被 422 invalid_request 拒掉; 带上 pictures 问「这张图片里是什么」,模型确实描述了图的内容(不是收下就忽略); 空数组与不传都接受
    • 没有附件时不发这两个字段 —— 请求体与从前逐字节一致
  • 附件改为上传到 DMS 文件服务(用户 2026-09-23):图片/文件走 POST {DMS}/file/uploadFile(multipart,五个字段全必填:file / type / columnId / contentId / paraName), 返回相对路径(如 /static/photo/1889-c_question-<uuid>.png),它随请求体发给后端; 原先直传厂商 OSS 的实现整段停用保留
    • 缩略图预览:相对路径本身打不开,拼上 DMS 代理前缀才能取(实测 200), 所以刷新页面后气泡里的图还在
    • 实测细节:type 决定目录(0 图 → /static/photo/,其余 → /static/file/); 少任何一个字段都返回 {"code":-1,"message":"缺少必传字段"}
  • 附件支持格式对齐产品提示,且不支持的格式改为 toast 提示(用户 2026-09-23):
    • 图片:JPEG、PNG、GIF、WebP;文件:Excel(xls/xlsx)、DOCX、文字 PDF (此前图片漏了 GIF,文件多了 doc/txt/md 却缺 Excel —— 与提示对不上)
    • 不支持的格式:弹 toast 提示 + 不放进输入框(此前是 window.alert; 拖拽路径还会把不支持的文件静默丢弃,用户不知道文件为什么没进去)
    • 顺带修掉一个真 bug:图片校验此前是 file.type.startsWith('image/'), 等于任何 image/* 都放行 —— SVG / BMP / HEIC 全被当图片收下;现改为按扩展名判
  • 全局提示改成 Ant Design 的 message(用户 2026-09-23:「现在的 toast 太丑了, 希望是图片中的类型,从顶部弹出,过一段时间消失」):
    • 原来的 <Toast> 是全屏黑遮罩 + 居中白框(本质是模态框的观感,还会挡住整页)
    • 现在统一走 @/utils/app-message(Ant Design Vue 4.x 的 message,本项目已有该依赖): 顶部弹出、3 秒自动消失、错误为粉底红感叹号 —— 就是设计稿那种小条
    • 调用方签名不变(showToast(text));顺带发现 BusinessRecord.vue 里那份 <Toast> 与 showToast 没有任何调用点、也没有子组件 inject(死代码),一并停用
  • 语音输入的 4 处 window.alert 一并换成轻提示(用户 2026-09-23:「另外那几处语音的 window.alert 一同更换」):PC 1 处(初始化失败)+ Mobile 3 处(麦克风权限 ×2、初始化失败 ×1)
    • 换成 showToast(...) 后与全站提示同一条链路(顶部弹出、自动消失), 不再有阻塞式弹窗;文案与收尾动作一字未动(仍会释放语音实例、复位状态)
    • PC 端 openMicroPhone 返回 false 时仍是静默 return(无任何提示)—— 与 Mobile 不一致,属已报告的待定项,未擅自补文案
    • 保留项:清空输入框那句 window.confirm("可能会清空之前的文字…") 不换 —— 它是要用户做决定的模态确认,不是轻提示
  • 轻提示时长 3s → 2s(工作区改动,随本次一并提交;改 APP_MESSAGE_DURATION 一处即可)
  • 修:提交之后气泡里看不到图片(用户 2026-09-23 报):
    • 根因:附件换成 DMS 之后,上传返回的是相对路径(/static/photo/…), 而消息附件仍把相对路径当展示地址用 —— <img src="/static/photo/…"> 经 dev server 取回的是 index.html(200 但 text/html),图解不出来。 实测对照:相对路径 → text/html;/dms-api + 相对路径 → image/png ✓
    • 修法:消息附件按上传模块同款分成两个字段 —— url(可显示地址,气泡里 <img> 用)与 fileUrl(相对路径,随请求体 pictures/files 发给后端), toChatAttachmentUrls 改取 fileUrl(发给后端的地址与从前一致,不受影响)
    • 非图片附件没有展示地址(气泡里本来就是「文件条」,只显示名字/大小)
    • 回归脚本 39 → 51/51(新增【7】12 项钉死这个契约,并用变异测试确认两个方向都会红)
  • 附件路径随问答记录落 DMS(c_image / c_file)(用户 2026-09-23:在「助手问答记录」里 新建了这两个字段,要求把对应的路径存进去):
    • 存的是相对路径(/static/photo/…)—— 与请求体 pictures/files 里发给后端的是 同一个值,库里记的和后端收到的能对上账;不存 /dms-api 展示地址(那是环境相关的拼接结果)
    • 多张图/多个文件用逗号分隔;没有附件的那一轮不写这两列
    • 写入前先拉模型定义确认两列真的存在 —— DMS 的坑:updateContent 遇到模型里没有的列会 静默丢弃整个 patch(返回仍是 200「成功」),所以更新路径只在「真有附件值」时才捎上这两列, 最坏也只影响带附件的轮次,不会连累补 answer
    • 验证:verify-dms-chat-storage.mjs 65 → 78/78(打真实 DMS 的写入-读回-补 answer 全链路)
  • 只要对话就在「助手会话」(1887)建行(用户 2026-09-23,推翻 2026-09-18 那条 「访客且没有企业信息就不建行」):
    • 未登录且本轮没带回企业资料时,行里放能填的那些:c_ip(真实 IP)、c_is_login="0"、 时间戳、系统 title,以及四个必填运营位(summary 空串 + 三个布尔 false,只做空值兜底、 不猜业务值);c_credit_code 留空(用户拍板:这一列只放真实信用代码,不写访客标签)
    • 建行时机不变:仍只在回答正常完成(totalResponse)时写一次 —— 中断/报错的轮次不建行
    • 运营分析回填列(c_title、c_description、c_focus_points、c_core_needs、 c_contact_clues、c_follow_up_departments、c_settlement_intent_level、jygjbm 等) 前端一律不碰,否则会把分析系统的值冲掉
    • 读侧不受影响:远端会话列表本来就排除 c_is_login="0" 的访客行,且只在登录后拉取
    • 验证:verify-dms-chat-storage.mjs 78 → 89/89(打真实 DMS 新增 11 项,含"后续轮次带回 企业信息时这个纯访客行能正常被填上");变异测试注入旧行为 → 断言大面积失败
  • 语音识别换阿里云 NLS(第 1~2 步:token 通路)(用户 2026-09-23):NLS 的 token 要用 AccessKeySecret 签名去换(CreateToken,24h 过期),而 secret 不能进前端产物 —— 所以走 dev 端点 GET /nls-token/(vite.config.ts 的中间件,apply:'serve'), 由 vite 在 node 侧签名并把 {token, expireAt, appkey} 交给前端;凭据只写在 .env.development.local(gitignored,值不进仓库任何文件)。
    • 签名口径收在 harness/tools/nls-token.mjs(唯一一份),命令行工具 harness/tools/get-nls-token.mjs 与 dev 端点共用;带 5 分钟提前刷新 + 并发去重
    • 验证:端点 200;拿端点发的 token 真调通一次识别(status:20000000 SUCCESS); npm run build 后产物里搜三个凭据值 0 命中
  • 修:界面卡在「正在初始化,请稍候...」(2026-09-24,我自己上一轮删出来的回归): 删 initToken 时在 index.html 的内联脚本里留了个不配对的 } → 脚本语法错误 → hasSequence 没置位、sequenceLoaded 没派发 → 加载遮罩永不隐藏。 构建没抓到,因为 Vite 不解析 index.html 里的普通 <script>(原样拷贝); 已修,并新增 harness/tools/verify-inline-scripts.mjs(编译 dist/*.html 的内联脚本 + 报出未替换的 %VITE_*% 占位符,变异测试确认会红),同时写进 sops/verification.md: 改了 index.html / redirect.html 必须跑它
  • 删掉走不到的卡片引导链 + asr 搬家(用户 2026-09-24):
    • index.html 的 initToken() / initCardInfo() / else 分支:两个环境都走不到 (isKnowledgeApi 恒 true),且它们的产物(globalThis.token / cardInfo)全仓库无读者; 那段本身还是坏的(调用从未挂到 globalThis 的 changeLoadingText/showReloadView,走到会抛)。 一并删掉随之孤儿的 randomString / changeLoadingText / showReloadView 与 VITE_APP_KEY(产物核对:app_key / openid_to_token 0 命中)
    • src/three-libs/asr/ → src/asr/,three-libs/ 目录删除(名字有误导性,且只剩 asr 一个子目录); import 同步为 @/asr/nls,验证脚本新增断言钉住新位置
  • 环境只保留开发与生产(用户 2026-09-24):删掉 qingpu / test 两个构建模式 —— .env.qingpu / .env.test 删除前,先把它俩独有的配置合并进 .env.production: VITE_DOWNLOAD_URL="/api/download/" 与 VITE_REDIRECT_MAP(后者 production 原本没有, 直接删会让 redirect.html 的产物变成 JS 语法错误 —— 顺带修掉了:以前 npm run build 打出来的生产包里那个页面就是坏的)
    • 脚本:删 build:qingpu / build:test,保留 zip,新增 npm run deploy:qingpu(= build + zip)
    • index.html 里两处本来就是死的 qingpu 分支一并删掉(isQingpuEnv 写的是字面量 "MODE"、 永远 false 且无人读;isLanMode 赋值后从未使用)
    • 另记一个待清理项(未动):initToken()(openid_to_token + VITE_APP_KEY)现在两个 环境都走不到(唯一为 false 的 test 环境已删),删它前要先理清与 initCardInfo() 的关系
  • 死代码清理(用户 2026-09-24「B 全部删掉」,清单见 harness/docs/reference/current-api-call-sites.md):
    • 删掉:厂商 OSS 上传的停用实现(含 VITE_UPLOAD_SIGN_URL)、旧厂商 ASR 全套 (three-libs/asr/index.ts + VITE_ASR / VITE_XF_ASR + vite /asr/ 代理 + index.html 的 globalThis.isXF)、stream-message-coordinator.ts 整个类(~1100 行)
    • 随之孤儿的 utils/scope-stream-boundary.ts、updateSessionInfo、 getModelServerUrl / isKnowledgeApi、三个空目录、两处停用注释块, 以及因此变成孤儿的 npm 依赖 qs / @microsoft/fetch-event-source
    • MessageState 枚举是活的,已从被删的协调器搬到 src/types/record.ts(取值未动)
    • 教训记档:删文件要按导出的符号名反查引用(只查 import 路径会漏 —— components/utils.ts 因此被误删,构建报错才发现,已恢复)
  • 实时语音识别已切到阿里云 NLS(2026-09-23,接管旧厂商 SDK):
    • 协议层 src/asr/nls-protocol.ts(纯函数:帧构建 / 事件折叠 / 16k PCM 编码)
    • 类 src/three-libs/asr/nls.ts,对外契约与旧 SDK 一致 → 两个壳组件只改了 import 与一处构造参数(去掉硬编码 appKey),其余一行没动
    • 协议是实测出来的:StartTranscription→TranscriptionStarted→二进制音频帧→ TranscriptionResultChanged(中间结果,会被覆盖)→SentenceEnd(整句定稿)→TranscriptionCompleted; 两条硬约束——每条指令都要带 appkey、task_id 不能是空串(各踩过一次,已写成断言)
    • 拼接全文只用 SentenceEnd;中间结果只用于实时显示
    • 验证:harness/tools/verify-nls-realtime.mjs 39/39,含真实识别(Windows TTS 造语音 → 推流给真实网关 → 识别结果与所说话一致)与切线的源码级断言
    • 重采样按实际 audioContext.sampleRate 算(旧实现写死 44100 的隐患不再沿用)
    • 文档:harness/docs/reference/asr-nls.md(旧的 asr-realtime.md 标注为"换之前那套")
    • ⚠️ 生产要由后端/网关做同样形态的 /nls-token/ 端点(已登记技术债 #10); 浏览器实测由用户确认(harness 不能替真人说话)
  • 实时语音转文字补了一份接口文档(用户 2026-09-23 要求): harness/docs/reference/asr-realtime.md —— SDK 在仓库里(src/three-libs/asr/index.ts),文档逐条对源码核对:构造参数与方法、 transform/error 事件契约、底层 WebSocket 协议(/common/asr_hub?project=qingpu&engine=aliyun_dashscope…、 首帧、16k/16bit 单声道 PCM 的二进制帧、结束帧、服务端 start/stop → mid/fin/breath 的映射)、 两端的生命周期与调用点、以及6 条已知坑(采样率换算写死 44100、"权限被拒绝"文案不可达、 openMicroPhone 只会 throw 或返回 true、非讯飞分支不主动关 socket、硬编码的十年期 JWT、 clear() 会清空所有监听)。只是文档,没改任何代码。
  • 🔴 待处理(登记进技术债 #9):.env.production / .env.qingpu / .env.test 都没有配 VITE_DMS_API —— 这些环境打出来的包里附件传不上去(直接抛「未配置 DMS 接口地址」)、 DMS 会话落库也静默跳过。接生产要先定:DMS 从部署环境是否可达、token 由谁注入 (开发环境是 vite 代理加的,生产得 nginx 做同样的事)