# AI 改动说明 · mky-vent-base · 点选令牌 + 结构化错误 + 管理入口收敛 > **对照网址**: > 主仓库 > fork   > > 依据:`/root/TFAgents/docs/基座(mky-vent-base)配合改动清单.md`(M1 / M2 / M3 三项必改) > 分支:`fix/base-agent-token` 基线:`b3ec299b`(交付时已 merge `upstream/master` → `f25e5319`) > 日期:2026-09-15 代码交付版本:`0b3c1857`;本说明随后追加(仓库 `docs/ai-changes/`),最终分支 tip 见提交清单 > 状态:真实浏览器验收通过(M1 3/3、M2 5/5、M3 9/9);已交付主仓库同名分支,待人工审核合并;**未部署** --- ## 一、这次改了什么(提交清单) ``` (本提交) docs(ai-changes): 补本分支改动说明(本文件) 0b3c1857 Merge upstream/master(b3ec299b..f25e5319) 29d9be9e fix(ventAI): 验收发现并修复 is_admin 仅 watch 拉取导致管理员也看不到入口 + 注释补全 aef76ebb feat(ventAI): 非管理员隐藏技能/子智能体/思考级别等全局变更入口 (M3) 2c2673eb fix(ventAI): 点击解读路径复用 StreamError 结构化错误并转可读文案 (M2) 21b2fc53 fix(ventAI): 点击解读 SSE 请求携带 X-Access-Token 鉴权头 (M1) b3ec299b ← 基线(当时的 upstream/master) ``` **改动面:6 个文件,+166 / −13**(其中约 100 行为审核注释,功能代码约 60 行;本说明文件不计入)。 | 文件 | 改了什么 | |---|---| | `src/views/ventAI/dataPicker/api.ts` | M1 请求头补令牌;M2 复用结构化错误解析 | | `src/views/ventAI/dataPicker/useDataPicker.ts` | M2 新增 `formatClickError()`,把 429/413/404 转中文文案 | | `src/views/ventAI/manageAssistent/api.ts` | M2 导出 `throwStreamHttpError`;M3 新增 `MeInfo` 与 `getMe()` | | `.../manageAssistent/components/AiAssistantModal.vue` | M3 拉取并持有 `isAdmin`,透传给两个子组件 | | `.../components/chatModal/TaskListPanel.vue` | M3 按 `isAdmin` 过滤「技能 / 子智能体」入口 | | `.../components/chatModal/ChatInputArea.vue` | M3 非管理员隐藏「思考级别」切换控件 | --- ## 二、逐项说明:改了什么、为什么这么改、与原来的差别 ### M1 · 点选解读补 `X-Access-Token` **位置**:`src/views/ventAI/dataPicker/api.ts`(`ssePost()`) **原来的行为**: ```ts headers: { 'Content-Type': 'application/json' }, // 完全没有令牌 ``` 点选解读是基座里**唯一**不带令牌调 TfAgents 的路径。后果:TfAgents 无法识别是谁在用, 只能把消耗记到兜底账号 `click-anonymous`,**做不到「按登录人员记账」**(决策 D5 / B1-20 的明确要求)。 **现在的行为**: ```ts headers: { 'Content-Type': 'application/json', 'X-Access-Token': getToken(), // 与对话链路 manageAssistent/api.ts:215 同名同源 }, ``` **关键设计点(审核重点)**: - 令牌是**可选**语义:`getToken()` 为空时**仍照常发请求**,没有加任何「未登录就拦截 / 跳登录」的判断。 TfAgents 侧刻意保留 `click-anonymous` 兜底,若在此强制登录,会把未登录/令牌过期场景直接变成不可用。 - 头名 `X-Access-Token` 与仓库既有写法一致;TfAgents 读小写 `x-access-token`,HTTP 头不区分大小写。 - 该请求用 `fetch` + `ReadableStream`(非 `EventSource`),故可自定义请求头,无技术障碍。 **与原来的差别**:请求头由「只有 Content-Type」变为「Content-Type + X-Access-Token」; 请求体、返回值 `{thread_id, session_id}`、事件解析**完全没动**。 --- ### M2 · 点选错误改用结构化解析 **位置**:`manageAssistent/api.ts`(导出解析器)、`dataPicker/api.ts`(调用)、`useDataPicker.ts`(渲染文案) **原来的行为**: ```ts if (!response.ok) { throw new Error(`HTTP错误: ${response.status}`); // 用户只能看到「HTTP错误: 429」 } ``` **现在的行为**:三步,全部复用仓库既有能力,**没有新写第二套解析器**。 1. `throwStreamHttpError` 由模块内私有改为 `export`(**函数体零改动**); 2. 点选链路改为 `await throwStreamHttpError(response)` —— 它解析响应体的 `detail` (字符串或 `{ code, message, retry_after, limit_tokens, spent_tokens }`),抛出带 `status / retryAfter / limitTokens / spentTokens` 的 `StreamError`; 3. `useDataPicker.ts` 新增本地 `formatClickError()`,把结构化错误渲染成中文: | 场景 | 展示文案 | |---|---| | 429 且带 `retry_after` | 服务端 message + `(约15分钟后恢复)`(不足 1 分钟显示「不到 1 分钟」) | | 429 无 `retry_after` | 服务端 message + `(请稍后再试)` | | 413 且服务端无文案 | `附件体积超过上限,请减小文件后重试` | | 404 且服务端无文案 | `会话不存在或无权访问` | | 其它 / 普通 Error | 保持原样 `e?.message \|\| '请求失败'` | **为什么必须改**:TfAgents 之后对 429(额度/并发闸)、413(体积超限)、404(会话归属)返回结构化错误体 (D6 / B1-15 / B1-19 / B1-08),前端必须能翻译成人话。 **与原来的差别**:错误提示由「HTTP错误: 429」变为可读中文;**兜底路径不变** (非 `StreamError` 的异常仍原样透出 message),不会把网络中断等场景改坏。 --- ### M3 · 管理入口按 `is_admin` 收敛(已定案「方案 B」) **背景**:TfAgents 按决策 D1 / D9 把 9 个「全局变更」端点收归管理员(白名单在其 `.env`,先留空)。 基座原先对这些入口**没有任何角色判断**,非管理员点了才吃 403。 **后端契约**(TfAgents 侧,B2-01 追加需求): ``` GET /api/me (需认证:X-Access-Token) → 200 { "username": "...", "is_admin": true|false, "admin_configured": true|false } → 401 未登录 / 令牌无效 ``` 基座侧对应 URL 为 `/ventAI/api/me`(`/ventAI` 反代到 TfAgents)。 **基座四处改动**: 1. **`manageAssistent/api.ts`** 新增 `MeInfo` 与 `getMe()`: ```ts export const getMe = () => defHttp.get( { url: Api.me }, { isTransformResponse: false, errorMessageMode: 'none' } ); ``` - **不手写请求头**:走 `defHttp` 时 axios 拦截器自动附 `Authorization` 与 `X-Access-Token` (`src/utils/http/axios/index.ts:209-216`)。 - `errorMessageMode: 'none'` 是**刻意加的**:`/api/me` 在 TfAgents 上线前返回 404, 若用默认错误模式,**每次打开 AI 弹框都会弹红色报错**,会被误判为故障。 2. **`AiAssistantModal.vue`** 新增 `isAdmin` / `adminConfigured` 两个 ref 与 `fetchMe()`, 并在**两处**触发:`onMounted` 一次 + `visible` 的 watch 里一次。 - 初值 `isAdmin = false`:接口返回前先按非管理员渲染,避免管理入口「闪现」给非管理员。 - 失败降级为**保守失败**:未登录 / 接口未上线 / 网络失败 → 一律 `isAdmin = false`(隐藏入口)。 - **`fetchModelMsg()` 未做任何改动**:思考级别的**读取**对所有人保持开放,只收敛「切换」。 3. **`TaskListPanel.vue`**:`optionItems` 由普通数组改为 `computed` 并按 `isAdmin` 过滤。 - 「技能」「子智能体」→ 仅管理员可见;「新建任务」「定时任务」→ 对所有人开放 (定时任务属个人功能,不在收敛范围内)。 - 用 `computed` 而非普通数组,是为了 `isAdmin` 异步返回后能**自动重算**,无需刷新页面。 4. **`ChatInputArea.vue`**:仅给 `.btn-think-wrapper`(思考级别切换控件)加 `v-if="isAdmin"`。 - **只包住这一个块**:左右分隔线与发送按钮保持原样,不改按钮区布局。 - `handleSelectThinkLevel` / `thinkLevelOptions` / `initialThinkLevel` 的 watch 均未改动。 **与原来的差别**: | | 原来 | 现在 | |---|---|---| | 非管理员看到「技能」入口 | 是 | **否** | | 非管理员看到「子智能体」入口 | 是 | **否** | | 非管理员看到「思考级别」切换 | 是 | **否** | | 非管理员看到「定时任务」 | 是 | 是(不变) | | 思考级别读取(`getModelMsg`) | 开放 | 开放(不变) | | `/api/me` 不可用时 | — | **保守隐藏**(不是全部放开) | > ⚠️ **这是界面可见性,不是安全边界**:真正的拦截在 TfAgents 后端。前端隐藏只是让非管理员 > 不用「点了才知道没权限」,不能替代后端鉴权。 --- ## 三、浏览器真实验收(Playwright + 真实 Chromium) **方法**:用基座自带 **mock 登录**(`/?mock-login=1`)取得**真实登录令牌**,再驱动真实页面。 点选链路用临时验收页挂载**真实的 `v-data-picker` 指令**(`mode: 'click'`), AI 弹框用临时验收页挂载**真实的 `AiAssistantModal` 组件**;`/api/me` 用请求拦截注入不同角色响应。 **临时页与临时路由已在验收后全部删除**,工作区只剩 6 个目标文件(本说明文件除外)。 ### 验收结果 | 项 | 断言 | 结果 | |---|---|---| | M1-1 | 点击后确实向点选解读接口发出请求(`/ventAI/api/interpret/click/tun`) | ✅ | | M1-2 | 请求头含 `X-Access-Token` 且非空 | ✅ | | M1-3 | 该令牌与登录态令牌**同源一致** | ✅ | | M2-1 | 429 显示服务端结构化中文文案 | ✅ `今日额度已用完,明日 00:00 恢复(约15分钟后恢复)` | | M2-2 | 不再出现旧的「HTTP错误: 429」 | ✅ | | M2-3 | 429 附加「约 X 分钟后恢复」 | ✅ | | M2-413 | 413 显示中文文案且不含「HTTP错误」 | ✅ `上传体积超过上限` | | M2-404 | 404 显示中文文案且不含「HTTP错误」 | ✅ `会话不存在或无权访问` | | M3-1~3 | 非管理员看不到技能 / 子智能体 / 思考级别 | ✅ 计数 0/0/0 | | M3-4~6 | 管理员能看到技能 / 子智能体 / 思考级别 | ✅ 计数 1/1/1 | | M3-7 | `/api/me` 未上线(404)时保守降级隐藏 | ✅ 计数 0/0/0 | M1+M2 合计 8/8 通过;M3 合计 9/9 通过(含前置项)。 ### 🔴 验收中发现并已修复的真实缺陷(`29d9be9e`) M3 初版把 `fetchMe()` **只**放在 `visible` 的 `watch` 里。但本组件存在「**挂载即显示**」的用法 (它在 `layouts/default/index.vue` 与 `homeAI/index.vue` 中由页面持有 `visible`),此时 watch 不触发 → **`getMe` 从未被调用** → `isAdmin` 恒为 `false` → **管理员同样看不到管理入口**。 - **实测证据**:拦截 `/api/me` 计数为 **0**;管理员可见性 `skill=0 / subAgents=0 / think=0`。 - **修复**:增加 `onMounted(() => fetchMe())`,挂载即拉取一次;watch 内保留原有调用 (用于「每次打开都刷新」,管理员名单可能在服务端 `.env` 变更)。 - **修复后**:管理员 `1/1/1`,非管理员 `0/0/0`,404 时 `0/0/0`。 > 该缺陷**纯静态审查(lint / 类型检查)发现不了**——初版代码能编译、能跑、lint 也干净, > 只是 `is_admin` 从头到尾没被取过。这正是坚持做浏览器验收的价值。 ### 验收环境说明 - dev server:`vite`(端口 3100,本机起、验收后已停)。 - TfAgents 后端:`182.92.126.35:8070` 真实可达(`/api/model` 返回 200); **`/api/me` 尚未实现**(全仓 grep 无实现,仅文档),故 M3 用请求拦截注入角色响应 ——这也顺带验证了「接口未上线即保守降级」。 - 已知无关噪声(非本次改动引入):VentModelWeb 子应用卸载报错 `P$.a.off is not a function` (既有问题)、dev 环境 `/js/libs/adapter.min.js` 等静态资源 404、Jeecg WebSocket 连不上。 --- ## 四、静态检查 | 检查 | 命令 | 结果 | |---|---|---| | ESLint | `node_modules/.bin/eslint <6 个文件>` | 8 个错误,**全在 `ChatInputArea.vue` 的既有未使用变量**(`handleSelectModel` 等);在基线 `b3ec299b` 复跑得到**同样 8 个**(仅行号偏移)→ **无新增问题** | | TypeScript | `node_modules/.bin/vue-tsc --noEmit -p tsconfig.json` | ⚠️ **无法完成**:默认堆 OOM(exit 134),加大堆后 `RangeError: Maximum call stack size exceeded`(无诊断输出);**基线同样崩** → 属既有环境缺陷,与本改动无关 | | 替代类型核验 | 受限 tsconfig 只覆盖 6 个改动文件 | 基线 1103 错误 / 分支 1103 错误,改动文件内错误行号偏移、**无一条涉及新标识符**(`getMe`/`isAdmin`/`formatClickError`/`throwStreamHttpError` 等) → **无新增类型错误** | | 工作区 | `git status --porcelain` | ✅ 只剩 6 个目标文件(+ 本说明);临时验收页/临时路由已删除;无 `package-lock.json` 变动 | --- ## 五、已知取舍(均不影响功能) 1. **`adminConfigured` 已赋值但暂无模板消费**:按改动清单预留(`false` 时可提示「管理员未配置」)。 若不需要,可删除该 ref 与 `getMe` 里的赋值,或补一句提示文案。 2. **隐藏思考级别后,它左右两个 `.action-divider` 会相邻**(纯视觉问题): 改动清单明确要求「只包住思考级别块、保留分隔线」,故未顺带调整布局。 3. **`getMe` 会随弹框打开被调用一次,且挂载时也会调用一次**:`a-modal` 有 `destroyOnClose`, `visible=false` 时内容不渲染,因此不产生额外副作用;代价是比原实现多一个轻量 GET。 4. **M1 的「计费归属」端到端核对本次未做**:需 TfAgents 上线可选令牌 + 可按用户查用量, 当前只验证到「请求头确实带了与登录态一致的令牌」。**建议上线后补一次端到端记账核对。** 5. **`/api/me` 上线前,管理员也看不到管理入口**(404 → 保守降级)。 这意味着**基座与 TfAgents 的 B2-01 必须同批上线**,否则管理功能无人可用。 --- ## 六、交付状态与上游同步 | 项 | 值 | |---|---| | 本地分支 | `fix/base-agent-token` | | 我的 fork(`origin` = `lizuo/mky-vent-base`) | ✅ 已推送 `fix/base-agent-token` | | 主仓库(`upstream` = `hrx/mky-vent-base`) | ✅ 已交付同名分支(**人工审核合并,未动 master**) | | 上游基线同步 | ✅ 已 merge `upstream/master`(`b3ec299b..f25e5319`,2 提交,仅涉及 `home/configurable/*`、`gasPumpMonitor/*`,与本改动**零文件重叠**),落后 0 | | 部署 | ❌ 未部署(需另行下令) | merge 后已确认:本次 6 个文件与验收时**逐字节一致**(`git diff 29d9be9e HEAD -- <6 文件>` 为空), 且 `git diff upstream/master..HEAD` 只含这 6 个文件 + 本说明 —— 第三节的验收结论对交付版本依然成立。 ### 复现 / 后续命令 ```bash cd /root/VentAnaly60/mky-vent-base # 拉取该分支查看 git fetch upstream && git checkout -b review/base-agent-token upstream/fix/base-agent-token # 若后续还需更新(改了代码后) git push origin fix/base-agent-token && git deliver fix/base-agent-token ``` > **前置依赖**:TfAgents 侧的 `GET /api/me`(返回 `is_admin` / `admin_configured`)需与其**同批上线**, > 否则管理员也会失去管理入口(见「五、5」)。