20260929-audit-r4.md 15 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)
  • 状态:已交付(本地提交完成;未推送、未部署;静态检查通过,未跑构建/未浏览器实测——见 §⑤⑦)

② 提交清单

一项一提交,提交信息均带编号(以 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

③ 改动面

文件 改了什么
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 watch 失效 + 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))。
  • 现在的行为:查询结果缓存为数组,仅在 qaPairs.length watch(已有)失效;另加 O(1) 存活校验(container.contains 首个 wrapper),防 DOM 整批替换(如切换任务同问答对数)后用到游离节点。scrollToPair 的点击路径也复用。rAF 合并不变。
  • 与原来的差别:滚动期每帧省一次 O(n) DOM 查询;判定逻辑与选中行为不变。

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 新起,已含团队最新结构;未 merge 更新的必要(派发时点 46df977e 即最新)。
  • 推送/部署:未 push、未 deliver、未部署(任务书 §〇.2,由 Lead 收口)。
  • 对照页:http://39.97.59.228:8013/hrx/mky-vent-base/compare/master...fix/audit-r4-20260929(推送后由 Lead 核验 HTTP 200)。