architecture.md 12 KB

架构说明

本文是项目的架构事实来源。根 CLAUDE.md 只做简短入口,深层规则都在这里。

项目定位

青浦区营商智能助手前端。Vue 3 + TypeScript + Vite,面向企业的政策匹配与办事指引: 用户自然语言提问,助手检索政策知识库,以「开场概述 + 政策卡片 + 综合说明 + 参考资料」 的形式给出有原文依据的回答;涉及具体企业时先引导用户确认公司主体,再结合工商信息作答。

容易误判的几件事

先看这几条,可以省掉不少弯路:

  • 对话走接口 POST /api/chat(SSE)。前端不直接渲染接口协议,而是经适配层 src/components/api-chat-coordinator.ts 翻译成既有渲染组件可识别的内容标记。 详见 reference/api-chat.md。
  • 没有 3D 虚拟人渲染。three 仅用于粒子背景(business-assistant-particle.ts); 语音识别在 src/asr/(nls.ts 采集与连接 + nls-protocol.ts 纯函数协议层), PC 与移动端都 import ASR from '@/asr/nls',点麦克风就走它。 现行实现(阿里云 NLS 直连,2026-09-23 起)见 reference/asr-nls.md; 换之前那套(厂商 asr_hub 中转)的记录见 reference/asr-realtime.md。 > 目录沿革:原先在 src/three-libs/asr/。2026-09-24 用户要求把 asr 移出来、删掉 three-libs/ > —— 那个目录名有误导性(与 three.js 无关),且 2026-09-24 清理后它只剩 asr 一个子目录。
  • 没有状态管理库(无 Vuex / Pinia)。对话状态在 useBusinessAssistantChat 组合式函数内, 会话与消息持久化到 localStorage。
  • 没有 src/hooks/ 目录;也没有 stream-message-coordinator-v2.ts。

技术栈

项 说明
框架 Vue 3(<script setup>)+ TypeScript + Vite 5
UI Ant Design Vue 4(unplugin-vue-components 按需自动引入)
状态 无状态管理库;组合式函数 + localStorage
流式 原生 fetch + ReadableStream(现行);@microsoft/fetch-event-source(旧协议,保留未用)
Markdown marked
其他 Three.js 0.143.0(粒子背景)、alloyfinger(语音相关)

主入口与页面壳

  • src/main.ts — 应用入口
  • src/App.vue — 根组件
  • src/components/BusinessAssistant.vue — 只做 PC / 移动端切换,不是主壳
  • src/components/BusinessAssistantPC.vue / BusinessAssistantMobile.vue — 真正的页面主壳 (输入区、消息列表、滚动与打字机联动都在这里)

目录结构

src/
├── components/
│   ├── Chat/                    消息渲染、政策卡片、补问问卷等
│   ├── Common/                  通用组件(MediaViewer、ScrollList、Toast…)
│   ├── business-assistant/      对话编排(useBusinessAssistantChat、上传、滚动等)
│   ├── api-chat-coordinator.ts  ★ 协议适配层(现行)
│   └── stream-message-coordinator.ts  旧协议实现(保留未用,勿改)
├── network/api/                 接口封装(chat-sessions、enterprise、assistant-statistics、card)
├── types/                       TypeScript 类型定义
├── utils/                       工具(runtime-config、stream-xml-filter、scope-record-rows…)
└── asr/                         语音识别(NLS:nls.ts + nls-protocol.ts)

核心系统

对话协议适配层(现行)

src/components/api-chat-coordinator.ts —— 接口 POST /api/chat 的客户端。 把 SSE 事件翻译成内容标记交给既有渲染组件:

接口事件 翻译为 由谁渲染
progress / heartbeat <scope title="阶段文字"> ScopeContent("思考中"卡片)
item <!-- POLICY_TABLE {"data":[…]} --> PolicyMatch(政策卡片 + 详情面板)
source <ref_links>[…]</ref_links> 参考资料 chips + 侧边面板
interrupt <question-cards>{…}</question-cards> QuestionCard(补问问卷)
answer / summary 纯文本 TextContent(markdown)

渲染层不需要感知新协议。 这是刻意设计:协议会变,渲染层不动。

旧协议实现(保留未用)

src/components/stream-message-coordinator.ts。当前未被使用,不要改动。

关键约束

接口层

  1. 请求体只接受 {thread_id, question} 两个字段,多一个返回 422。 旧协议的 transmission.files / file_pos 发不出去(这是"文件上传"功能待定的原因)。
  2. answer.text 与 result.response 内容相同,只能展示一次(result 是快照)。
  3. 未收到 done/error 即断流 → 按连接中断处理,不自动重试。
  4. 不要用后端文案做判断。 补问形态由 kind + status 字段决定, 而不是拿 input_help 的中文措辞做正则 —— 后端改一个字判定就会失效(踩过)。

界面

  • 界面样式必须与原版一致。 新协议能力通过适配层翻译成内容标记来复用既有渲染组件, 不要新建带自有样式的 UI 组件。
  • 既有 UI 的可选项、文案、筛选逻辑不要擅自改动 —— 确需变更先取得确认。

开发环境

  • dev server 为 HTTPS(自签证书),浏览器首次访问会有证书告警,属正常现象
  • 四个后端的接口都走同源代理(见 vite.config.ts;代理由服务端发起,所以局域网同事访问也不会有混合内容问题): /oauth/** → 121.43.55.7:10086 · /dms/** → 121.43.55.7:10081 · /api/** → 121.43.55.7:10011(剥前缀)· /chat-api/** → 聊天后端(剥前缀)
  • 生产环境 vite 代理不生效,需由 nginx 做等价转发(含 DMS 的 token 注入)
  • .vue 的 <script> 里不要出现裸的 HTML 注释起始符(那四个字符): Vite 的依赖预扫描把 .vue 也当 HTML,扫描前先执行 replace(/<!--.*?-->/gs, "<!---->") —— 一个不成对的起始符会和后面任意一个结束符(哪怕隔着几十行)配成"HTML 注释"整段吃掉, 冷扫描随即报 Failed to scan for dependencies … Unexpected "."(2026-09-28 踩过)。 标记类字面量统一放 .ts(如 stream-xml-filter.ts 的 POLICY_TABLE_START / POLICY_TABLE_END), 组件里 import;改完用 node harness/tools/verify-vue-script-html-comments.mjs 自查 (<template> 里的常规注释不受影响,脚本只查 <script> 区块)

环境变量

2026-09-28 按代码重核过一遍(此前这张表列着已删的 VITE_MODEL_SERVER / VITE_ASR / VITE_STREAM_SERVER,且缺了一半现役变量)。改 env 时请同步更新本表。

变量 用途 定义在 谁在读 / 备注
VITE_VERSION 页面标题里的版本号 两份 index.html 的 %VITE_VERSION%
VITE_API 主业务网关(企业登录 / 企业信息,老接口) 两份 enterprise.ts:51,83、两个壳组件的 apiBaseUrl
VITE_POLICY_REMOTE_URL 惠企政策库远端完整 URL —— 已删:改到 public/config.js 的 dms.policyRemotePath(填 uploadFile 返回的相对路径;DMS 会改名,不能写死 policies_public.v2.json)。取不到再本地兜底 — policy-library-loader.ts
VITE_PARENT_ORIGIN 嵌 iframe 时父页面的 origin(postMessage 目标) —— ✅ 2026-09-29 已删:用户确认本项目是独立页面、不再嵌入任何父页面 ⇒ index.html 里的 initMessageChannel()(监听父页面消息 + 回发 ready)整套删除;localStorage.initMessage 也无读者 — —
VITE_CHAT_API 对话后端(SSE)。服务前缀(/chat-api、http://host:8000)会自动补 /api/chat,完整地址则原样用 两份 api-chat-coordinator.ts;企业分类也走它(company-classification.ts)
VITE_DMS_API DMS 数据服务(会话/问答/反馈/企业信息/政策库文件)。值是同源前缀 /dms 两份 ✅ dms/client.ts、dms/file-upload.ts、policy-library-loader.ts;token 由前端自己带
public/config.js → __ZHAOSHANG_CONFIG__.dms 运行时 DMS 栏目/模型 id + 内网双 c_ 开关(部署后可改、无需重建) 随包 index.html 先引入;dms/client.ts 读栏目;fieldDoubleCPrefix 时出入网编解码 c_* ↔ c_c_*
VITE_OAUTH_LOGIN_URL OAuth 同源前缀(默认 /oauth);登录路径 /api/user/login 由代码拼接 两份 dms/service-token.ts;生产需 nginx 直通 /oauth/
VITE_GATEWAY_API 后端网关同源前缀(取 IP + NLS token;默认 /api;代码再拼 /proxy/verify/…)。不是企业业务网关 VITE_API 两份 client-ip.ts、nls-token.ts;nginx/vite 剥前缀 → 网关
VITE_IP_API 旧名 —— 已改名为 VITE_GATEWAY_API(2026-09-29) — —
VITE_NLS_TOKEN_API 语音 token 完整路径 —— 已删:与取 IP 共用 VITE_GATEWAY_API,路径由 getNlsTokenApiUrl() 拼接 — —
VITE_NLS_APPKEY NLS 的 appkey(不是密钥)。2026-09-28 起已内置进代码(runtime-config.ts 的 NLS_APPKEY_BUILTIN),本变量降级为"可选覆盖" 可不出现在任何 env 语音识别请求要带;globalThis 注入 → env → 内置值 的覆盖链
VITE_REDIRECT_MAP redirect.html 跳转映射 —— ✅ 2026-09-29 已删(政策申报中转与本助手无关;登录回跳后续自行对接) — —

📌 代理前缀方案(2026-09-29 用户指定):前缀尽量与服务真实路径一致,只有服务端本身不带前缀时才剥: /oauth/**(→10086,直通)· /dms/**(→10081,直通,并注入 token)· /api/**(→10011,剥掉 /api:网关真实路径是 /proxy/verify/…)· /chat-api/**(→聊天后端,剥前缀)。 合并前是 /dms-api(补 /dms)+ /ip-api + /aly-api(后两条打的是同一台 10011)。

📌 代理 target 直接写在 vite.config.ts 里,不走环境变量(用户 2026-09-29 明确要求 「vite.config.ts 里面不要加环境变量」)—— 该文件已不 loadEnv,四个 target 就是四行字面量。 换地址=改那四行,改完重启 dev server。 .env.* 只管产物里用到的值(VITE_DMS_API 等同源前缀)、以及注入用的 globalThis。

⚠️ DMS 的 token:2026-09-29 起由「前端自己换」(用户要求) —— 未登录访客用默认账号 (src/network/api/dms/service-token.ts 里内置)经 POST /oauth/api/user/login 换一个 JWT, 缓存在内存里,随 DMS 请求带 token 头;代理(dev 的 vite / 生产的 nginx)只在请求没带时才注入。 📌 该账号是用户专门准备的「默认账号」(用户 2026-09-29 确认:已处理过、就是给这个用途的)。 仍存在的两个代价:① 密码在公开产物里(要收紧就改密码并同步改两处代码); ② token 落在浏览器(能打开站点的人都能拿到一个 DMS 令牌)。 技术债 #8(后端按调用者身份做行级授权)仍是"生产接入前必须处理"。

📌 .dms-token 保留(用户 2026-09-29 决定):前端已不依赖它,但它仍是命令行工具 经 dev 代理读写 DMS 的凭据缓存(predev 会自动续期)。要彻底不落盘需要改的三件事, 记在 harness/tools/dms-token.mjs 头部注释里。

⚠️ 生产没有 vite 代理:server.proxy 只对 npm run dev 生效,而 npm run preview 会复用同一份(容易误判"生产已通")—— 生产侧必须由 nginx 做等价转发。

🧹 2026-09-28 顺带清掉的:VITE_DOWNLOAD_URL(无读者,见 reference/current-api-call-sites.md)、 runtime-config.ts 里无人调用的 getApiBaseUrl()(它读的 globalThis.VITE_API 从来没被赋值过)。