20260929-audit-r4.md 21 KB

第四轮审计整改 · 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;该路径 2026-09-30 实测校正为 /ventAI/api/model/me,见 §⑩)
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/model/me + getMe() + 按 is_admin 隐藏全局变更入口(重放 M3;路径经 2026-09-30 实测校正,见 §⑩)

  • 原来的行为:所有角色都能看到并点击「技能」「子智能体」管理入口与「思考级别」切换;非管理员点了才被后端 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 为指向主检出的只读软链):

# 1) .ts 解析(仓库自带 @babel/parser@7.29.7,经由 .pnpm 路径 createRequire)
node /tmp/parse-ts.mjs <changed .ts files>
# 结果: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 管理员/非管理员入口显隐仍待浏览器验收。

⑩ M3 接口路径校正与浏览器实测(2026-09-30)

在 dev(:3100)+ 部署后端上做了浏览器实测,发现 M3 的 me 路径与部署不一致,已修正:

  • 原:api/user.ts 用 /ventAI/api/me(审计文档 §④ 当时写的契约是 "GET /api/me")。
  • 实测:dev 代理会剥掉 /ventAI 前缀,实际请求 /api/me —— 部署后端返回 404; 该后端 OpenAPI 中实际提供的是 /api/model/me(描述与 MeInfo 契约一致: {"username": str, "is_admin": bool, "admin_configured": bool}),带令牌请求 200。
  • 修正:me = '/ventAI/api/model/me',getMe() 注释同步更新(补 admin_configured 语义)。

实测结果(当前部署):

  • /api/model/me → {"username":"admin","is_admin":false,"admin_configured":false}(管理员账号也是 false);
  • 管理端点 POST /api/subagents/{name}/toggle → 403 admin_not_configured: "管理员未配置(TFAGENTS_ADMIN_USERNAMES 为空),全局变更端点对所有人拒绝;请运维在 .env 配置 TFAGENTS_ADMIN_USERNAMES 后重启"。

即:当前部署下前端隐藏入口与后端"对所有人拒绝"是一致的,不是前端过严;要让管理员看到 「技能 / 子智能体 / 思考级别」入口,需 TfAgents 侧配置 TFAGENTS_ADMIN_USERNAMES 并重启。

其余实测(隐藏分支):左侧仅「新建任务 / 定时任务」、输入区无思考级别控件、打开弹框无红色报错 (errorMessageMode:'none' 生效);对话 SSE 流式、链接胶囊(R4-2)、导航条点击跳转与滚动跟随(R4-5)、 关闭弹框(R4-4/R4-8)均已在浏览器中跑通。M1/M2 的点选解读因该环境主风机页无设备数据、无行可点, UI 触发未能实测(运行模块经 dev server 取源码确认已含 M1/M2 改动)。