# 第四轮审计整改 · mky-vent-base(M1/M2/M3 重放 + R4-2/R4-4/R4-5) ## ① 头部 - **对照网址**:`http://39.97.59.228:8013/hrx/mky-vent-base/compare/master...fix/audit-r4-20260929` - **依据文档**: - `tools/verify/代码审核-第四轮-mky-vent-base-20260929.md`(§1 R4-1/R4-2/R4-4/R4-5,§2 M1/M2/M3 重放落点) - `tools/verify/第四轮-施工任务书-20260929.md` 第二章(工作令) - `AGENTS.md` §七(§7.2 基座口径)/§九(改动说明随分支提交) - **分支**:`fix/audit-r4-20260929`(worktree `/root/VentAnaly60/.wt/mky-r4`) - **基线**:`upstream/master` = `46df977e`(团队 AI 助手文件层面重构后的新结构) - **日期**:2026-09-29(Asia/Shanghai) - **交付版本**:以分支 `git log` 为准(**不写死自指 hash**) - **状态**:已交付,已推送,**已 merge 更新后的 master 并通过全量构建**(2026-09-30,见 §⑨);未部署,未浏览器实测。 ## ② 提交清单 一项一提交,提交信息均带编号(以 `git log 46df977e..HEAD` 为准): | # | 提交 | 内容 | |---|---|---| | 1 | `[Fix R4-M1] …` | 点选解读 SSE 补 `X-Access-Token`(重放 fix/base-agent-token M1,保留令牌可选语义) | | 2 | `[Fix R4-M2] …` | `throwStreamHttpError` 加 export;点选链路复用它替换裸 `throw new Error`;`formatClickError`(重放 M2) | | 3 | `[Fix R4-M3] …` | `/ventAI/api/me` + `getMe()` + 按 `is_admin` 隐藏技能/子智能体/思考级别入口(重放 M3) | | 4 | `[Fix R4-R4-4] …` | 删除 `Modal/src/index.less` 与 `antCss.less` 双份定义的关闭按钮宽度规则 | | 5 | `[Fix R4-R4-2] …` | `extractMarkdownLinks` 加 `MD_LINK_CACHE_MAX=200` LRU | | 6 | `[Fix R4-R4-5] …` | `ConversationNav` 滚动跟随缓存 wrappers 列表 | | 7 | `[Fix R4-R4-7/8] …` | `markdownSafe` 注释旧路径更正;`useChatScroll` 卸载时 `cancelAnimationFrame` | | 8 | `[Docs] …` | 本说明 + `docs/ai-changes/README.md` | | 9 | `[Fix R4-R4-5] …(复核 FR-1 收口)` | 缓存失效源补 `props.messages.length`(删 AI 消息场景)+ 注释/文档同步 | | 10 | `[Merge] …` | 合并上游 `origin/master`(5 个新提交,见 §⑨):3 处冲突解决 + 复验 | ## ③ 改动面 | 文件 | 改了什么 | |---|---| | `src/views/ventAI/dataPicker/api.ts` | M1:import `getToken`;`ssePost` headers 增 `'X-Access-Token': getToken()`。M2:import `throwStreamHttpError`;`!response.ok` 分支改 `await throwStreamHttpError(response)` | | `src/views/ventAI/dataPicker/useDataPicker.ts` | M2:import `StreamError`;新增 `formatClickError(e)`(429 带 retryAfter→分钟、413/404 兜底文案);catch 分支改 `error.value = formatClickError(e)` | | `src/views/ventAI/manageAssistent/api/client.ts` | M2:`throwStreamHttpError` 加 `export`(唯一硬阻塞点,函数体零改动)。M3:Api 枚举加 `me = '/ventAI/api/me'`;新增 `MeInfo` 接口与 `getMe()`(`errorMessageMode: 'none'`) | | `src/views/ventAI/manageAssistent/components/AiAssistantModal.vue` | M3:import `getMe`;新增 `isAdmin` ref(默认 false);新增 `fetchMe()`(失败置 false 不缓存);visible watch 的 fetch 组加 `fetchMe()`;`TaskListPanel`/`ChatInputArea` 各透传 `:isAdmin="isAdmin"` | | `src/views/ventAI/manageAssistent/components/panels/TaskListPanel.vue` | M3:props 加 `isAdmin?: boolean`;`optionItems` 由普通数组转 computed,按 `!isAdmin` 过滤 `skill`/`sub-agents` | | `src/views/ventAI/manageAssistent/components/chat/ChatInputArea.vue` | M3:props 加 `isAdmin?: boolean`(默认 false);`ThinkLevelPopup` **连同前面的 `.action-divider`** 包 `v-if="isAdmin"`。未改 `ThinkLevelPopup` 组件内部 | | `src/components/Modal/src/index.less` | R4-4:删除与 `antCss.less` 重复的 `.zxm-modal-close-x` 96px 规则块;保留本块独有的 `font-weight/outline` | | `src/views/ventAI/manageAssistent/lib/markdown.ts` | R4-2:`extractMarkdownLinks` 加 `MD_LINK_CACHE_MAX=200` FIFO LRU(与 `renderMarkdownCached` 同型) | | `src/views/ventAI/manageAssistent/components/chat/ConversationNav.vue` | R4-5:新增 `getWrappers()` 缓存(失效源=`qaPairs.length` + `props.messages.length` + O(1) `container.contains` 存活校验),`updateActivePairByScroll`/`scrollToPair` 改用它,rAF 合并不变 | | `src/utils/markdownSafe.ts` | R4-7(顺手):头部注释的旧路径 `chatModal/utils.ts` 更正为 `lib/markdown.ts`(纯注释) | | `src/views/ventAI/manageAssistent/hooks/useChatScroll.ts` | R4-8(顺手):`onBeforeUnmount` 时 `cancelAnimationFrame(scrollRafId)` | **冻结面零触碰**:`permissionGuard.ts` / `useAutoLogin.ts` / mock-login 相关、`.env`、`public/js/config.js` 均未改(`git diff 46df977e..HEAD --name-only` 已核,无命中)。 ## ④ 逐项说明 ### M1 点选解读 SSE 补 `X-Access-Token`(重放自我方 fix/base-agent-token M1) - **原来的行为**:`dataPicker/api.ts` 的 `ssePost` 只带 `Content-Type`,是基座唯一一条匿名调 TfAgents 的路径;消耗记到兜底账号 click-anonymous,无法按登录人记账。 - **现在的行为**:headers 增 `'X-Access-Token': getToken()`,与对话链路(`client.ts:217/266/317`)同名同源。 - **为什么这么改**:恢复按登录人记账归因(TfAgents 决策 D5 / B1-20)。 - **与原来的差别**:仅多一个请求头。**令牌是「可选」语义:空令牌照发请求**(TfAgents 有 click-anonymous 兜底),**未加任何「未登录拦截/跳登录」判断**——这是用户已定的 D5 决策,若改成强制登录会把未登录场景直接变成不可用。 - 注意:新 master 已把 SSE 读流收敛为 `consumeSse`(`api/sse.ts`),本项只补头,**没有**把旧分支的手写 SSE 循环带回来。 ### M2 复用 `throwStreamHttpError`/`StreamError`,429 变可读文案(重放 M2) - **原来的行为**:`!response.ok` 时裸 `throw new Error('HTTP错误: 429')`;`useDataPicker.ts` 一行 `e?.message || '请求失败'`,用户看不懂 429/413/404。 - **现在的行为**: 1. `client.ts:70` 的 `throwStreamHttpError` 加 `export`(新结构导致的**唯一硬阻塞点**,函数体零改动;`StreamError` 本就导出); 2. `dataPicker/api.ts` 改 `await throwStreamHttpError(response)`——解析响应体 `detail`(字符串或 `{code,message,retry_after,limit_tokens,spent_tokens}`),抛出带 `status/retryAfter` 的 `StreamError`; 3. `useDataPicker.ts` 新增 `formatClickError(e)`:429 带 `retryAfter` 换算分钟(不足 1 分钟显示「不到 1 分钟」);413 服务端无文案时兜底「附件体积超过上限…」;404 兜底「会话不存在或无权访问」;普通 Error 一律走原 fallback,行为不回归。 - **为什么这么改**:TfAgents 已返回结构化错误体(D6/B1-15/B1-19/B1-08),且与对话链路共用一套解析器,避免两份实现各自漂移。 - **与原来的差别**:错误展示从 `HTTP错误: xxx` 变为可读文案;其余链路(consumeSse 流处理、成功路径)不变。 ### M3 `/ventAI/api/me` + `getMe()` + 按 `is_admin` 隐藏全局变更入口(重放 M3) - **原来的行为**:所有角色都能看到并点击「技能」「子智能体」管理入口与「思考级别」切换;非管理员点了才被后端 403。 - **现在的行为**: 1. `client.ts`:Api 枚举加 `me`;新增 `MeInfo` + `getMe()`(走 defHttp,axios 拦截器自动带令牌;`errorMessageMode:'none'` 静默失败,防接口未上线时每次开弹框都弹红错)。 2. `AiAssistantModal.vue`:新增 `isAdmin` ref(默认 false)与 `fetchMe()`;visible watch(`immediate: true`)的 fetch 组每次打开都调 `fetchMe()`;向 `TaskListPanel`、`ChatInputArea` 透传 `:isAdmin`。 3. `TaskListPanel.vue`:`optionItems` 转 computed,`!isAdmin` 时过滤 `skill`/`sub-agents`;「新建任务/定时任务」保持开放(属个人功能)。 4. `ChatInputArea.vue`:`ThinkLevelPopup` **连同前面的 `.action-divider`** 一起包 `v-if="isAdmin"`(不连分隔线会残留竖线);思考级别的**读取**(getModelMsg)不受影响,对所有人可用。 - **为什么这么改**:TfAgents 已把这三类全局变更端点收归管理员(用户定案方案 B:后端返回 is_admin,前端隐藏入口)。 - **与原来的差别**:仅入口可见性收敛;**`fetchMe` 失败置 `isAdmin=false` 且不缓存**(每次打开重新拉取,管理员名单可能在服务端变更)——这是历史浏览器验收踩过的坑(失败停留 false 导致管理员看不到入口),已原样保留。**未改 `ThinkLevelPopup` 组件内部**(可能被别处复用)。旧分支的 `adminConfigured` ref 在模板中从未消费(死状态),本次未搬运。 ### R4-4 关闭按钮规则去重 - **原来的行为**:`Modal/src/index.less:77-87` 与 `design/vent/antCss.less:201-209` 双份定义同一条 `.zxm-modal-close-x { 96px }` 规则(f4a47a3c 为修生产错位写了两遍),加载顺序不同会互相覆盖。 - **现在的行为**:删组件层重复块,保留 `antCss.less` 一份(其注释说明了用后代选择器提升优先级的意图);`index.less` 该块独有的 `font-weight: normal; outline: none;` 保留。 - **为什么这么改**:单一事实源,避免将来改一处漏一处。 - **与原来的差别**:编译产物 CSS 少一份同规则;选择器与值不变,渲染行为等价(less 编译实测见 §⑥)。 ### R4-2 `extractMarkdownLinks` 加 LRU - **原来的行为**:每次调用都 `marked.lexer` 全量解析;流式期间胶囊 computed 每个 token 全量重扫所有 AI 消息,CPU 随会话长度线性放大。 - **现在的行为**:加 `MD_LINK_CACHE_MAX=200` 的 FIFO Map(与 `renderMarkdownCached` 同型),以原文为 key;唯一消费方 `lib/capsule.ts:61` 只读遍历,返回数组被缓存共享安全。 - **为什么这么改 / 差别**:流式期每 token 的链接提取从 O(全部 AI 消息全文) 降为 O(1)(未变化消息全部命中);缓存有上限,内存代价 ≤ 几十 KB。 ### R4-5 滚动跟随 wrappers 缓存 - **原来的行为**:`updateActivePairByScroll` 在滚动 rAF 回调里每帧 `querySelectorAll('.message-wrapper')` 全量查询(>100 条时每帧 O(n))。 - **现在的行为**:查询结果缓存为数组,失效源=**「消息对数量 + 消息条数」**(`watch([qaPairs.length, props.messages.length])`);另加 O(1) 存活校验(`container.contains` 首个 wrapper),防 DOM 整批替换(如切换任务同问答对数)后用到游离节点。`scrollToPair` 的点击路径也复用。rAF 合并不变。 - **与原来的差别**:滚动期每帧省一次 O(n) DOM 查询;判定逻辑与选中行为不变。 - **复核后修正(FR-1)**:初版只 watch `qaPairs.length`,而它只随 **user** 消息数变化——删除 AI 消息(`ChatMessages.vue` 的 delete 按钮)不会改它,缓存会陈旧(外观级、可自愈)。已把 `props.messages.length` 并入失效源并同步注释。 ### R4-7 / R4-8(顺手项) - R4-7:`utils/markdownSafe.ts` 头部注释引用的 `chatModal/utils.ts` 已不存在(重构迁至 `lib/markdown.ts`),纯注释更正,无行为变化。 - R4-8:`useChatScroll` 的 `scrollRafId` 原仅在重入时 cancel 自身,卸载后不 cancel(残留回调全 optional chaining,无崩溃);现 `onBeforeUnmount` 对称 cancel。该 composable 仅在 `AiAssistantModal` setup 内调用,钩子等价。 ## ⑤ 验收证据 - **静态解析/ Lint**:全部通过,见 §⑥。 - **less 编译行为等价实测**:对改动后的 `Modal/src/index.less` 用仓库 `less@4.7.0` 编译(缺失的全局变量按 `generateModifyVars` 语义打桩),输出 CSS 中 `.zxm-modal-close-x` 规则已消失、`font-weight: normal` 保留 → 删除的是与 antCss.less 完全相同的选择器+值,渲染等价。 - **引用完整性自查**:`getToken`(`/@/utils/auth`)、`throwStreamHttpError`/`StreamError`/`getMe`(`/@/views/ventAI/manageAssistent/api/client`)均为真实导出;`AiAssistantModal → TaskListPanel/ChatInputArea` 的 `:isAdmin` 透传与两侧 props 声明一一对应;`formatClickError` 在 `handleClick` 之前定义(函数声明提升亦成立)。 - **未验证项(如实)**: - **未跑 `vite build`**(NAS 16G 悬崖边,硬性禁止;由 Lead 统一走远程构建)。 - **未浏览器实测**:M1 令牌头实际到达 TfAgents、M2 的 429/413/404 真实文案、M3 管理员/非管理员入口显隐、R4-5 滚动跟随手感——均为静态推演 + 旧分支既有验收结论外推(旧分支 M1/M2/M3 曾浏览器验收通过)。 - `extractMarkdownLinks` 缓存命中收益未跑 profiler 量化(结构推导可靠)。 ## ⑥ 静态检查 在 `.wt/mky-r4` 执行(node_modules 为指向主检出的只读软链): ```bash # 1) .ts 解析(仓库自带 @babel/parser@7.29.7,经由 .pnpm 路径 createRequire) node /tmp/parse-ts.mjs # 结果:6 个 .ts 全部 OK(exit 0) # 2) .vue 解析(仓库自带 @vue/compiler-sfc@3.5.40 的 parse + compileScript) node /tmp/parse-vue.mjs AiAssistantModal.vue TaskListPanel.vue ChatInputArea.vue ConversationNav.vue # 结果:4 个 .vue 全部 OK(exit 0) # 3) ESLint(eslint@8.57.1,仓库 .eslintrc.js) node_modules/.bin/eslint --ext .ts,.vue <全部 10 个改动文件> # 结果:exit 0,0 error / 0 warning(-f json 复核:10 文件 messages=0) # 4) less 编译抽查(R4-4 行为等价) less.render(index.less, modifyVars 打桩) # 输出无 modal-close-x、保留 font-weight → OK ``` - 既有问题说明:本次 eslint 目标文件零告警,**未发现需要标注的既有问题**;`--ext` 参数与直接文件列表混用时 eslint 仅处理匹配文件,已用 `-f json` 复核确认真处理了目标文件。 - 未跑 `vite build`(任务书明令禁止,8092 构建属 Lead 收口步骤)。 ## ⑦ 已知取舍 1. **M3 未搬运 `adminConfigured`**:旧分支该 ref 在模板中从未消费(死状态),按最小改动原则未重放。 2. **R4-5 存活校验**:除 `qaPairs.length` 失效外,额外保留 O(1) 的 `container.contains` 校验,覆盖「切换任务后问答对数恰好相同」的边角——比审计建议略保守,无行为风险。 3. **R4-2 只做了 `extractMarkdownLinks` LRU**:胶囊 computed 每 token 全量重扫 messages 的**外层循环**仍在(收益主要在免除最重的 lexer);按消息对象缓存 links 的更大改动未做(改动面与回归面变大,收益递减)。 4. **未做项(仅记录,不属本批)**:R4-3(ThinkingCard 每卡 1s interval 空转)、R4-6(TaskListPanel `:key="index"`)——审计定级 🟢 且明确"非本轮施工/可延后"。 5. **未推送/未部署**:按任务书,push / deliver / 部署由 Lead 复核后统一执行。 ## ⑧ 交付状态与上游同步 - **分支**:`fix/audit-r4-20260929`(worktree `/root/VentAnaly60/.wt/mky-r4`),基线 `upstream/master = 46df977e`,无分叉冲突项。 - **提交**:7 个功能提交 + 1 个 `[Docs]` 提交(清单见 §②,hash 以 `git log` 为准)。 - **上游同步**:分支自 `upstream/master` 新起(`46df977e`);**2026-09-30 已 merge 更新后的 `origin/master`**(+5 提交,含 API 按域拆分重构、dxf 支持、文件预览修复、导航条缓存),冲突与复验见 §⑨。 - **推送/部署**:未 push、未 deliver、未部署(任务书 §〇.2,由 Lead 收口)。 - **对照页**:`http://39.97.59.228:8013/hrx/mky-vent-base/compare/master...fix/audit-r4-20260929`(推送后由 Lead 核验 HTTP 200)。 ## ⑨ 合并新 master(2026-09-30)与复验 审计分支交付后,上游 `origin/master` 又前进 5 个提交(`83eb0a1e` 来源卡片默认折叠、`e7e89214` 导航条逻辑+警告修复、 `6de78411` API 按域拆分重构、`5f41a3be` dxf 支持、`64cfbb1a` 文件预览修复)。本次将 `origin/master` 合入分支, 逐项核对「审计修改在新结构上是否仍然成立」。 **取值原则**:AI 助手相关代码冲突时**以审计分支为主**(M1/M2/M3/R4-* 的审计语义一律保留); 上游若已删除/重构掉载体文件,则把审计逻辑**平移**到新结构(见下表 `api/client.ts` 一行),不改审计行为。 核对方式:对每个审计改动文件比对 `git grep -c "审计-第四轮"` 在审计分支与合并结果上的计数,10 个文件全部一致。 ### 冲突解决(3 处) | 文件 | 冲突 | 解决 | |---|---|---| | `api/client.ts` | 上游删除(拆分成 `api/*.ts`)vs 本分支修改(M2 加 export + M3 `getMe`) | 接受删除。M2 的「加 export」在新结构中**天然满足**(`throwStreamHttpError`/`StreamError` 已是 `api/errors.ts` 的导出);M3 的 `MeInfo`/`getMe()` 迁到新建 `api/user.ts`(按 barrel 约定 `export * from './user'`) | | `components/AiAssistantModal.vue` | 双方改同一 import 行 | 取上游的 barrel 路径并补 `getMe`:`import { …, StreamError, getMe } from '.../api'` | | `hooks/useChatScroll.ts` | 双方各加了 `onBeforeUnmount` 取消 rAF(R4-8 与上游重构撞车) | 两边等价(上游为 destroyOnClose 语义,本分支为 R4-8),按**冲突以审计分支为主**取审计版本(`if (scrollRafId !== null)` + R4-8 注释) | ### 其他适配(非冲突但必须改) - `dataPicker/api.ts`、`dataPicker/useDataPicker.ts`:`throwStreamHttpError`/`StreamError` 的 import 由已删除的 `api/client` 改为 `api/errors`;注释中的旧路径一并更正(与 R4-7 同类)。 - 自动合并文件复核结论:`ConversationNav.vue`(R4-5 的 `getWrappers` 缓存与上游 `toPlainTextCached` 共存, 失效源仍为「qaPairs.length + props.messages.length」FR-1 收口版)、`ChatInputArea.vue`/`TaskListPanel.vue` (M3 的 `isAdmin` 过滤/隐藏完整)、`lib/markdown.ts`(R4-2 LRU 完整)。 ### 复验证据 - **ESLint**:11 个审计改动文件,关闭 `prettier/prettier` 后 **0 error / 0 warning**。 (prettier 报的 3422 处 `Delete ␍` 是 Windows 检出 CRLF 环境噪声——对照组:本次未触碰的 `api/chat.ts` 同样报 161 处。) - **TypeScript**:`.tmp-tsconfig.json`(scoped:`ventAI/**` + `dataPicker/**` + `utils/markdownSafe.ts`)编译, **ventAI 相关 0 error**;其余 61 处均为既有问题(`store/`、`router/`、`utils/http/` 等,与本次无关)。 - **`vite build`**:exit 0(全量生产构建)。 - **产物核对**:`/ventAI/api/me`(M3)、`X-Access-Token`(M1,`useDataPicker` chunk)、 `formatClickError` 的 429/413/404 分支文案(M2)、`ConversationNav` chunk(R4-5)均出现在 dist; R4-4 在编译后 CSS 中 `.zxm-modal-close-x{…width:96px…}` **仅剩 1 处**(`antCss.less` 一份,组件层重复已消失)。 - **未做(如实)**:浏览器实测与连后端联调(无 TfAgents 环境);M1 令牌头到达后端、M2 真实 429/413/404 文案、M3 管理员/非管理员入口显隐仍待浏览器验收。