1
0
فهرست منبع

[Docs] 第四轮审计整改交付说明(M1/M2/M3 重放 + R4-2/R4-4/R4-5)+ ai-changes README

lizuo 2 هفته پیش
والد
کامیت
4e804715a0
2فایلهای تغییر یافته به همراه195 افزوده شده و 0 حذف شده
  1. 152 0
      docs/ai-changes/20260929-audit-r4.md
  2. 43 0
      docs/ai-changes/README.md

+ 152 - 0
docs/ai-changes/20260929-audit-r4.md

@@ -0,0 +1,152 @@
+# 第四轮审计整改 · 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 为指向主检出的只读软链):
+
+```bash
+# 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)。

+ 43 - 0
docs/ai-changes/README.md

@@ -0,0 +1,43 @@
+# AI 改动说明(ai-changes)
+
+本目录存放**由 AI 协助完成的改动**的说明文档,目的是让人工审核在 Gogs 的「改动对照」页面上
+就能直接看到「改了什么 / 为什么这么改 / 与原来的差别 / 怎么验收的 / 有什么已知取舍」,
+不必再去翻外部文档。
+
+## 为什么要放进分支里
+
+改动说明**必须随改动一起提交到分支**,这样它才会出现在改动对照(compare)页面里:
+
+```
+http://39.97.59.228:8013/hrx/<repo>/compare/master...<分支名>
+```
+
+放在仓库外面的说明文件(例如 `/root/TFAgents/docs/...`)不会出现在对照页,审核人看不到。
+
+## 命名约定
+
+```
+docs/ai-changes/<YYYYMMDD>-<简短主题>.md
+```
+
+- 日期用**改动/交付当天**的日期(本地时区 Asia/Shanghai),便于按文件名排序找到最新一批;
+- 主题用 ASCII 短横线连接的小写单词(例:`agent-token`、`sec-cleanup`、`perf-load-v5`);
+- 同一天同一分支多次追加说明时,直接**更新同一个文件**,不要新建(避免同一批改动出现多份说明)。
+
+## 每份说明应包含
+
+1. **头部**:对照网址、依据文档、分支、基线、日期、交付版本、状态(是否已验收/已交付/已部署);
+2. **提交清单**:本次分支上有哪些提交(功能提交 + merge 提交);
+3. **改动面**:涉及哪些文件、各改了什么;
+4. **逐项说明**:每项都写清「原来的行为 / 现在的行为 / 为什么这么改 / 与原来的差别」,
+   关键取舍(例如「令牌是可选语义,不能改成强制登录」)必须写明,便于人工判断有没有改错业务形态;
+5. **验收证据**:怎么验的、结果如何、失败项的实际现象;**验收中发现并修复的缺陷要单独标出**;
+6. **静态检查**:lint / 类型检查的命令与结果,以及**哪些是既有问题**(避免被误认为新引入);
+7. **已知取舍**:明确写清未做的部分与原因,不要让审核人以为漏了;
+8. **交付状态与上游同步**:fork / 主仓库 / 是否 merge 了最新 `upstream/master` / 是否部署。
+
+## 已有说明
+
+| 日期 | 文件 | 分支 | 主题 |
+|---|---|---|---|
+| 2026-09-29 | `20260929-audit-r4.md` | `fix/audit-r4-20260929` | 第四轮审计整改:M1/M2/M3 重放 + R4-2/R4-4/R4-5 + R4-7/R4-8 顺手项 |