Răsfoiți Sursa

[Feat 0000]ai助手新增技能展示、输入框各个接口联调改动

wangkeyi 2 zile în urmă
părinte
comite
8997a8d6f3

+ 1 - 0
src/assets/images/ventAI/chatModalBlue/2-10.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1786064150244" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4992" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M923.008 736.853333a25.6 25.6 0 0 1-12.8 22.186667l-29.653333 17.194667-222.165334 128.725333-90.325333 52.352a25.6 25.6 0 0 1-38.442667-22.186667V463.786667a25.6 25.6 0 0 1 14.208-22.954667L799.146667 314.026667l86.869333-43.093334a25.6 25.6 0 0 1 36.992 22.912V736.853333zM209.194667 314.026667l255.317333 126.805333a25.6 25.6 0 0 1 14.208 22.912V927.786667a25.6 25.6 0 0 1-36.949333 22.912l-86.954667-43.221334-222.165333-110.293333-33.109334-16.469333A25.6 25.6 0 0 1 85.333333 757.76V293.802667a25.6 25.6 0 0 1 36.992-22.954667L209.194667 314.026667z m-38.229334 391.210666a25.6 25.6 0 0 0 14.208 22.912l170.922667 84.906667a25.6 25.6 0 0 0 36.992-22.912V516.266667a25.6 25.6 0 0 0-14.208-22.954667l-170.922667-84.906667a25.6 25.6 0 0 0-36.992 22.954667v273.834667z m458.496-211.84a25.6 25.6 0 0 0-14.208 22.954667v270.677333c0 19.712 21.333333 32 38.4 22.144l170.965334-99.072a25.6 25.6 0 0 0 12.757333-22.144v-256.512a25.6 25.6 0 0 0-36.992-22.954666l-170.922667 84.906666zM537.088 54.186667l222.165333 91.946666 120.618667 49.962667a25.6 25.6 0 0 1 1.621333 46.592l-116.906666 58.026667-222.122667 110.378666-26.88 13.354667a25.6 25.6 0 0 1-22.784 0l-26.922667-13.354667-222.165333-110.336-116.906667-58.069333a25.6 25.6 0 0 1 1.621334-46.592l120.661333-49.92 222.122667-91.989333 23.168-9.557334a25.6 25.6 0 0 1 19.584 0l23.125333 9.557334zM334.037333 203.093333a25.6 25.6 0 0 0-1.578666 46.634667l160.341333 79.616a25.6 25.6 0 0 0 22.741333 0l160.341334-79.616a25.6 25.6 0 0 0-1.578667-46.592l-160.341333-66.346667a25.6 25.6 0 0 0-19.584 0L334.037333 203.093333z" p-id="4993" fill="#ffffff"></path></svg>

+ 1 - 0
src/assets/images/ventAI/chatModalBlue/2-9.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1785993167154" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5233" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M109.714286 900.608h804.571428c25.234286 0 45.714286 18.358857 45.714286 41.106286 0 22.674286-20.48 41.179429-45.714286 41.179428h-804.571428c-25.234286 0-45.714286-18.432-45.714286-41.179428 0-22.674286 20.48-41.179429 45.714286-41.179429z m433.956571-645.632l-337.92 337.92v142.189714h142.189714l337.92-337.92-142.189714-142.189714z m58.148572-58.221714l142.262857 142.262857 58.148571-58.221714-142.189714-142.189715-58.221714 58.148572zM136.850286 545.426286L627.565714 54.564571a45.714286 45.714286 0 0 1 64.731429 0l193.901714 193.901715a45.714286 45.714286 0 0 1 0 64.658285l-490.788571 490.861715a45.714286 45.714286 0 0 1-32.402286 13.385143H169.106286a45.714286 45.714286 0 0 1-45.641143-45.714286V577.828571c0-12.141714 4.754286-23.771429 13.385143-32.329142z" fill="#ffffff" p-id="5234"></path></svg>

+ 2 - 0
src/design/themify/deepblue.less

@@ -104,7 +104,9 @@ html[data-theme='deepblue'] {
   --img-chat-welcome-img: url('/src/assets/images/ventAI/chatModalBlue/5-1.png');
   --img-chat-pin-icon: url('/src/assets/images/ventAI/chatModalBlue/3-1.svg');
   --img-chat-delete-icon: url('/src/assets/images/ventAI/chatModalBlue/3-2.svg');
+  --img-chat-edit-icon: url('/src/assets/images/ventAI/chatModalBlue/2-9.svg');
   --img-chat-mode-confirm-icon: url('/src/assets/images/ventAI/chatModalBlue/3-9.svg');
+  --img-chat-skill-item-icon: url('/src/assets/images/ventAI/chatModalBlue/2-10.svg');
   --img-chat-mode-auto-icon: url('/src/assets/images/ventAI/chatModalBlue/3-10.svg');
   --img-chat-mode-plan-icon: url('/src/assets/images/ventAI/chatModalBlue/3-11.svg');
   --img-chat-mode-full-icon: url('/src/assets/images/ventAI/chatModalBlue/3-12.svg');

+ 2 - 0
src/design/themify/default.less

@@ -110,7 +110,9 @@ html {
   --img-chat-welcome-img: url('/src/assets/images/ventAI/chatModalBlue/5-1.png');
   --img-chat-pin-icon: url('/src/assets/images/ventAI/chatModalBlue/3-1.svg');
   --img-chat-delete-icon: url('/src/assets/images/ventAI/chatModalBlue/3-2.svg');
+  --img-chat-edit-icon: url('/src/assets/images/ventAI/chatModalBlue/2-9.svg');
   --img-chat-mode-confirm-icon: url('/src/assets/images/ventAI/chatModalBlue/3-9.svg');
+  --img-chat-skill-item-icon: url('/src/assets/images/ventAI/chatModalBlue/2-10.svg');
   --img-chat-mode-auto-icon: url('/src/assets/images/ventAI/chatModalBlue/3-10.svg');
   --img-chat-mode-plan-icon: url('/src/assets/images/ventAI/chatModalBlue/3-11.svg');
   --img-chat-mode-full-icon: url('/src/assets/images/ventAI/chatModalBlue/3-12.svg');

+ 168 - 6
src/views/ventAI/manageAssistent/api.ts

@@ -6,7 +6,10 @@ enum Api {
   getHistoryList = '/ventAI/api/sessions',
   getDetail = '/ventAI/api/chat/history/',
   deleteSession = '/ventAI/api/sessions/',
-  reviewPdf = '/ventAI/api/vent/review/pdf',
+  chatResume = '/ventAI/api/chat/resume',
+  getModelMsg = '/ventAI/api/model',
+  switchThinking = '/ventAI/api/model/thinking',
+  getSkills = '/ventAI/api/skills',
 }
 
 /**
@@ -26,6 +29,76 @@ export const getDetail = (session_id: string) => defHttp.get({ url: Api.getDetai
  */
 export const deleteSession = (session_id: string) => defHttp.delete({ url: Api.deleteSession + session_id }, { isTransformResponse: false });
 
+/**
+ * 置顶会话
+ * @param session_id - 会话ID
+ */
+export const pinSession = (session_id: string) => defHttp.put({ url: `/ventAI/api/sessions/${session_id}/pin` }, { isTransformResponse: false });
+
+/**
+ * 编辑会话名称
+ * @param session_id - 会话ID
+ * @param title - 新标题
+ */
+export const editSessionTitle = (session_id: string, title: string) =>
+  defHttp.put({ url: `/ventAI/api/sessions/${session_id}/title`, data: { title } }, { isTransformResponse: false });
+
+/**
+ * 获取对话权限模式
+ * @param session_id - 会话ID
+ */
+export const getSessionMode = (session_id: string) => defHttp.get({ url: `/ventAI/api/sessions/${session_id}/mode` }, { isTransformResponse: false });
+
+/**
+ * 设置对话权限模式
+ * @param session_id - 会话ID
+ */
+export const putSessionMode = (session_id: string, mode: string) =>
+  defHttp.put({ url: `/ventAI/api/sessions/${session_id}/mode`, data: { mode } }, { isTransformResponse: false });
+/**
+ * 上下文用量查询
+ * @param session_id - 会话ID
+ */
+export const getContext = (session_id: string) => defHttp.get({ url: `/ventAI/api/sessions/${session_id}/context` }, { isTransformResponse: false });
+
+/**
+ * 查询模型信息
+ */
+export const getModelMsg = () => defHttp.get({ url: Api.getModelMsg }, { isTransformResponse: false });
+
+/**
+ * 切换模型思考深度
+ * @param level - 思考级别 off/high/highest
+ */
+export const switchThinking = (level: string) => defHttp.post({ url: Api.switchThinking, data: { level } }, { isTransformResponse: false });
+
+/**
+ * 查询模型技能
+ */
+export const getSkills = () => defHttp.get({ url: Api.getSkills }, { isTransformResponse: false });
+
+/**
+ * 查询模型技能详情
+ */
+export const getSkillDetails = (name: string) => defHttp.get({ url: Api.getSkills + '/' + name }, { isTransformResponse: false });
+
+/**
+ * 启用/禁用模型技能
+ */
+export const toggleSkill = (name: string) => defHttp.post({ url: Api.getSkills + '/' + name + '/toggle' }, { isTransformResponse: false });
+
+/**
+ * 上传新技能
+ * @param file - zip附件
+ */
+export const uploadSkill = (file: File) => {
+  const formData = new FormData();
+  formData.append('file', file);
+  return defHttp.post(
+    { url: Api.getSkills + '/upload', data: formData, headers: { 'Content-Type': 'multipart/form-data' } },
+    { isTransformResponse: false }
+  );
+};
 /**
  * 统一对话接口(SSE流式响应)
  * @param params.message - 用户输入的文本,必填
@@ -35,7 +108,7 @@ export const deleteSession = (session_id: string) => defHttp.delete({ url: Api.d
  * @returns Promise<{ session_id: string }>
  */
 export const unifiedStream = async (
-  params: { message: string; session_id?: string; file?: File },
+  params: { message: string; session_id?: string; file?: File; mode?: string },
   onChunk: (chunk: string) => void
 ): Promise<{ session_id: string }> => {
   try {
@@ -47,6 +120,9 @@ export const unifiedStream = async (
     if (params.file) {
       formData.append('file', params.file);
     }
+    if (params.mode) {
+      formData.append('mode', params.mode);
+    }
 
     const response = await fetch(Api.unified, {
       method: 'POST',
@@ -84,7 +160,7 @@ export const unifiedStream = async (
 
             onChunk(line);
 
-            if (data.type === 'done' || data.type === 'error') {
+            if (data.type === 'done' || data.type === 'error' || data.type === 'interrupt') {
               if (data.session_id) {
                 sessionId = data.session_id;
               }
@@ -116,7 +192,7 @@ export const unifiedStream = async (
  * @returns Promise<{ session_id: string }>
  */
 export const reviewPdfStream = async (
-  params: { file: File; session_id?: string; message?: string },
+  params: { file: File; session_id?: string; message?: string; mode?: string },
   onChunk: (chunk: string) => void
 ): Promise<{ session_id: string }> => {
   try {
@@ -128,8 +204,11 @@ export const reviewPdfStream = async (
     if (params.message) {
       formData.append('message', params.message);
     }
+    if (params.mode) {
+      formData.append('mode', params.mode);
+    }
 
-    const response = await fetch(Api.reviewPdf, {
+    const response = await fetch(Api.unified, {
       method: 'POST',
       body: formData,
       headers: { 'X-Access-Token': getToken() },
@@ -165,7 +244,7 @@ export const reviewPdfStream = async (
 
             onChunk(line);
 
-            if (data.type === 'done' || data.type === 'error') {
+            if (data.type === 'done' || data.type === 'error' || data.type === 'interrupt') {
               if (data.session_id) {
                 sessionId = data.session_id;
               }
@@ -187,3 +266,86 @@ export const reviewPdfStream = async (
     throw error;
   }
 };
+
+/**
+ * 恢复会话接口(SSE流式响应)
+ * @param params.session_id - 会话ID,必填
+ * @param params.thread_id - LangGraph线程ID,可选
+ * @param params.action - "approve"(批准)或 "reject"(拒绝),必填
+ * @param onChunk - 流式数据回调函数
+ * @returns Promise<{ session_id: string }>
+ */
+export const chatResumeStream = async (
+  params: { session_id: string; thread_id?: string; action: string },
+  onChunk: (chunk: string) => void
+): Promise<{ session_id: string }> => {
+  try {
+    const body: Record<string, string> = {
+      session_id: params.session_id,
+      action: params.action,
+    };
+    if (params.thread_id) {
+      body.thread_id = params.thread_id;
+    }
+
+    const response = await fetch(Api.chatResume, {
+      method: 'POST',
+      body: JSON.stringify(body),
+      headers: {
+        'Content-Type': 'application/json',
+        'X-Access-Token': getToken(),
+      },
+    });
+
+    if (!response.ok) {
+      throw new Error(`HTTP错误: ${response.status}`);
+    }
+
+    const reader = response.body!.getReader();
+    const decoder = new TextDecoder();
+    let buffer = '';
+    let sessionId = '';
+    let isDone = false;
+
+    // eslint-disable-next-line no-constant-condition
+    while (true) {
+      const { done, value } = await reader.read();
+      if (done || isDone) break;
+
+      buffer += decoder.decode(value, { stream: true });
+      const lines = buffer.split('\n');
+      buffer = lines.pop() || '';
+
+      for (const line of lines) {
+        if (line.trim() === '') continue;
+
+        if (line.startsWith('data: ')) {
+          const dataStr = line.replace(/^data: /, '');
+
+          try {
+            const data = JSON.parse(dataStr);
+
+            onChunk(line);
+
+            if (data.type === 'done' || data.type === 'error') {
+              if (data.session_id) {
+                sessionId = data.session_id;
+              }
+              isDone = true;
+              break;
+            }
+          } catch (e) {
+            console.warn('SSE数据解析失败:', dataStr);
+          }
+        }
+      }
+
+      if (isDone) break;
+    }
+
+    return { session_id: sessionId };
+  } catch (error) {
+    console.error('恢复会话流式请求失败:', error);
+    throw error;
+  }
+};

+ 398 - 13
src/views/ventAI/manageAssistent/components/AiAssistantModal.vue

@@ -27,6 +27,7 @@
           @switch-task="switchTask"
           @create-task="createNewTask"
           @delete-task="handleDeleteTask"
+          @pin-task="handlePinTask"
         />
       </div>
 
@@ -45,7 +46,15 @@
               <div class="nav-icon add-icon"></div>
             </div>
           </template>
-          <span v-if="currentTask?.sessionId" class="chat-header-title">{{ currentTask.name }}</span>
+          <div v-if="currentTask?.sessionId" class="chat-header-title-wrapper">
+            <template v-if="editingTitle">
+              <input v-model="editTitleValue" class="title-edit-input" @keyup.enter="saveTitle" @keyup.escape="cancelEditTitle" @blur="saveTitle" />
+            </template>
+            <template v-else>
+              <div class="title-edit-btn" @click="startEditTitle" title="编辑标题"></div>
+              <span class="chat-header-title">{{ currentTask.name }}</span>
+            </template>
+          </div>
           <div v-if="currentWordUrl" class="upload-bg" @click="downloadWordReport">
             <div class="download-icon"></div>
           </div>
@@ -55,18 +64,35 @@
         <FileListPanel :visible="showFileList" :files="getCurrentTaskFiles()" @close="showFileList = false" @file-click="openFilePreview" />
 
         <!-- 消息列表 -->
-        <ChatMessages ref="chatMessagesRef" :messages="messages" @file-preview="openFilePreview" @download-word="downloadWordFile" />
+        <ChatMessages
+          ref="chatMessagesRef"
+          :messages="messages"
+          @file-preview="openFilePreview"
+          @download-word="downloadWordFile"
+          @approve="handleApprove"
+          @reject="handleReject"
+        />
 
         <!-- 输入区域 -->
         <ChatInputArea
           ref="chatInputRef"
           v-model="inputMessage"
           :pendingFile="pendingFile"
-          :loading="loading"
+          :loading="loading || !!pendingApprovalSessionId"
+          :contextUsed="contextUsed"
+          :contextMax="contextMax"
+          :contextPercent="contextPercent"
+          :contextBreakdown="contextBreakdown"
+          :initialThinkLevel="thinkLevel"
+          :initialEditMode="editMode"
+          :taskList="taskList"
           @send="handleSendMessage"
           @file-upload="triggerFileUpload"
           @file-change="handleFileUpload"
           @remove-pending-file="removePendingFile"
+          @think-level-change="handleThinkLevelChange"
+          @edit-mode-change="handleEditModeChange"
+          @insert-session="handleInsertSession"
         />
       </div>
 
@@ -81,9 +107,22 @@
 <script setup lang="ts">
   import { ref, nextTick, watch, computed, onMounted, onBeforeUnmount } from 'vue';
   import dayjs from 'dayjs';
-  import { unifiedStream, reviewPdfStream, getHistoryList, getDetail, deleteSession } from '../api';
+  import {
+    unifiedStream,
+    reviewPdfStream,
+    chatResumeStream,
+    getHistoryList,
+    getDetail,
+    deleteSession,
+    pinSession,
+    editSessionTitle,
+    getContext,
+    switchThinking,
+    getModelMsg,
+    getSessionMode,
+    putSessionMode,
+  } from '../api';
   import { message, Modal } from 'ant-design-vue';
-  import { isPdfFile } from './chatModal/utils';
   import TaskListPanel from './chatModal/TaskListPanel.vue';
   import FileListPanel from './chatModal/FileListPanel.vue';
   import ChatMessages from './chatModal/ChatMessages.vue';
@@ -110,13 +149,14 @@
       const res = await getHistoryList({});
       const sessions = res?.sessions || res?.data || [];
       if (Array.isArray(sessions)) {
-        const apiTasks: Task[] = sessions.map((item: { session_id: string; title: string; updated_at: string }) => ({
+        const apiTasks: Task[] = sessions.map((item: { session_id: string; title: string; updated_at: string; sort_order?: number }) => ({
           id: item.session_id,
           name: item.title || '未命名会话',
           sessionId: item.session_id,
           messages: [],
           attachedFiles: [],
           updatedAt: item.updated_at,
+          sortOrder: item.sort_order,
         }));
 
         const mergedTasks = apiTasks.map((apiTask) => {
@@ -153,11 +193,54 @@
 
   const messages = ref<Message[]>([]);
 
+  const contextUsed = ref(0);
+  const contextMax = ref(1000000);
+  const contextPercent = ref(0);
+  const thinkLevel = ref('off');
+  const editMode = ref('full');
+  const pendingApprovalSessionId = ref('');
+  const pendingApprovalThreadId = ref('');
+  const contextBreakdown = ref({ messages: 0, mcp: 0, skills: 0, system_prompt: 0, other: 0 });
+
   const panelCollapsed = ref(false);
   const togglePanel = () => {
     panelCollapsed.value = !panelCollapsed.value;
   };
 
+  const editingTitle = ref(false);
+  const editTitleValue = ref('');
+
+  const startEditTitle = () => {
+    if (!currentTask.value?.sessionId) return;
+    editTitleValue.value = currentTask.value.name;
+    editingTitle.value = true;
+    nextTick(() => {
+      const input = document.querySelector('.title-edit-input') as HTMLInputElement;
+      input?.focus();
+      input?.select();
+    });
+  };
+
+  const saveTitle = async () => {
+    const newTitle = editTitleValue.value.trim();
+    if (!currentTask.value?.sessionId || !newTitle || newTitle === currentTask.value.name) {
+      editingTitle.value = false;
+      return;
+    }
+    try {
+      await editSessionTitle(currentTask.value.sessionId, newTitle);
+      currentTask.value.name = newTitle;
+      message.success('标题修改成功');
+    } catch {
+      message.error('标题修改失败');
+    }
+    editingTitle.value = false;
+  };
+
+  const cancelEditTitle = () => {
+    editingTitle.value = false;
+  };
+
   const isFullscreen = ref(false);
   const toggleFullscreen = () => {
     isFullscreen.value = !isFullscreen.value;
@@ -251,6 +334,16 @@
       }
     }
 
+    if (newTask.sessionId) {
+      fetchContext(newTask.sessionId);
+      fetchSessionMode(newTask.sessionId);
+    } else {
+      contextUsed.value = 0;
+      contextPercent.value = 0;
+      contextBreakdown.value = { messages: 0, mcp: 0, skills: 0, system_prompt: 0, other: 0 };
+      editMode.value = 'full';
+    }
+
     messages.value = newTask.messages && newTask.messages.length > 0 ? [...newTask.messages] : getDefaultMessages();
 
     await nextTick();
@@ -272,10 +365,151 @@
       attachedFiles: [],
     };
 
+    contextUsed.value = 0;
+    contextPercent.value = 0;
+    contextBreakdown.value = { messages: 0, mcp: 0, skills: 0, system_prompt: 0, other: 0 };
+    editMode.value = 'full';
     taskList.value.unshift(newTask);
     switchTask(newTask.id);
   };
 
+  const handlePinTask = async (task: Task) => {
+    try {
+      await pinSession(task.sessionId);
+      await fetchSessionList();
+    } catch {
+      message.error('置顶操作失败');
+    }
+  };
+
+  const handleThinkLevelChange = async (levelKey: string) => {
+    try {
+      await switchThinking(levelKey);
+    } catch {
+      message.error('切换思考级别失败');
+    }
+  };
+
+  const handleInsertSession = (_taskId: string, _taskName: string) => {
+    // 会话引用已由 ChatInputArea 内部以 chip 形式管理
+  };
+
+  const handleEditModeChange = async (modeKey: string) => {
+    const sessionId = getCurrentSessionId();
+    if (!sessionId) return;
+    try {
+      await putSessionMode(sessionId, modeKey);
+      editMode.value = modeKey;
+    } catch {
+      message.error('切换模式失败');
+    }
+  };
+
+  const handleApprove = async () => {
+    const sessionId = pendingApprovalSessionId.value || getCurrentSessionId();
+    if (!sessionId) return;
+
+    // 清除当前消息的 pendingApproval 状态,获取中断前累计时长和思考内容
+    const lastMsg = messages.value[messages.value.length - 1];
+    const prevElapsedMs = lastMsg?.pendingApprovalData?.elapsedMs || 0;
+    const prevThinkingContent = lastMsg?.pendingApprovalData?.prevThinkingContent || lastMsg?.thinkingContent || '';
+    if (lastMsg && lastMsg.isPendingApproval) {
+      lastMsg.isPendingApproval = false;
+    }
+
+    const now = dayjs();
+    const aiMsgIndex = messages.value.length;
+    const aiMsg: Message = {
+      type: 'ai',
+      content: '',
+      thinkingContent: prevThinkingContent,
+      time: now.format('HH:mm'),
+      createdAt: now.toISOString(),
+      isLoading: true,
+      thinkingSteps: [],
+      generateStartTime: Date.now(),
+      baseDurationMs: prevElapsedMs,
+    };
+    messages.value.push(aiMsg);
+    await scrollToBottom();
+
+    loading.value = true;
+
+    try {
+      const result = await chatResumeStream(
+        {
+          session_id: sessionId,
+          thread_id: pendingApprovalThreadId.value || undefined,
+          action: 'approve',
+        },
+        (chunk: string) => {
+          parseSSEData(chunk, aiMsgIndex);
+          scrollToBottom();
+        }
+      );
+
+      if (result.session_id) {
+        setCurrentSessionId(result.session_id);
+      }
+    } catch (error) {
+      console.error('审批继续执行失败:', error);
+      message.error('审批失败,请重试');
+      const errorMsg = messages.value[aiMsgIndex];
+      if (errorMsg && errorMsg.isLoading) {
+        errorMsg.isLoading = false;
+        errorMsg.content = '抱歉,审批请求失败,请稍后重试。';
+      }
+    } finally {
+      loading.value = false;
+      pendingApprovalSessionId.value = '';
+      pendingApprovalThreadId.value = '';
+      await scrollToBottom();
+    }
+  };
+
+  const handleReject = async () => {
+    const sessionId = pendingApprovalSessionId.value || getCurrentSessionId();
+    if (!sessionId) return;
+
+    // 清除当前消息的 pendingApproval 状态
+    const lastMsg = messages.value[messages.value.length - 1];
+    if (lastMsg && lastMsg.isPendingApproval) {
+      lastMsg.isPendingApproval = false;
+    }
+
+    try {
+      await chatResumeStream(
+        {
+          session_id: sessionId,
+          thread_id: pendingApprovalThreadId.value || undefined,
+          action: 'reject',
+        },
+        () => {}
+      );
+    } catch (error) {
+      console.error('拒绝操作失败:', error);
+    }
+
+    const now = dayjs();
+    const rejectMsg: Message = {
+      type: 'ai',
+      content: '已拒绝执行计划。',
+      time: now.format('HH:mm'),
+      createdAt: now.toISOString(),
+    };
+    messages.value.push(rejectMsg);
+
+    pendingApprovalSessionId.value = '';
+    pendingApprovalThreadId.value = '';
+
+    const currentTask = taskList.value.find((t) => t.id === currentTaskId.value);
+    if (currentTask) {
+      currentTask.messages = [...messages.value];
+    }
+
+    await scrollToBottom();
+  };
+
   const handleDeleteTask = (task: Task, _index: number) => {
     Modal.confirm({
       title: '提示',
@@ -357,12 +591,6 @@
     const file = target.files?.[0];
     if (!file) return;
 
-    if (!isPdfFile(file.name)) {
-      message.error('仅支持 PDF 文件格式');
-      target.value = '';
-      return;
-    }
-
     try {
       const newFile: AttachedFile = {
         id: `file-${Date.now()}`,
@@ -473,6 +701,10 @@
       return;
     }
     if (loading.value) return;
+    if (pendingApprovalSessionId.value) {
+      message.warning('请先审批执行计划');
+      return;
+    }
 
     const userInput = inputMessage.value.trim();
     const fileToSend = pendingFile.value;
@@ -482,6 +714,8 @@
 
     try {
       loading.value = true;
+      await nextTick();
+      chatMessagesRef.value?.forceScrollToBottom();
 
       if (hasFile) {
         await sendWithAttachment(fileToSend!, userInput);
@@ -544,6 +778,48 @@
     steps.push(step);
   };
 
+  const fetchModelMsg = async () => {
+    try {
+      const res = await getModelMsg();
+      if (res?.thinking_level) {
+        thinkLevel.value = res.thinking_level;
+      }
+    } catch (e) {
+      console.error('获取模型信息失败:', e);
+    }
+  };
+
+  const fetchSessionMode = async (sessionId: string) => {
+    try {
+      const res = await getSessionMode(sessionId);
+      if (res?.mode) {
+        editMode.value = res.mode;
+      }
+    } catch (e) {
+      console.error('获取会话模式失败:', e);
+    }
+  };
+
+  const fetchContext = async (sessionId: string) => {
+    try {
+      const res = await getContext(sessionId);
+      if (res?.current_usage != null) {
+        contextUsed.value = res.current_usage;
+      }
+      if (res?.total_limit != null) {
+        contextMax.value = res.total_limit;
+      }
+      if (res?.usage_pct != null) {
+        contextPercent.value = res.usage_pct;
+      }
+      if (res?.breakdown) {
+        contextBreakdown.value = res.breakdown;
+      }
+    } catch (e) {
+      console.error('获取上下文容量失败:', e);
+    }
+  };
+
   const handleSSEMessage = (data: any, aiMsgIndex: number) => {
     const aiMsg = messages.value[aiMsgIndex];
     const normalizedContent = normalizeLineBreaks(data.content || '');
@@ -561,6 +837,13 @@
         }
         break;
 
+      // thinking_token - 模型思考过程内容
+      case 'thinking_token':
+        if (normalizedContent) {
+          aiMsg.thinkingContent = (aiMsg.thinkingContent || '') + normalizedContent;
+        }
+        break;
+
       // 系统进度消息 - 仅当 agent 不是 system 时才作为 thinking step
       case 'progress': {
         if (data.agent && data.agent !== 'system') {
@@ -639,6 +922,10 @@
           message: `${(data.message || '审查完成').replace(/(\d+ms)/g, '')}${data.duration_ms ? `(${(data.duration_ms / 1000).toFixed(0)}s)` : ''}${data.progress ? ` [${data.progress}]` : ''}`,
           timestamp: Date.now(),
         });
+        // 用后端返回的时间替换前端计时,作为中断前的累计基准
+        if (data.duration_ms) {
+          aiMsg.baseDurationMs = data.duration_ms;
+        }
         if (data.preview) {
           aiMsg.content += data.preview;
         }
@@ -685,9 +972,10 @@
       case 'done':
         if (data.session_id) {
           aiMsg.sessionId = data.session_id;
+          fetchContext(data.session_id);
         }
         if (data.duration_ms) {
-          aiMsg.durationMs = data.duration_ms;
+          aiMsg.durationMs = (aiMsg.baseDurationMs || 0) + data.duration_ms;
         }
         aiMsg.isLoading = false;
         break;
@@ -697,6 +985,24 @@
           aiMsg.content += normalizedContent.replace(/\n/g, '  \n');
         }
         break;
+
+      case 'interrupt': {
+        // 优先使用 agent_done 已设置的后端时间,否则用前端计时
+        const elapsedMs = aiMsg.baseDurationMs || (aiMsg.generateStartTime ? Date.now() - aiMsg.generateStartTime : 0);
+        aiMsg.isLoading = false;
+        aiMsg.isPendingApproval = true;
+        aiMsg.pendingApprovalData = {
+          message: data.message || '智能体制定了执行计划,等待您的审批...',
+          sessionId: data.session_id || getCurrentSessionId(),
+          threadId: data.thread_id,
+          interrupts: data.interrupts,
+          elapsedMs,
+          prevThinkingContent: aiMsg.thinkingContent,
+        };
+        pendingApprovalSessionId.value = data.session_id || getCurrentSessionId();
+        pendingApprovalThreadId.value = data.thread_id || '';
+        break;
+      }
     }
 
     const currentTask = taskList.value.find((t) => t.id === currentTaskId.value);
@@ -763,6 +1069,7 @@
       {
         message: userInput,
         session_id: getCurrentSessionId() || undefined,
+        mode: editMode.value,
       },
       (chunk: string) => {
         parseSSEData(chunk, aiMsgIndex);
@@ -828,6 +1135,7 @@
         file: file.originalFile!,
         session_id: getCurrentSessionId() || undefined,
         message: userInput || undefined,
+        mode: editMode.value,
       },
       (chunk: string) => {
         parseSSEData(chunk, aiMsgIndex);
@@ -864,6 +1172,7 @@
         }
       } else {
         await fetchSessionList();
+        fetchModelMsg();
 
         // 去除重复的空白任务,只保留一个
         taskList.value = taskList.value.filter((task, index) => {
@@ -908,6 +1217,16 @@
           }
         }
 
+        if (targetTask.sessionId) {
+          fetchContext(targetTask.sessionId);
+          fetchSessionMode(targetTask.sessionId);
+        } else {
+          contextUsed.value = 0;
+          contextPercent.value = 0;
+          contextBreakdown.value = { messages: 0, mcp: 0, skills: 0, system_prompt: 0, other: 0 };
+          editMode.value = 'full';
+        }
+
         messages.value = targetTask.messages && targetTask.messages.length > 0 ? [...targetTask.messages] : getDefaultMessages();
 
         previewFile.value = null;
@@ -1101,6 +1420,59 @@
         margin-left: 10px;
       }
 
+      .chat-header-title-wrapper {
+        display: flex;
+        align-items: center;
+        flex: 1;
+        min-width: 0;
+
+        &:hover .title-edit-btn {
+          opacity: 1;
+        }
+
+        .title-edit-btn {
+          width: 28px;
+          height: 28px;
+          flex-shrink: 0;
+          background-image: var(--img-chat-edit-icon);
+          background-repeat: no-repeat;
+          background-size: 16px 16px;
+          background-position: center;
+          cursor: pointer;
+          opacity: 0;
+          transition: opacity 0.2s;
+          border-radius: 4px;
+          margin-right: 4px;
+
+          &:hover {
+            background-color: rgba(10, 132, 255, 0.2);
+          }
+        }
+
+        .chat-header-title {
+          margin-left: 0;
+          flex: 1;
+          min-width: 0;
+        }
+
+        .title-edit-input {
+          flex: 1;
+          min-width: 0;
+          height: 28px;
+          padding: 0 8px;
+          font-size: 14px;
+          color: #e0e6ed;
+          background: rgba(10, 132, 255, 0.1);
+          border: 1px solid rgba(10, 132, 255, 0.5);
+          border-radius: 4px;
+          outline: none;
+
+          &:focus {
+            border-color: rgba(10, 132, 255, 0.8);
+          }
+        }
+      }
+
       .nav-btn {
         width: 28px;
         height: 28px;
@@ -1354,6 +1726,19 @@
           background-size: 100% 100%;
         }
       }
+      .chat-header-title-wrapper {
+        .title-edit-input {
+          height: 36px;
+          font-size: 16px;
+          background: rgba(10, 132, 255, 0.15);
+          border-color: rgba(10, 132, 255, 0.6);
+        }
+        .title-edit-btn {
+          width: 24px;
+          height: 24px;
+          background-size: 16px 16px;
+        }
+      }
       .chat-input-area {
         margin: 24px;
       }

+ 384 - 19
src/views/ventAI/manageAssistent/components/chatModal/ChatInputArea.vue

@@ -13,15 +13,28 @@
       </div>
     </div>
 
+    <!-- 已选会话引用标签 -->
+    <div v-if="selectedSessions.length > 0" class="session-chips">
+      <div v-for="session in selectedSessions" :key="session.id" class="session-chip">
+        <div class="session-chip-icon"></div>
+        <span class="session-chip-name">{{ session.name }}</span>
+        <CloseOutlined class="session-chip-remove" @click.stop="removeSession(session.id)" />
+      </div>
+    </div>
+
     <a-textarea
+      ref="textareaRef"
       :value="modelValue"
-      @update:value="emit('update:modelValue', $event)"
+      @update:value="handleTextareaInput"
       :rows="3"
       class="chat-input"
       placeholder="请输入你的问题"
       :auto-size="{ minRows: 3, maxRows: 6 }"
+      @keydown="handleSessionKeydown"
       @keydown.enter.prevent="handleEnterKey"
+      @keydown.backspace="handleBackspace"
     />
+
     <div class="input-actions">
       <div class="action-left">
         <div class="btn-upload-wrapper">
@@ -35,18 +48,18 @@
               <div class="popup-item-icon attach-icon"></div>
               <span class="popup-item-text">添加附件</span>
             </div>
-            <div class="popup-item" @click="handleInsertMention">
+            <!-- <div class="popup-item" @click="handleInsertMention">
               <div class="popup-item-icon mention-icon"></div>
               <span class="popup-item-text">插入 @ 提及</span>
-            </div>
+            </div> -->
             <div class="popup-item" @click="handleInsertSession">
               <div class="popup-item-icon session-icon"></div>
               <span class="popup-item-text">插入 # 会话</span>
             </div>
-            <div class="popup-item" @click="handleInsertCommand">
+            <!-- <div class="popup-item" @click="handleInsertCommand">
               <div class="popup-item-icon command-icon"></div>
               <span class="popup-item-text">插入 / 命令</span>
-            </div>
+            </div> -->
           </div>
         </div>
         <div class="action-divider"></div>
@@ -75,11 +88,11 @@
             </div>
           </div>
         </div>
-        <input ref="fileInputRef" type="file" style="display: none" @change="emit('file-change', $event)" accept=".pdf" />
+        <input ref="fileInputRef" type="file" style="display: none" @change="emit('file-change', $event)" />
       </div>
       <div class="action-right">
-        <div class="btn-context-wrapper" ref="contextPopupRef">
-          <div class="btn-context" @click="toggleContextPopup" @mouseenter="showContextPopup = true" @mouseleave="showContextPopup = false">
+        <div class="btn-context-wrapper" ref="contextPopupRef" @mouseenter="showContextPopup = true" @mouseleave="showContextPopup = false">
+          <div class="btn-context" @click="toggleContextPopup">
             <svg class="context-ring" viewBox="0 0 32 32">
               <circle class="context-ring-bg" cx="16" cy="16" r="13" />
               <circle
@@ -105,9 +118,31 @@
             <div class="context-popup-bar-track">
               <div class="context-popup-bar-fill" :style="{ width: contextPercent + '%', background: contextColor }"></div>
             </div>
+            <div class="context-breakdown">
+              <div class="breakdown-item">
+                <span class="breakdown-label">历史消息</span>
+                <span class="breakdown-value">{{ formatTokens(props.contextBreakdown.messages) }}</span>
+              </div>
+              <div class="breakdown-item">
+                <span class="breakdown-label">MCP工具</span>
+                <span class="breakdown-value">{{ formatTokens(props.contextBreakdown.mcp) }}</span>
+              </div>
+              <div class="breakdown-item">
+                <span class="breakdown-label">技能描述</span>
+                <span class="breakdown-value">{{ formatTokens(props.contextBreakdown.skills) }}</span>
+              </div>
+              <div class="breakdown-item">
+                <span class="breakdown-label">系统提示词</span>
+                <span class="breakdown-value">{{ formatTokens(props.contextBreakdown.system_prompt) }}</span>
+              </div>
+              <div class="breakdown-item">
+                <span class="breakdown-label">其他</span>
+                <span class="breakdown-value">{{ formatTokens(props.contextBreakdown.other) }}</span>
+              </div>
+            </div>
           </div>
         </div>
-        <div class="action-divider"></div>
+        <!-- <div class="action-divider"></div>
         <div class="btn-model-wrapper" ref="modelPopupRef">
           <a-tooltip title="选择模型">
             <div class="btn-model" @click="toggleModelPopup">
@@ -130,7 +165,7 @@
               <div class="model-item-check" :class="{ visible: selectedModel.key === item.key }"></div>
             </div>
           </div>
-        </div>
+        </div> -->
         <div class="action-divider"></div>
         <div class="btn-think-wrapper" ref="thinkPopupRef">
           <a-tooltip title="思考级别">
@@ -157,20 +192,37 @@
           </div>
         </div>
         <div class="action-divider"></div>
-        <div class="btn-send" :class="{ loading, disabled: !modelValue.trim() }" @click="handleSend">
+        <div class="btn-send" :class="{ disabled: !modelValue.trim() || loading }" @click="handleSend">
           <div class="send-icon"></div>
         </div>
       </div>
+      <!-- 会话列表弹框 -->
+      <div v-if="showSessionPopup" ref="sessionPopupRef" class="session-popup" @click.stop @pointerdown.stop>
+        <div class="session-list">
+          <div
+            v-for="(task, index) in filteredSessionList"
+            :key="task.id"
+            class="session-item"
+            :class="{ active: index === activeSessionIndex }"
+            @click="handleSelectSession(task)"
+          >
+            <div class="session-item-icon"></div>
+            <span class="session-item-name">{{ task.name }}</span>
+          </div>
+          <div v-if="filteredSessionList.length === 0" class="session-empty">暂无匹配会话</div>
+        </div>
+        <div class="session-popup-footer">输入内容以搜索对话</div>
+      </div>
     </div>
   </div>
 </template>
 
 <script setup lang="ts">
-  import { ref, computed } from 'vue';
+  import { ref, computed, watch, nextTick } from 'vue';
   import { onClickOutside } from '@vueuse/core';
   import { CloseOutlined } from '@ant-design/icons-vue';
   import { formatFileSize } from './utils';
-  import type { AttachedFile } from './types';
+  import type { AttachedFile, Task } from './types';
 
   const props = withDefaults(
     defineProps<{
@@ -179,10 +231,20 @@
       loading: boolean;
       contextUsed?: number;
       contextMax?: number;
+      contextPercent?: number;
+      contextBreakdown?: { messages: number; mcp: number; skills: number; system_prompt: number; other: number };
+      initialThinkLevel?: string;
+      initialEditMode?: string;
+      taskList?: Task[];
     }>(),
     {
       contextUsed: 0,
-      contextMax: 200000,
+      contextMax: 1000000,
+      contextPercent: 0,
+      contextBreakdown: () => ({ messages: 0, mcp: 0, skills: 0, system_prompt: 0, other: 0 }),
+      initialThinkLevel: 'off',
+      initialEditMode: 'full',
+      taskList: () => [],
     }
   );
 
@@ -194,6 +256,8 @@
     (e: 'remove-pending-file'): void;
     (e: 'model-change', modelKey: string): void;
     (e: 'think-level-change', levelKey: string): void;
+    (e: 'edit-mode-change', modeKey: string): void;
+    (e: 'insert-session', taskId: string, taskName: string): void;
   }>();
 
   const handleSelectModel = (item: ModelOption) => {
@@ -211,8 +275,7 @@
   const showContextPopup = ref(false);
 
   const contextPercent = computed(() => {
-    if (!props.contextMax) return 0;
-    return Math.min(100, Math.round((props.contextUsed / props.contextMax) * 100));
+    return props.contextPercent;
   });
 
   const contextCircumference = computed(() => 2 * Math.PI * 13);
@@ -234,6 +297,11 @@
     return props.contextMax.toLocaleString();
   });
 
+  const formatTokens = (tokens: number) => {
+    if (tokens >= 10000) return (tokens / 10000).toFixed(1) + '万';
+    return tokens.toLocaleString();
+  };
+
   const toggleContextPopup = () => {
     showContextPopup.value = !showContextPopup.value;
   };
@@ -289,6 +357,15 @@
 
   const selectedThinkLevel = ref<ThinkLevelOption>(thinkLevelOptions[0]);
 
+  watch(
+    () => props.initialThinkLevel,
+    (level) => {
+      const found = thinkLevelOptions.find((opt) => opt.key === level);
+      if (found) selectedThinkLevel.value = found;
+    },
+    { immediate: true }
+  );
+
   const toggleThinkPopup = () => {
     showThinkPopup.value = !showThinkPopup.value;
   };
@@ -313,14 +390,23 @@
   }
 
   const editModeOptions: EditModeOption[] = [
-    { key: 'confirm', label: '变更前确认', desc: '改文件前先问我' },
-    { key: 'auto', label: '自动编辑', desc: '自动编辑文件' },
+    // { key: 'confirm', label: '变更前确认', desc: '改文件前先问我' },
+    // { key: 'auto', label: '自动编辑', desc: '自动编辑文件' },
     { key: 'plan', label: '计划模式', desc: '编辑前先出计划' },
     { key: 'full', label: '完全访问', desc: '减少确认次数' },
   ];
 
   const selectedEditMode = ref<EditModeOption>(editModeOptions[1]);
 
+  watch(
+    () => props.initialEditMode,
+    (mode) => {
+      const found = editModeOptions.find((opt) => opt.key === mode);
+      if (found) selectedEditMode.value = found;
+    },
+    { immediate: true }
+  );
+
   onClickOutside(editModePopupRef, () => {
     showEditModePopup.value = false;
   });
@@ -332,6 +418,7 @@
   const handleSelectEditMode = (item: EditModeOption) => {
     selectedEditMode.value = item;
     showEditModePopup.value = false;
+    emit('edit-mode-change', item.key);
   };
 
   onClickOutside(popupRef, () => {
@@ -351,16 +438,140 @@
     showPopup.value = false;
   };
 
+  // 会话列表弹框
+  const textareaRef = ref<InstanceType<any>>();
+  const sessionPopupRef = ref<HTMLElement>();
+  const showSessionPopup = ref(false);
+  const activeSessionIndex = ref(0);
+  const selectedSessions = ref<Array<{ id: string; name: string }>>([]);
+  let isRemovingTag = false;
+  let sessionJustSelected = false;
+
+  const sessionSearchText = computed(() => {
+    const val = props.modelValue || '';
+    const hashIndex = val.lastIndexOf('#');
+    if (hashIndex === -1) return '';
+    const afterHash = val.substring(hashIndex + 1);
+    if (afterHash.includes(' ')) return '';
+    return afterHash.toLowerCase();
+  });
+
+  const filteredSessionList = computed(() => {
+    const list = props.taskList.filter((t) => t.sessionId);
+    if (!sessionSearchText.value) return list;
+    return list.filter((t) => t.name.toLowerCase().includes(sessionSearchText.value));
+  });
+
+  watch(filteredSessionList, () => {
+    activeSessionIndex.value = 0;
+  });
+
+  watch(showSessionPopup, (val) => {
+    if (val) activeSessionIndex.value = 0;
+  });
+
+  const handleSessionKeydown = (e: KeyboardEvent) => {
+    if (!showSessionPopup.value || filteredSessionList.value.length === 0) return;
+    if (e.key === 'ArrowDown') {
+      e.preventDefault();
+      activeSessionIndex.value = (activeSessionIndex.value + 1) % filteredSessionList.value.length;
+    } else if (e.key === 'ArrowUp') {
+      e.preventDefault();
+      activeSessionIndex.value = (activeSessionIndex.value - 1 + filteredSessionList.value.length) % filteredSessionList.value.length;
+    } else if (e.key === 'Enter') {
+      e.preventDefault();
+      const task = filteredSessionList.value[activeSessionIndex.value];
+      if (task) {
+        sessionJustSelected = true;
+        handleSelectSession(task);
+      }
+    }
+  };
+
   const handleInsertSession = () => {
     showPopup.value = false;
+    showSessionPopup.value = true;
+    const cur = props.modelValue || '';
+    const needsSpace = cur && !cur.endsWith(' ') ? ' ' : '';
+    emit('update:modelValue', `${cur}${needsSpace}#`);
+    nextTick(() => {
+      const el = textareaRef.value?.$el?.querySelector('textarea') || textareaRef.value?.$el;
+      if (el) el.focus();
+    });
+  };
+
+  const handleSelectSession = (task: Task) => {
+    selectedSessions.value = [{ id: task.id, name: task.name }];
+    showSessionPopup.value = false;
+    removeSearchTag();
+    emit('insert-session', task.id, task.name);
+  };
+
+  const removeSearchTag = () => {
+    isRemovingTag = true;
+    const val = props.modelValue || '';
+    const hashIndex = val.lastIndexOf('#');
+    if (hashIndex === -1) {
+      isRemovingTag = false;
+      return;
+    }
+    emit('update:modelValue', val.substring(0, hashIndex).trimEnd());
+    nextTick(() => {
+      isRemovingTag = false;
+    });
+  };
+
+  watch(
+    () => props.modelValue,
+    (newVal, oldVal) => {
+      if (isRemovingTag) return;
+      if (oldVal && !newVal) selectedSessions.value = [];
+    }
+  );
+
+  const removeSession = (sessionId: string) => {
+    selectedSessions.value = selectedSessions.value.filter((s) => s.id !== sessionId);
+  };
+
+  const handleTextareaInput = (val: string) => {
+    emit('update:modelValue', val);
+    const hashIndex = val.lastIndexOf('#');
+    if (hashIndex === -1) {
+      showSessionPopup.value = false;
+    } else {
+      const afterHash = val.substring(hashIndex + 1);
+      if (afterHash.includes(' ')) {
+        showSessionPopup.value = false;
+      } else {
+        showSessionPopup.value = true;
+      }
+    }
+  };
+
+  const handleBackspace = (e: KeyboardEvent) => {
+    const val = props.modelValue || '';
+    if (val) return;
+    if (selectedSessions.value.length > 0) {
+      e.preventDefault();
+      selectedSessions.value.pop();
+    }
   };
 
+  onClickOutside(sessionPopupRef, () => {
+    showSessionPopup.value = false;
+  });
+
   const handleInsertCommand = () => {
     showPopup.value = false;
   };
 
   const handleEnterKey = (e: KeyboardEvent) => {
-    if (!e.shiftKey && props.modelValue.trim()) {
+    if (showSessionPopup.value) return;
+    if (sessionJustSelected) {
+      sessionJustSelected = false;
+      return;
+    }
+    if (!e.shiftKey && props.modelValue.trim() && !props.loading) {
       emit('send');
     }
   };
@@ -376,6 +587,7 @@
 
 <style scoped lang="less">
   .chat-input-area {
+    position: relative;
     padding: 5px 10px;
     flex-shrink: 0;
     background-color: #002f4a;
@@ -746,6 +958,15 @@
           border-radius: 8px;
           min-width: 200px;
           padding: 14px 16px;
+
+          &::before {
+            content: '';
+            position: absolute;
+            bottom: -12px;
+            left: 0;
+            right: 0;
+            height: 12px;
+          }
           z-index: 1000;
           box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
 
@@ -796,6 +1017,29 @@
               transition: width 0.3s ease;
             }
           }
+
+          .context-breakdown {
+            margin-top: 10px;
+            padding-top: 10px;
+            border-top: 1px solid rgba(255, 255, 255, 0.08);
+
+            .breakdown-item {
+              display: flex;
+              justify-content: space-between;
+              align-items: center;
+              padding: 3px 0;
+
+              .breakdown-label {
+                font-size: 12px;
+                color: #8b949e;
+              }
+
+              .breakdown-value {
+                font-size: 12px;
+                color: #e0e6ed;
+              }
+            }
+          }
         }
         .btn-model-wrapper {
           position: relative;
@@ -1000,5 +1244,126 @@
         }
       }
     }
+
+    .session-chips {
+      display: flex;
+      flex-wrap: wrap;
+      gap: 6px;
+      margin-bottom: 5px;
+
+      .session-chip {
+        display: inline-flex;
+        align-items: center;
+        gap: 4px;
+        padding: 3px 6px 3px 4px;
+        background: rgba(10, 132, 255, 0.15);
+        border: 1px solid rgba(10, 132, 255, 0.3);
+        border-radius: 4px;
+        font-size: 12px;
+        color: #e0e6ed;
+        cursor: default;
+
+        .session-chip-icon {
+          width: 16px;
+          height: 16px;
+          background-image: var(--img-chat-popup-session-icon);
+          background-repeat: no-repeat;
+          background-size: 100% 100%;
+          flex-shrink: 0;
+        }
+
+        .session-chip-name {
+          max-width: 150px;
+          overflow: hidden;
+          text-overflow: ellipsis;
+          white-space: nowrap;
+        }
+
+        .session-chip-remove {
+          font-size: 10px;
+          color: #8b949e;
+          cursor: pointer;
+          padding: 0 2px;
+          transition: color 0.2s;
+
+          &:hover {
+            color: #ff4d4f;
+          }
+        }
+      }
+    }
+
+    .session-popup {
+      position: absolute;
+      bottom: 100%;
+      left: 0;
+      right: 0;
+      margin-bottom: 8px;
+      background: #0a2a3f;
+      border: 1px solid #1a4a6f;
+      border-radius: 8px;
+      max-height: 300px;
+      display: flex;
+      flex-direction: column;
+      z-index: 1000;
+      box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
+
+      .session-list {
+        flex: 1;
+        overflow-y: auto;
+        padding: 6px;
+
+        .session-item {
+          display: flex;
+          align-items: center;
+          gap: 10px;
+          padding: 8px 10px;
+          cursor: pointer;
+          border-radius: 6px;
+          transition: background 0.2s;
+
+          &:hover {
+            background: rgba(10, 132, 255, 0.15);
+          }
+
+          &.active {
+            background: rgba(10, 132, 255, 0.2);
+          }
+
+          .session-item-icon {
+            width: 18px;
+            height: 18px;
+            background-image: var(--img-chat-popup-session-icon);
+            background-repeat: no-repeat;
+            background-size: 100% 100%;
+            flex-shrink: 0;
+          }
+
+          .session-item-name {
+            font-size: 13px;
+            color: #e0e6ed;
+            overflow: hidden;
+            text-overflow: ellipsis;
+            white-space: nowrap;
+          }
+        }
+
+        .session-empty {
+          text-align: center;
+          color: #8b949e;
+          font-size: 13px;
+          padding: 20px 0;
+        }
+      }
+
+      .session-popup-footer {
+        padding: 8px 14px;
+        font-size: 12px;
+        color: #8b949e;
+        text-align: center;
+        border-top: 1px solid rgba(63, 80, 106, 0.3);
+        flex-shrink: 0;
+      }
+    }
   }
 </style>

+ 165 - 4
src/views/ventAI/manageAssistent/components/chatModal/ChatMessages.vue

@@ -24,6 +24,20 @@
                     <span class="thinking-title">思考过程 持续了几秒</span>
                   </div>
                 </div>
+
+                <!-- thinking_token 思考内容 -->
+                <div v-if="message.thinkingContent" class="thinking-content-card">
+                  <div class="thinking-content-header" @click="toggleThinkingContent(index)">
+                    <span class="thinking-content-title">模型思考</span>
+                    <div :class="['thinking-arrow', { expanded: !collapsedThinkingContent[index] }]">
+                      <img :src="getCssUrl('--img-chat-arrow-icon')" width="10" height="10" />
+                    </div>
+                  </div>
+                  <div v-show="!collapsedThinkingContent[index]" class="thinking-content-body">
+                    {{ message.thinkingContent }}
+                  </div>
+                </div>
+
                 <div class="thinking-body">
                   <div
                     v-for="(group, groupIdx) in groupStepsByAgent(message.thinkingSteps)"
@@ -109,6 +123,19 @@
               <span class="dot"></span>
               <span class="dot"></span>
             </div>
+
+            <!-- 计划审批 UI -->
+            <div v-if="message.isPendingApproval" class="approval-card">
+              <div class="approval-icon"></div>
+              <div class="approval-content">
+                <div class="approval-title">执行计划待审批</div>
+                <div class="approval-desc">{{ message.pendingApprovalData?.message }}</div>
+                <div class="approval-actions">
+                  <button class="approval-btn approve-btn" @click="emit('approve')">同意</button>
+                  <button class="approval-btn reject-btn" @click="emit('reject')">拒绝</button>
+                </div>
+              </div>
+            </div>
           </div>
           <!-- AI消息操作栏 -->
           <div class="message-actions">
@@ -193,16 +220,24 @@
   let timerInterval: ReturnType<typeof setInterval> | null = null;
 
   const getDurationText = (message: Message): string => {
-    // 优先使用后端返回的 duration_ms
+    // 优先使用后端返回的 duration_ms(已包含 baseDurationMs 累加)
     if (message.durationMs) {
       const totalSec = Math.round(message.durationMs / 1000);
       const min = Math.floor(totalSec / 60);
       const sec = totalSec % 60;
       return `已工作 ${min}分${sec.toString().padStart(2, '0')}秒`;
     }
-    // 前端实时计时
+    // 审批等待中 - 暂停显示中断前的累计时长
+    if (message.isPendingApproval && message.pendingApprovalData?.elapsedMs) {
+      const totalSec = Math.round(message.pendingApprovalData.elapsedMs / 1000);
+      const min = Math.floor(totalSec / 60);
+      const sec = totalSec % 60;
+      return `已工作 ${min}分${sec.toString().padStart(2, '0')}秒`;
+    }
+    // 前端实时计时(含中断前累计时长)
     if (message.generateStartTime && message.isLoading) {
-      const elapsed = Math.floor((nowTs.value - message.generateStartTime) / 1000);
+      const baseMs = message.baseDurationMs || 0;
+      const elapsed = Math.floor((nowTs.value - message.generateStartTime + baseMs) / 1000);
       const min = Math.floor(elapsed / 60);
       const sec = elapsed % 60;
       return `已工作 ${min}分${sec.toString().padStart(2, '0')}秒`;
@@ -217,10 +252,13 @@
   const emit = defineEmits<{
     (e: 'file-preview', file: AttachedFile): void;
     (e: 'download-word', url: string): void;
+    (e: 'approve'): void;
+    (e: 'reject'): void;
   }>();
 
   const messagesRef = ref<HTMLElement>();
   const collapsedThinking = reactive<Record<number, boolean>>({});
+  const collapsedThinkingContent = reactive<Record<number, boolean>>({});
   const expandedAgents = reactive<Record<string, boolean>>({});
   const isAtBottom = ref(true);
   const copiedMessageIndex = ref(-1);
@@ -342,6 +380,10 @@
     collapsedThinking[index] = !collapsedThinking[index];
   };
 
+  const toggleThinkingContent = (index: number) => {
+    collapsedThinkingContent[index] = !collapsedThinkingContent[index];
+  };
+
   const toggleAgent = (key: string) => {
     expandedAgents[key] = !expandedAgents[key];
   };
@@ -450,7 +492,6 @@
           display: flex;
           margin-bottom: 8px;
           opacity: 0;
-
         }
 
         .message-body {
@@ -530,6 +571,54 @@
               }
             }
 
+            .thinking-content-card {
+              margin: 4px 14px 8px;
+              border: 1px solid rgba(63, 80, 106, 0.25);
+              border-radius: 6px;
+              overflow: hidden;
+
+              .thinking-content-header {
+                display: flex;
+                align-items: center;
+                justify-content: space-between;
+                padding: 8px 12px;
+                cursor: pointer;
+                user-select: none;
+                transition: background 0.2s;
+
+                &:hover {
+                  background: rgba(10, 132, 255, 0.08);
+                }
+
+                .thinking-content-title {
+                  font-size: 12px;
+                  font-weight: 500;
+                  color: #8b949e;
+                }
+
+                .thinking-arrow {
+                  display: flex;
+                  align-items: center;
+                  transition: transform 0.2s;
+
+                  &.expanded {
+                    transform: rotate(180deg);
+                  }
+                }
+              }
+
+              .thinking-content-body {
+                padding: 8px 12px;
+                max-height: 200px;
+                overflow-y: auto;
+                font-size: 13px;
+                line-height: 1.6;
+                color: #8b949e;
+                white-space: pre-wrap;
+                word-break: break-word;
+              }
+            }
+
             .thinking-body {
               padding: 8px 14px;
 
@@ -834,6 +923,78 @@
             opacity: 1;
           }
         }
+
+        .approval-card {
+          display: flex;
+          align-items: flex-start;
+          gap: 12px;
+          padding: 14px 16px;
+          margin-top: 12px;
+          background: rgba(10, 132, 255, 0.08);
+          border: 1px solid rgba(10, 132, 255, 0.25);
+          border-radius: 8px;
+
+          .approval-icon {
+            width: 36px;
+            height: 36px;
+            background-image: var(--img-chat-popup-command-icon);
+            background-repeat: no-repeat;
+            background-size: 100% 100%;
+            flex-shrink: 0;
+          }
+
+          .approval-content {
+            flex: 1;
+
+            .approval-title {
+              font-size: 14px;
+              font-weight: 600;
+              color: #e0e6ed;
+              margin-bottom: 6px;
+            }
+
+            .approval-desc {
+              font-size: 13px;
+              color: #8b949e;
+              line-height: 1.5;
+              margin-bottom: 12px;
+            }
+
+            .approval-actions {
+              display: flex;
+              gap: 10px;
+
+              .approval-btn {
+                padding: 6px 20px;
+                border-radius: 6px;
+                font-size: 13px;
+                font-weight: 500;
+                cursor: pointer;
+                border: none;
+                transition: all 0.2s;
+
+                &.approve-btn {
+                  background: #0a84ff;
+                  color: #fff;
+
+                  &:hover {
+                    background: #3a9bfd;
+                  }
+                }
+
+                &.reject-btn {
+                  background: rgba(255, 255, 255, 0.08);
+                  color: #e0e6ed;
+                  border: 1px solid rgba(63, 80, 106, 0.5);
+
+                  &:hover {
+                    background: rgba(255, 255, 255, 0.15);
+                  }
+                }
+              }
+            }
+          }
+        }
       }
 
       .user-message {

+ 515 - 0
src/views/ventAI/manageAssistent/components/chatModal/SkillModal.vue

@@ -0,0 +1,515 @@
+<template>
+  <a-modal :visible="visible" title="模型技能" :width="1000" :footer="null" destroyOnClose @cancel="emit('close')" class="skill-modal">
+    <div class="skill-filter-bar">
+      <a-input v-model:value="searchKeyword" placeholder="搜索技能..." allow-clear size="small" class="skill-search-input" />
+      <a-select v-model:value="filterStatus" size="small" class="skill-filter-select">
+        <a-select-option value="all">全部</a-select-option>
+        <a-select-option value="enabled">已启用</a-select-option>
+        <a-select-option value="disabled">未启用</a-select-option>
+      </a-select>
+    </div>
+    <div class="skill-summary">
+      <span class="skill-summary-label">技能</span>
+      <span class="skill-summary-count">{{ filteredSkills.length }}项</span>
+    </div>
+    <div class="skill-list" v-if="!loading">
+      <div v-for="skill in filteredSkills" :key="skill.name" class="skill-item" @click="handleSkillClick(skill)">
+        <div class="skill-header">
+          <div class="skill-name-wrapper">
+            <div class="skill-item-icon"></div>
+            <span class="skill-name">{{ skill.display_name }}</span>
+          </div>
+          <a-tag :color="skill.enabled ? 'green' : 'default'" class="skill-toggle-tag" @click.stop="handleToggleSkill(skill)">
+            {{ skill.enabled ? '已启用' : '未启用' }}
+          </a-tag>
+        </div>
+        <div class="skill-desc">{{ skill.description }}</div>
+        <div v-if="skill.agents && skill.agents.length > 0" class="skill-agents">
+          <div class="agents-label">
+            <div class="agents-label-icon"></div>
+            <span>关联智能体</span>
+          </div>
+          <div class="agents-tags">
+            <span v-for="agent in skill.agents" :key="agent" class="agent-tag">{{ agent }}</span>
+          </div>
+        </div>
+        <div v-if="skillDetailMap[skill.name]" class="skill-detail">
+          <div class="skill-detail-content" v-html="renderMarkdown(skillDetailMap[skill.name])"></div>
+        </div>
+        <div v-else-if="detailLoadingMap[skill.name]" class="skill-detail-loading">
+          <a-spin size="small" />
+        </div>
+      </div>
+      <a-empty v-if="filteredSkills.length === 0" description="暂无技能" />
+    </div>
+    <div class="skill-loading" v-else>
+      <a-spin />
+    </div>
+  </a-modal>
+</template>
+
+<script setup lang="ts">
+  import { ref, reactive, computed, watch } from 'vue';
+  import { getSkills, toggleSkill, getSkillDetails } from '../../api';
+  import { renderMarkdown } from './utils';
+  import { message } from 'ant-design-vue';
+
+  interface Skill {
+    name: string;
+    display_name: string;
+    path: string;
+    description: string;
+    enabled: boolean;
+    agents: string[];
+    has_skill_md: boolean;
+  }
+
+  const props = defineProps<{ visible: boolean }>();
+  const emit = defineEmits<{ (e: 'close'): void }>();
+
+  const skills = ref<Skill[]>([]);
+  const loading = ref(false);
+  const skillDetailMap = reactive<Record<string, string>>({});
+  const detailLoadingMap = reactive<Record<string, boolean>>({});
+  const searchKeyword = ref('');
+  const filterStatus = ref<'all' | 'enabled' | 'disabled'>('all');
+
+  const filteredSkills = computed(() => {
+    return skills.value.filter((skill) => {
+      const keyword = searchKeyword.value.trim();
+      const matchKeyword = !keyword || skill.display_name.includes(keyword) || skill.name.includes(keyword);
+      const matchStatus = filterStatus.value === 'all' || (filterStatus.value === 'enabled' ? skill.enabled : !skill.enabled);
+      return matchKeyword && matchStatus;
+    });
+  });
+
+  const fetchSkills = async () => {
+    loading.value = true;
+    try {
+      const res = await getSkills();
+      skills.value = res?.skills || [];
+    } catch {
+      skills.value = [];
+    } finally {
+      loading.value = false;
+    }
+  };
+
+  const handleToggleSkill = async (skill: Skill) => {
+    try {
+      await toggleSkill(skill.name);
+      skill.enabled = !skill.enabled;
+      message.success(`${skill.display_name} 已${skill.enabled ? '启用' : '禁用'}`);
+    } catch {
+      message.error('操作失败,请重试');
+    }
+  };
+
+  const handleSkillClick = async (skill: Skill) => {
+    return;
+    if (skillDetailMap[skill.name]) {
+      delete skillDetailMap[skill.name];
+      return;
+    }
+    detailLoadingMap[skill.name] = true;
+    try {
+      const res = await getSkillDetails(skill.name);
+      skillDetailMap[skill.name] = res?.content || '';
+    } catch {
+      message.error('获取技能详情失败');
+    } finally {
+      detailLoadingMap[skill.name] = false;
+    }
+  };
+
+  watch(
+    () => props.visible,
+    (val) => {
+      if (val) fetchSkills();
+    }
+  );
+</script>
+
+<style scoped lang="less">
+  .skill-filter-bar {
+    display: flex;
+    gap: 12px;
+    margin-bottom: 16px;
+    padding: 0 2px;
+
+    .skill-search-input {
+      flex: 1;
+      height: 36px;
+      background: rgba(10, 132, 255, 0.08);
+      border: 1px solid rgba(63, 80, 106, 0.4);
+      border-radius: 6px;
+      color: #e0e6ed;
+      transition: all 0.3s;
+
+      &:hover,
+      &:focus {
+        border-color: rgba(10, 132, 255, 0.5);
+        background: rgba(10, 132, 255, 0.12);
+      }
+
+      :deep(.zxm-input) {
+        background: transparent;
+        color: #e0e6ed;
+        font-size: 13px;
+      }
+    }
+
+    .skill-filter-select {
+      width: 130px;
+      height: 36px;
+
+      :deep(.zxm-select-selector) {
+        background: rgba(10, 132, 255, 0.08) !important;
+        border: 1px solid rgba(63, 80, 106, 0.4) !important;
+        border-radius: 6px !important;
+        color: #e0e6ed;
+        height: 36px;
+      }
+      :deep(.zxm-select-selection-item) {
+        line-height: 34px !important;
+      }
+    }
+  }
+
+  .skill-summary {
+    display: flex;
+    align-items: center;
+    gap: 6px;
+    padding: 0 2px;
+    margin-bottom: 12px;
+    font-size: 13px;
+
+    .skill-summary-label {
+      color: #e8edf3;
+      font-weight: 500;
+    }
+
+    .skill-summary-count {
+      color: #6a7a8a;
+    }
+  }
+
+  .skill-list {
+    max-height: 420px;
+    overflow-y: auto;
+    padding: 0 2px;
+
+    &::-webkit-scrollbar {
+      width: 6px;
+    }
+
+    &::-webkit-scrollbar-track {
+      background: transparent;
+    }
+
+    &::-webkit-scrollbar-thumb {
+      background: rgba(63, 80, 106, 0.4);
+      border-radius: 3px;
+
+      &:hover {
+        background: rgba(63, 80, 106, 0.6);
+      }
+    }
+  }
+
+  .skill-loading {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    padding: 80px 0;
+  }
+
+  .skill-item {
+    padding: 16px 18px;
+    margin-bottom: 8px;
+    border-radius: 8px;
+    border: 1px solid rgba(63, 80, 106, 0.15);
+    background: rgba(10, 132, 255, 0.03);
+    cursor: pointer;
+    transition: all 0.25s ease;
+
+    &:hover {
+      background: rgba(10, 132, 255, 0.08);
+      border-color: rgba(10, 132, 255, 0.25);
+      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+    }
+
+    &:last-child {
+      margin-bottom: 0;
+    }
+  }
+
+  .skill-header {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    margin-bottom: 10px;
+  }
+
+  .skill-name-wrapper {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+  }
+
+  .skill-item-icon {
+    width: 24px;
+    height: 24px;
+    flex-shrink: 0;
+    background-image: var(--img-chat-skill-item-icon);
+    background-repeat: no-repeat;
+    background-size: 100% 100%;
+    background-position: center;
+    filter: drop-shadow(0 0 4px rgba(10, 132, 255, 0.3));
+  }
+
+  .skill-name {
+    font-size: 15px;
+    font-weight: 600;
+    color: #e8edf3;
+    letter-spacing: 0.3px;
+  }
+
+  .skill-toggle-tag {
+    cursor: pointer;
+    user-select: none;
+    padding: 2px 12px;
+    border-radius: 12px;
+    font-size: 12px;
+    transition: all 0.2s;
+    letter-spacing: 0.5px;
+
+    &:hover {
+      opacity: 0.85;
+      transform: scale(1.02);
+    }
+  }
+
+  .skill-desc {
+    font-size: 13px;
+    color: #8b949e;
+    line-height: 1.7;
+    margin-bottom: 10px;
+    padding-left: 34px;
+  }
+
+  .skill-label {
+    color: #5a6a7a;
+    margin-right: 4px;
+    font-size: 12px;
+  }
+
+  .skill-agents {
+    margin-top: 10px;
+    margin-left: 34px;
+    padding: 10px 14px;
+    background: rgba(10, 132, 255, 0.04);
+    border: 1px solid rgba(63, 80, 106, 0.18);
+    border-radius: 6px;
+    display: flex;
+    align-items: center;
+    gap: 12px;
+
+    .agents-label {
+      display: flex;
+      align-items: center;
+      gap: 6px;
+      font-size: 12px;
+      color: #6a7a8a;
+      white-space: nowrap;
+      flex-shrink: 0;
+
+      .agents-label-icon {
+        width: 14px;
+        height: 14px;
+        background-image: var(--img-chat-agent-icon);
+        background-repeat: no-repeat;
+        background-size: 100% 100%;
+        background-position: center;
+        opacity: 0.6;
+      }
+    }
+
+    .agents-tags {
+      display: flex;
+      align-items: center;
+      flex-wrap: wrap;
+      gap: 6px;
+
+      .agent-tag {
+        display: inline-flex;
+        align-items: center;
+        padding: 3px 10px;
+        font-size: 12px;
+        color: #5cb8ff;
+        background: rgba(10, 132, 255, 0.1);
+        border: 1px solid rgba(10, 132, 255, 0.2);
+        border-radius: 4px;
+        letter-spacing: 0.3px;
+        transition: all 0.2s;
+
+        &:hover {
+          background: rgba(10, 132, 255, 0.18);
+          border-color: rgba(10, 132, 255, 0.35);
+        }
+      }
+    }
+  }
+
+  .skill-detail {
+    margin-top: 14px;
+    margin-left: 34px;
+    padding: 14px 16px;
+    background: rgba(10, 132, 255, 0.05);
+    border: 1px solid rgba(10, 132, 255, 0.15);
+    border-radius: 6px;
+    position: relative;
+    overflow: hidden;
+
+    .skill-detail-content {
+      font-size: 13px;
+      color: #c9d1d9;
+      line-height: 1.8;
+      word-break: break-word;
+
+      :deep(h1),
+      :deep(h2),
+      :deep(h3),
+      :deep(h4) {
+        color: #e8edf3;
+        margin: 12px 0 8px;
+        font-weight: 600;
+      }
+      :deep(h1) {
+        font-size: 18px;
+      }
+      :deep(h2) {
+        font-size: 16px;
+      }
+      :deep(h3) {
+        font-size: 14px;
+      }
+
+      :deep(p) {
+        margin: 6px 0;
+      }
+
+      :deep(ul),
+      :deep(ol) {
+        padding-left: 20px;
+        margin: 6px 0;
+      }
+
+      :deep(code) {
+        background: rgba(10, 132, 255, 0.12);
+        padding: 1px 5px;
+        border-radius: 3px;
+        font-size: 12px;
+        color: #79c0ff;
+      }
+
+      :deep(pre) {
+        background: rgba(0, 0, 0, 0.25);
+        padding: 12px;
+        border-radius: 6px;
+        overflow-x: auto;
+        margin: 8px 0;
+      }
+
+      :deep(pre code) {
+        background: transparent;
+        padding: 0;
+        color: #c9d1d9;
+      }
+
+      :deep(blockquote) {
+        border-left: 3px solid rgba(10, 132, 255, 0.4);
+        padding-left: 12px;
+        margin: 8px 0;
+        color: #8b949e;
+      }
+
+      :deep(table) {
+        border-collapse: collapse;
+        margin: 8px 0;
+        width: 100%;
+      }
+
+      :deep(th),
+      :deep(td) {
+        border: 1px solid rgba(63, 80, 106, 0.4);
+        padding: 6px 10px;
+        text-align: left;
+      }
+
+      :deep(th) {
+        background: rgba(10, 132, 255, 0.1);
+        font-weight: 600;
+      }
+
+      :deep(strong) {
+        font-weight: 600;
+        color: #e8edf3;
+      }
+
+      :deep(a) {
+        color: #58a6ff;
+        text-decoration: none;
+
+        &:hover {
+          text-decoration: underline;
+        }
+      }
+    }
+  }
+
+  .skill-detail-loading {
+    margin-top: 14px;
+    margin-left: 34px;
+    display: flex;
+    justify-content: center;
+    padding: 16px 0;
+  }
+</style>
+
+<style lang="less">
+  .skill-modal {
+    .zxm-modal-content {
+      background: linear-gradient(135deg, #0a1628 0%, #0d2137 50%, #0a1e35 100%);
+      border: 1px solid rgba(63, 80, 106, 0.4);
+      border-radius: 12px;
+      box-shadow:
+        0 8px 32px rgba(0, 0, 0, 0.4),
+        0 0 0 1px rgba(10, 132, 255, 0.1);
+    }
+
+    .zxm-modal-header {
+      background: transparent;
+      border-bottom: 1px solid rgba(63, 80, 106, 0.25);
+      padding: 18px 24px;
+      border-radius: 10px 10px 0 0;
+    }
+
+    .zxm-modal-title {
+      color: #e8edf3;
+      font-size: 18px;
+      font-weight: 600;
+      letter-spacing: 1px;
+    }
+
+    .zxm-modal-body {
+      padding: 20px 24px 24px;
+    }
+
+    .zxm-modal-close {
+      .zxm-modal-close-x {
+        color: #8b949e;
+      }
+    }
+  }
+</style>

+ 31 - 36
src/views/ventAI/manageAssistent/components/chatModal/TaskListPanel.vue

@@ -2,13 +2,24 @@
   <div class="task-list-panel">
     <!-- 操作区域(顶部) -->
     <div class="options-area">
-      <div v-for="item in optionItems" :key="item.type" :class="['options-area-item', `options-${item.type}`]" @click="handleOptionClick(item.type)">
+      <div
+        v-for="item in optionItems"
+        :key="item.type"
+        v-show="item.type !== 'search'"
+        :class="['options-area-item', `options-${item.type}`]"
+        @click="handleOptionClick(item.type)"
+      >
         <div class="options-img">
           <div class="options-icon"></div>
         </div>
         <a-button size="small" type="primary">{{ item.label }}</a-button>
       </div>
     </div>
+
+    <!-- 搜索模式 -->
+    <div class="search-header">
+      <a-input v-model:value="searchKeyword" placeholder="搜索任务名称..." allow-clear size="small" class="search-input" />
+    </div>
     <!-- 置顶任务区域 -->
     <div v-if="pinnedTaskList.length > 0" class="pinned-task-list">
       <div
@@ -17,7 +28,7 @@
         :class="['task-item', { active: currentTaskId === task.id }]"
         @click="emit('switch-task', task.id)"
       >
-        <div class="task-pin-icon pinned" @click.stop="togglePin(task.id)" title="取消置顶"></div>
+        <div class="task-pin-icon pinned" @click.stop="togglePin(task)" title="取消置顶"></div>
         <span class="task-name">{{ task.name }}</span>
         <div class="task-right">
           <span class="task-time">{{ formatRelativeTime(task.updatedAt) }}</span>
@@ -25,10 +36,6 @@
         </div>
       </div>
     </div>
-    <!-- 搜索模式 -->
-    <div v-if="activeOption === 'search'" class="search-header">
-      <a-input v-model:value="searchKeyword" placeholder="搜索任务名称..." allow-clear size="small" class="search-input" />
-    </div>
 
     <div class="task-list">
       <div
@@ -37,22 +44,25 @@
         :class="['task-item', { active: currentTaskId === task.id }]"
         @click="emit('switch-task', task.id)"
       >
-        <div class="task-pin-icon" @click.stop="togglePin(task.id)" title="置顶"></div>
+        <div class="task-pin-icon" @click.stop="togglePin(task)" title="置顶"></div>
         <span class="task-name">{{ task.name }}</span>
         <div class="task-right">
           <span class="task-time">{{ formatRelativeTime(task.updatedAt) }}</span>
           <div class="task-delete" @click.stop="emit('delete-task', task, index)" title="删除"></div>
         </div>
       </div>
-      <div v-if="activeOption === 'search' && searchKeyword && filteredTaskList.length === 0" class="empty-hint"> 未找到匹配的任务 </div>
+      <div v-if="searchKeyword && filteredTaskList.length === 0" class="empty-hint"> 未找到匹配的任务 </div>
       <div v-else-if="filteredTaskList.length === 0" class="empty-hint"> 还没有任务 </div>
     </div>
+
+    <SkillModal :visible="skillVisible" @close="skillVisible = false" />
   </div>
 </template>
 
 <script setup lang="ts">
   import { ref, computed } from 'vue';
   import type { Task } from './types';
+  import SkillModal from './SkillModal.vue';
 
   const props = defineProps<{
     taskList: Task[];
@@ -63,49 +73,35 @@
     (e: 'switch-task', taskId: string): void;
     (e: 'create-task'): void;
     (e: 'delete-task', task: Task, index: number): void;
+    (e: 'pin-task', task: Task): void;
   }>();
 
   type OptionType = 'add' | 'search' | 'skill';
 
   const activeOption = ref<OptionType>('add');
   const searchKeyword = ref('');
-  const PINNED_TASK_KEY = 'chat_pinned_tasks';
-  const pinnedTaskIds = ref<string[]>(loadPinnedTasks());
-
-  function loadPinnedTasks(): string[] {
-    try {
-      const cached = localStorage.getItem(PINNED_TASK_KEY);
-      return cached ? JSON.parse(cached) : [];
-    } catch {
-      return [];
-    }
-  }
-
-  function savePinnedTasks(ids: string[]) {
-    localStorage.setItem(PINNED_TASK_KEY, JSON.stringify(ids));
-  }
+  const skillVisible = ref(false);
 
   const pinnedTaskList = computed(() => {
-    return props.taskList.filter((task) => pinnedTaskIds.value.includes(task.id));
+    let list = props.taskList.filter((task) => task.sortOrder === 1);
+    if (searchKeyword.value.trim()) {
+      const keyword = searchKeyword.value.trim().toLowerCase();
+      list = list.filter((task) => task.name.toLowerCase().includes(keyword));
+    }
+    return list;
   });
 
   const filteredTaskList = computed(() => {
-    let list = props.taskList.filter((task) => !pinnedTaskIds.value.includes(task.id));
-    if (activeOption.value === 'search' && searchKeyword.value.trim()) {
+    let list = props.taskList.filter((task) => task.sortOrder !== 1);
+    if (searchKeyword.value.trim()) {
       const keyword = searchKeyword.value.trim().toLowerCase();
       list = list.filter((task) => task.name.toLowerCase().includes(keyword));
     }
     return list;
   });
 
-  const togglePin = (taskId: string) => {
-    const index = pinnedTaskIds.value.indexOf(taskId);
-    if (index >= 0) {
-      pinnedTaskIds.value.splice(index, 1);
-    } else {
-      pinnedTaskIds.value.push(taskId);
-    }
-    savePinnedTasks(pinnedTaskIds.value);
+  const togglePin = (task: Task) => {
+    emit('pin-task', task);
   };
 
   const optionItems = [
@@ -114,7 +110,6 @@
     { type: 'skill' as OptionType, label: '技能' },
   ];
 
-
   const formatRelativeTime = (dateStr?: string) => {
     if (!dateStr) return '刚刚';
     const now = Date.now();
@@ -130,7 +125,7 @@
     if (type === 'add') {
       emit('create-task');
     } else if (type === 'skill') {
-      // TODO: 技能点击事件
+      skillVisible.value = true;
     } else {
       activeOption.value = type;
       searchKeyword.value = '';

+ 14 - 0
src/views/ventAI/manageAssistent/components/chatModal/types.ts

@@ -30,19 +30,32 @@ export interface ThinkingStep {
   timestamp: number;
 }
 
+export interface PendingApprovalData {
+  message: string;
+  sessionId: string;
+  threadId?: string;
+  interrupts?: any[];
+  elapsedMs?: number;
+  prevThinkingContent?: string;
+}
+
 export interface Message {
   type: 'user' | 'ai';
   content: string;
+  thinkingContent?: string;
   time: string;
   createdAt?: string;
   isTerminal?: boolean;
   isLoading?: boolean;
+  isPendingApproval?: boolean;
+  pendingApprovalData?: PendingApprovalData;
   attachedFile?: AttachedFile;
   agentAnswers?: AgentAnswer[];
   thinkingSteps?: ThinkingStep[];
   sessionId?: string;
   wordDownloadUrl?: string;
   durationMs?: number;
+  baseDurationMs?: number;
   generateStartTime?: number;
 }
 
@@ -54,6 +67,7 @@ export interface Task {
   attachedFiles: AttachedFile[];
   wordUrl?: string;
   updatedAt?: string;
+  sortOrder?: number;
 }
 
 export type SectionType = 'select' | 'params' | 'result';