本文是项目的架构事实来源。根
CLAUDE.md只做简短入口,深层规则都在这里。
青浦区营商智能助手前端。Vue 3 + TypeScript + Vite,面向企业的政策匹配与办事指引: 用户自然语言提问,助手检索政策知识库,以「开场概述 + 政策卡片 + 综合说明 + 参考资料」 的形式给出有原文依据的回答;涉及具体企业时先引导用户确认公司主体,再结合工商信息作答。
先看这几条,可以省掉不少弯路:
POST /api/chat(SSE)。前端不直接渲染接口协议,而是经适配层
src/components/api-chat-coordinator.ts 翻译成既有渲染组件可识别的内容标记。
详见 reference/api-chat.md。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 一个子目录。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。当前未被使用,不要改动。
{thread_id, question} 两个字段,多一个返回 422。
旧协议的 transmission.files / file_pos 发不出去(这是"文件上传"功能待定的原因)。answer.text 与 result.response 内容相同,只能展示一次(result 是快照)。done/error 即断流 → 按连接中断处理,不自动重试。kind + status 字段决定,
而不是拿 input_help 的中文措辞做正则 —— 后端改一个字判定就会失效(踩过)。vite.config.ts;代理由服务端发起,所以局域网同事访问也不会有混合内容问题):
/oauth/** → 121.43.55.7:10086 · /dms/** → 121.43.55.7:10081 ·
/api/** → 121.43.55.7:10011(剥前缀)· /chat-api/** → 聊天后端(剥前缀).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 |
public/config.js 的 dms.policyRemotePath(填 uploadFile 返回的相对路径;DMS 会改名,不能写死 policies_public.v2.json)。取不到再本地兜底 |
— | policy-library-loader.ts |
VITE_PARENT_ORIGIN |
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 |
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 用户指定):前缀尽量与服务真实路径一致,只有服务端本身不带前缀时才剥:
/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从来没被赋值过)。