1
0
Эх сурвалжийг харах

[Fix R4-M3] /ventAI/api/me + getMe() + 按 is_admin 隐藏技能/子智能体/思考级别入口(重放 fix/base-agent-token M3,保留失败置 false 不缓存语义)

lizuo 2 долоо хоног өмнө
parent
commit
e1b33e82e4

+ 28 - 0
src/views/ventAI/manageAssistent/api/client.ts

@@ -5,6 +5,8 @@ import type { SseEvent } from '/@/views/ventAI/manageAssistent/types/index';
 
 enum Api {
   usageToday = '/ventAI/api/usage/today',
+  // [审计-第四轮 M3] 重放自我方 fix/base-agent-token(M1/M2/M3):当前登录用户权限信息(含 is_admin)
+  me = '/ventAI/api/me',
   unified = '/ventAI/api/chat',
   getHistoryList = '/ventAI/api/sessions',
   getDetail = '/ventAI/api/chat/history/',
@@ -95,6 +97,32 @@ export const throwStreamHttpError = async (response: Response): Promise<never> =
  */
 export const getTodayUsage = () => defHttp.get({ url: Api.usageToday }, { isTransformResponse: false });
 
+/**
+ * [审计-第四轮 M3] 重放自我方 fix/base-agent-token(M1/M2/M3)
+ * 当前登录用户的权限信息。TfAgents 把「技能上传/启停、子智能体增删改、思考级别切换」等
+ * 全局变更端点收归管理员后,基座据此隐藏入口(用户已定案「方案 B:由后端返回 is_admin,前端隐藏」)。
+ *
+ * 后端契约(TfAgents 侧):
+ *   GET /api/me(需认证,带 X-Access-Token)
+ *   → 200 { "username": "...", "is_admin": true|false, "admin_configured": true|false }
+ *   → 401 未登录 / 令牌无效
+ * 说明:admin_configured 表示服务端管理员白名单是否已配置;接口不返回白名单内容。
+ *
+ * 两点实现说明:
+ * 1) 这里不手写请求头:走 defHttp 时,axios 请求拦截器(src/utils/http/axios/index.ts)
+ *    会自动把令牌塞进 Authorization 与 X-Access-Token,与其它接口保持一致。
+ * 2) errorMessageMode: 'none' 是刻意加的:该接口在 TfAgents 尚未上线时返回 404,
+ *    若用默认的错误提示模式,每次打开 AI 弹框都会弹一个红色报错(误判为故障)。
+ *    这里静默失败,由调用方(AiAssistantModal.fetchMe)按「非管理员」保守降级。
+ */
+export interface MeInfo {
+  username: string;
+  is_admin: boolean;
+  admin_configured: boolean;
+}
+
+export const getMe = () => defHttp.get<MeInfo>({ url: Api.me }, { isTransformResponse: false, errorMessageMode: 'none' });
+
 /**
  * 历史会话接口
  */

+ 37 - 1
src/views/ventAI/manageAssistent/components/AiAssistantModal.vue

@@ -27,6 +27,7 @@
         <TaskListPanel
           :taskList="taskList"
           :currentTaskId="currentTaskId"
+          :isAdmin="isAdmin"
           @switch-task="switchTask"
           @create-task="createNewTask"
           @delete-task="handleDeleteTask"
@@ -35,6 +36,9 @@
           @toggle-sub-agents="toggleSubAgentPanel"
           @toggle-schedules="toggleSchedulePanel"
         />
+        <!-- [审计-第四轮 M3] 重放自我方 fix/base-agent-token(M1/M2/M3)::isAdmin 透传给左侧任务面板,
+             由面板内部过滤掉「技能」「子智能体」两个管理入口(按钮渲染在 TaskListPanel 的 optionItems 里)。
+             注意「定时任务」不属于全局变更,保持对所有人开放。 -->
       </div>
 
       <!-- 右侧内容区:根据 activePanel 切换显示 -->
@@ -86,6 +90,9 @@
           <TodoListBar v-if="showTodoBar" :todos="currentTodos" />
 
           <!-- 输入区域 -->
+          <!-- [审计-第四轮 M3] 重放自我方 fix/base-agent-token(M1/M2/M3)::isAdmin 透传给输入区,
+               由输入区隐藏「思考级别」切换控件;注意只隐藏"切换",
+               思考级别的读取(getModelMsg)对所有人保持开放 -->
           <ChatInputArea
             ref="chatInputRef"
             v-model="inputMessage"
@@ -101,6 +108,7 @@
             :usageRemaining="usageToday?.remaining_tokens ?? 0"
             :usageByModel="usageToday?.by_model ?? []"
             :initialThinkLevel="thinkLevel"
+            :isAdmin="isAdmin"
             :initialEditMode="editMode"
             :taskList="taskList"
             :currentTaskId="currentTaskId"
@@ -161,7 +169,8 @@
 <script setup lang="ts">
   import { ref, nextTick, watch, computed, onBeforeUnmount } from 'vue';
   import dayjs from 'dayjs';
-  import { getDetail, deleteSession, deleteMessage, StreamError } from '/@/views/ventAI/manageAssistent/api/client';
+  // [审计-第四轮 M3] 重放自我方 fix/base-agent-token(M1/M2/M3):引入 getMe 用于按 is_admin 隐藏管理入口
+  import { getDetail, deleteSession, deleteMessage, StreamError, getMe } from '/@/views/ventAI/manageAssistent/api/client';
   import { message, Modal } from 'ant-design-vue';
   import TaskListPanel from '/@/views/ventAI/manageAssistent/components/panels/TaskListPanel.vue';
   import ChatHeader from '/@/views/ventAI/manageAssistent/components/chat/ChatHeader.vue';
@@ -469,6 +478,9 @@
   const inputMessage = ref('');
   const selectedSessions = ref<Array<{ id: string; name: string }>>([]);
   const selectedSkill = ref<{ name: string; display_name: string } | null>(null);
+  // [审计-第四轮 M3] 重放自我方 fix/base-agent-token(M1/M2/M3):当前登录用户是否管理员(TfAgents /ventAI/api/me 的 is_admin)。
+  // 默认 false = 保守语义:取不到权限信息时按非管理员处理,管理入口隐藏。
+  const isAdmin = ref(false);
 
   // —— 以下四组职责已下沉到 composables(额度/导出/滚动/文件),组件只做接线 ——
   const { scrollToBottom, scrollToBottomThrottled } = useChatScroll(() => chatMessagesRef.value);
@@ -718,6 +730,28 @@
     },
   });
 
+  /**
+   * 拉取当前登录用户的权限信息(TfAgents 的 GET /ventAI/api/me)。
+   *
+   * [审计-第四轮 M3] 重放自我方 fix/base-agent-token(M1/M2/M3)。
+   * 为什么需要:TfAgents 已把「技能上传/启停、子智能体增删改、思考级别切换」等全局变更端点收归管理员
+   * (用户已定案方案 B:后端返回 is_admin,前端隐藏入口),基座据此隐藏对应入口,避免非管理员点了才被 403。
+   *
+   * 安全语义(保守失败,历史踩坑点必须保留):任何异常(未登录 401 / 接口未上线 404 / 网络失败)
+   * 一律按「非管理员」处理——isAdmin 置 false,且不缓存结果(每次打开都重新拉取,管理员名单可能在
+   * 服务端 .env 变更)。注意这只是界面可见性,真正的拦截仍在 TfAgents 后端——前端隐藏不等于安全。
+   */
+  const fetchMe = async () => {
+    try {
+      const me = await getMe();
+      isAdmin.value = !!me?.is_admin;
+    } catch (e) {
+      // 取不到(未登录/接口未上线/令牌失效)→ 按非管理员处理,隐藏管理入口
+      isAdmin.value = false;
+      console.warn('获取当前用户权限失败,管理入口将隐藏:', e);
+    }
+  };
+
   // 发送流式 AI 回答的核心:推送一条空的 AI 消息 → 建 AbortController → 调 SSE → 处理 session_id 迁移 → 异常/清理。
   // 正常发送、编辑重发、重新生成都复用此函数,file 存在且带 originalFile 时走 PDF 审查流,否则走纯文本流。
   // Throttled version for SSE streaming (no await, uses rAF)
@@ -733,6 +767,8 @@
         fetchModelMsg();
         fetchScheduleListForDetail();
         fetchTodayUsage();
+        // [审计-第四轮 M3] 重放自我方 fix/base-agent-token(M1/M2/M3):每次打开时刷新权限(不缓存;失败置 false 见 fetchMe)
+        fetchMe();
 
         // 去除重复的空白任务,只保留一个
         taskList.value = taskList.value.filter((task, index) => {

+ 15 - 2
src/views/ventAI/manageAssistent/components/chat/ChatInputArea.vue

@@ -78,8 +78,16 @@
         <ContextRing :used="contextUsed" :max="contextMax" :percent="contextPercent" :breakdown="contextBreakdown" />
         <div class="action-divider"></div>
         <UsageRing :limit="usageLimit" :spent="usageSpent" :remaining="usageRemaining" :by-model="usageByModel" />
-        <div class="action-divider"></div>
-        <ThinkLevelPopup :initial-level="initialThinkLevel" @change="emit('think-level-change', $event)" />
+        <!-- [审计-第四轮 M3] 重放自我方 fix/base-agent-token(M1/M2/M3):思考级别切换控件对非管理员隐藏。
+             原行为:任何角色都能看到并使用「思考级别」切换(TfAgents 已将其列为全局变更端点)。
+             现行为:ThinkLevelPopup 连同它前面的 .action-divider 一起包 v-if="isAdmin"(注意必须连分隔线
+                     一起隐藏,否则非管理员会残留一根竖线);后面的 .action-divider 保留,发送按钮布局不变。
+             为什么:入口可见性收敛到管理员;不改 ThinkLevelPopup 组件内部(可能被别处复用)。
+             思考级别的"读取"(getModelMsg)不受影响,对所有人依旧可用。 -->
+        <template v-if="isAdmin">
+          <div class="action-divider"></div>
+          <ThinkLevelPopup :initial-level="initialThinkLevel" @change="emit('think-level-change', $event)" />
+        </template>
         <div class="action-divider"></div>
         <div class="btn-send" :class="{ disabled: !streaming && (!modelValue.trim() || loading) }" @click="handleSendClick">
           <div class="send-icon" :class="{ 'stop-icon': streaming }"></div>
@@ -137,6 +145,9 @@
       usageRemaining?: number;
       usageByModel?: UsageByModel[];
       initialThinkLevel?: string;
+      // [审计-第四轮 M3] 重放自我方 fix/base-agent-token(M1/M2/M3):是否管理员(AiAssistantModal 透传)。
+      // 可选、默认 false:值缺失 = 按非管理员处理,隐藏「思考级别」切换入口。
+      isAdmin?: boolean;
       initialEditMode?: string;
       taskList?: Task[];
       currentTaskId?: string;
@@ -152,6 +163,8 @@
       usageRemaining: 0,
       usageByModel: () => [],
       initialThinkLevel: 'off',
+      // [审计-第四轮 M3] 保守默认:未传 isAdmin 时按非管理员处理(隐藏思考级别切换)
+      isAdmin: false,
       initialEditMode: 'full',
       taskList: () => [],
       currentTaskId: '',

+ 18 - 7
src/views/ventAI/manageAssistent/components/panels/TaskListPanel.vue

@@ -60,6 +60,9 @@
   const props = defineProps<{
     taskList: Task[];
     currentTaskId: string;
+    // [审计-第四轮 M3] 重放自我方 fix/base-agent-token(M1/M2/M3):是否管理员(由 AiAssistantModal 透传 TfAgents 的 is_admin)。
+    // 为可选属性并默认 false:既兼容其它潜在调用方,也保证「值缺失 = 按非管理员处理」的保守语义。
+    isAdmin?: boolean;
   }>();
 
   const emit = defineEmits<{
@@ -99,13 +102,21 @@
     emit('pin-task', task);
   };
 
-  const optionItems = [
-    { type: 'add' as OptionType, label: '新建任务' },
-    // { type: 'search' as OptionType, label: '搜索' },
-    { type: 'skill' as OptionType, label: '技能' },
-    { type: 'sub-agents' as OptionType, label: '子智能体' },
-    { type: 'schedules' as OptionType, label: '定时任务' },
-  ];
+  // [审计-第四轮 M3] 重放自我方 fix/base-agent-token(M1/M2/M3):顶部操作区的入口列表。
+  // 原行为:普通 const 数组,恒含「技能」「子智能体」,任何角色都能点进管理面板。
+  // 现行为:改为 computed 并按 isAdmin 过滤 —— 非管理员看不到这两个「全局变更」入口,
+  //         「新建任务 / 定时任务」对所有角色保持开放(定时任务属于个人功能,不在 TfAgents 的收敛范围内)。
+  // 为什么:用 computed(而非普通数组)是为了让 isAdmin 异步返回后能自动重算,无需刷新。
+  const optionItems = computed(() => {
+    const items = [
+      { type: 'add' as OptionType, label: '新建任务' },
+      // { type: 'search' as OptionType, label: '搜索' },
+      { type: 'skill' as OptionType, label: '技能' },
+      { type: 'sub-agents' as OptionType, label: '子智能体' },
+      { type: 'schedules' as OptionType, label: '定时任务' },
+    ];
+    return props.isAdmin ? items : items.filter((item) => item.type !== 'skill' && item.type !== 'sub-agents');
+  });
 
   const formatRelativeTime = (dateStr?: string) => {
     if (!dateStr) return '刚刚';