Ver Fonte

[Feat 0000]添加定时任务功能

wangkeyi há 4 semanas atrás
pai
commit
eb15d864d1

+ 1 - 0
src/assets/images/ventAI/chatModalBlue/4-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="1787021396395" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8053" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M860.234512 407.749329L300.00663 16.804918A133.214004 133.214004 0 0 0 102.328047 124.949364v774.853998a129.632639 129.632639 0 0 0 191.539099 108.208399l560.227882-390.944411a117.801342 117.801342 0 0 0 6.139484-209.318021z" fill="#ffffff" p-id="8054"></path></svg>

+ 1 - 0
src/assets/images/ventAI/chatModalBlue/4-11.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="1787033308537" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9083" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M826.61 365.066c0 0 0 0 0 0-58.569 0-106.04800001 47.479-106.048 106.048 0 58.569 47.479 106.04800001 106.04799999 106.048 0 0 0 0 0 0 58.569 0 106.04800001-47.479 106.04800001-106.04800001 0-58.569-47.479-106.04800001-106.048-106.04799999 0 0 0 0 0 0z" fill="#ffffff" p-id="9084"></path><path d="M512 365.066c0 0 0 0 0 0-58.569 0-106.04800001 47.479-106.04800001 106.048 0 58.569 47.479 106.04800001 106.04800001 106.04800001 0 0 0 0 0 0 58.569 0 106.04800001-47.479 106.04800001-106.04800001 0-58.569-47.479-106.04800001-106.04800001-106.048 0 0 0 0 0 0z" fill="#ffffff" p-id="9085"></path><path d="M197.39 365.066c0 0 0 0 0 0-58.569 0-106.04800001 47.479-106.048 106.04799999 0 58.569 47.479 106.04800001 106.04800001 106.04800001 0 0 0 0 0 0 58.569 0 106.04800001-47.479 106.04799999-106.048 0-58.569-47.479-106.04800001-106.048-106.048 0 0 0 0 0 0z" fill="#ffffff" p-id="9086"></path></svg>

+ 1 - 0
src/assets/images/ventAI/chatModalBlue/4-8.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="1787015280607" class="icon" viewBox="0 0 1039 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="17987" xmlns:xlink="http://www.w3.org/1999/xlink" width="202.9296875" height="200"><path d="M520.174998 16.511359a503.064985 503.064985 0 0 0-503.725175 501.744604 509.666888 509.666888 0 0 0 120.154629 330.095134L56.061239 926.253549a41.591987 41.591987 0 0 0 58.096744 58.756934l81.203403-80.543213a501.084414 501.084414 0 0 0 649.627225 0L925.531823 985.010483a41.591987 41.591987 0 0 0 58.096744-58.756934l-80.543213-80.543213a495.802892 495.802892 0 0 0 120.154629-325.473802A504.385365 504.385365 0 0 0 520.174998 16.511359z m419.881011 503.725175a419.881011 419.881011 0 1 1-419.881011-419.881011 420.541201 420.541201 0 0 1 419.881011 419.881011z" fill="#ffffff" p-id="17988"></path><path d="M529.417662 516.935582v-264.076107a45.553129 45.553129 0 0 0-90.446067 0v309.629236l186.173656 141.940908a44.232748 44.232748 0 0 0 62.718076-8.582474 45.553129 45.553129 0 0 0-8.582474-66.019027zM78.507708 179.578355l99.028541-99.688731a44.892938 44.892938 0 0 0 0-66.019026 46.213319 46.213319 0 0 0-66.019027 0L14.469252 115.539899a44.892938 44.892938 0 0 0 0 66.019027 44.892938 44.892938 0 0 0 31.689133 12.543615 46.213319 46.213319 0 0 0 32.349323-14.524186zM1024.560364 114.219518L925.531823 14.530788a46.873509 46.873509 0 0 0-66.019026 0 44.892938 44.892938 0 0 0-13.203806 32.349323 44.232748 44.232748 0 0 0 13.203806 31.689133l99.02854 99.02854a44.232748 44.232748 0 0 0 32.349323 13.203806 42.912367 42.912367 0 0 0 31.689133-13.203806 44.232748 44.232748 0 0 0 1.980571-63.378266z" fill="#ffffff" p-id="17989"></path></svg>

+ 1 - 0
src/assets/images/ventAI/chatModalBlue/4-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="1787015618299" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="21653" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M 441.585 411.417 a 29.256 29.256 0 0 0 -29.256 -30.1703 H 269.706 a 30.1703 30.1703 0 0 0 0 60.3406 h 142.623 a 29.256 29.256 0 0 0 29.256 -30.1703 Z M 241.364 612.552 a 29.256 29.256 0 0 0 28.3418 27.4275 H 314.504 a 365.701 365.701 0 0 1 19.1993 -60.3406 h -63.9976 a 29.256 29.256 0 0 0 -28.3418 30.1703 Z m 421.47 -211.192 A 316.331 316.331 0 0 0 341.017 713.12 a 317.245 317.245 0 0 0 320.902 310.845 a 317.245 317.245 0 0 0 321.816 -310.845 a 316.331 316.331 0 0 0 -320.902 -311.76 Z m 0 562.265 A 256.905 256.905 0 0 1 405.929 712.206 A 256.905 256.905 0 0 1 662.834 457.129 a 256.905 256.905 0 0 1 260.562 255.076 a 256.905 256.905 0 0 1 -261.476 251.419 Z m -40.2271 -752.429 A 29.256 29.256 0 0 0 593.351 182.854 H 269.706 a 30.1703 30.1703 0 0 0 0 60.3406 h 323.645 a 29.256 29.256 0 0 0 29.256 -30.1703 Z m 0 0" fill="#ffffff" p-id="21654"></path><path d="M 771.63 682.949 h -85.9396 V 574.154 c 0 -28.3418 0 -52.1123 -31.9988 -52.1123 s -31.9988 22.8563 -31.9988 52.1123 V 713.12 c 0 28.3418 0 30.1703 31.9988 30.1703 H 771.63 c 29.256 0 53.9408 0 53.9408 -30.1703 S 799.057 682.949 771.63 682.949 Z m 0 0" fill="#ffffff" p-id="21655"></path><path d="M 728.66 0.003657 H 155.424 A 114.281 114.281 0 0 0 40.2286 112.457 v 871.282 l 171.879 -125.252 l 80.4541 59.4263 h 43.8841 l -10.971 -46.6268 l -114.281 -91.4251 l -110.624 88.6824 V 112.457 a 52.1123 52.1123 0 0 1 54.8551 -53.0266 h 573.236 a 52.1123 52.1123 0 0 1 54.8551 53.0266 v 269.704 a 359.301 359.301 0 0 1 59.4263 27.4275 V 112.457 A 114.281 114.281 0 0 0 728.66 0.003657 Z m 0 0" fill="#ffffff" p-id="21656"></path></svg>

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

@@ -83,6 +83,10 @@ html {
   --img-chat-options-add-icon: url('/src/assets/images/ventAI/chatModalBlue/1-7.svg');
   --img-chat-options-search-icon: url('/src/assets/images/ventAI/chatModalBlue/1-8.svg');
   --img-chat-options-skill-icon: url('/src/assets/images/ventAI/chatModalBlue/1-9.svg');
+  --img-chat-options-schedule-icon: url('/src/assets/images/ventAI/chatModalBlue/4-8.svg');
+  --img-chat-schedule-empty-icon: url('/src/assets/images/ventAI/chatModalBlue/4-9.svg');
+  --img-chat-schedule-run-icon: url('/src/assets/images/ventAI/chatModalBlue/4-10.svg');
+  --img-chat-schedule-more-icon: url('/src/assets/images/ventAI/chatModalBlue/4-11.svg');
   --img-chat-expand-icon: url('/src/assets/images/ventAI/chatModalBlue/1-10.svg');
   --img-chat-collapse-icon: url('/src/assets/images/ventAI/chatModalBlue/1-11.svg');
   --img-chat-prev-icon: url('/src/assets/images/ventAI/chatModalBlue/1-14.png');

+ 35 - 0
src/views/ventAI/manageAssistent/api.ts

@@ -10,6 +10,7 @@ enum Api {
   getModelMsg = '/ventAI/api/model',
   switchThinking = '/ventAI/api/model/thinking',
   getSkills = '/ventAI/api/skills',
+  schedules = '/ventAI/api/schedules',
 }
 
 /**
@@ -358,3 +359,37 @@ export const chatResumeStream = async (
     throw error;
   }
 };
+
+/**
+ * 定时任务接口
+ */
+export const createSchedule = (data: {
+  name: string;
+  schedule?: { freq: string; hour?: number; minute?: number; day_of_week?: number; day_of_month?: number };
+  cron_expr?: string;
+  prompt: string;
+  notify_webhook?: string;
+}) => defHttp.post({ url: Api.schedules, data }, { isTransformResponse: false });
+
+export const getScheduleList = (include_disabled = true) =>
+  defHttp.get({ url: Api.schedules, params: { include_disabled } }, { joinParamsToUrl: true, isTransformResponse: false });
+
+export const getScheduleDetail = (task_id: number) =>
+  defHttp.get({ url: `${Api.schedules}/${task_id}` }, { isTransformResponse: false });
+
+export const updateSchedule = (
+  task_id: number,
+  data: { name?: string; schedule?: Record<string, any>; cron_expr?: string; prompt?: string; notify_webhook?: string }
+) => defHttp.put({ url: `${Api.schedules}/${task_id}`, data }, { isTransformResponse: false });
+
+export const pauseSchedule = (task_id: number) => defHttp.post({ url: `${Api.schedules}/${task_id}/pause` }, { isTransformResponse: false });
+
+export const resumeSchedule = (task_id: number) => defHttp.post({ url: `${Api.schedules}/${task_id}/resume` }, { isTransformResponse: false });
+
+export const deleteSchedule = (id: number) => defHttp.delete({ url: `${Api.schedules}/${id}` }, { isTransformResponse: false });
+
+export const getScheduleRuns = (task_id: number, limit = 50) =>
+  defHttp.get({ url: `${Api.schedules}/${task_id}/runs`, params: { limit } }, { joinParamsToUrl: true, isTransformResponse: false });
+
+export const runScheduleNow = (task_id: number) =>
+  defHttp.post({ url: `${Api.schedules}/${task_id}/run` }, { isTransformResponse: false });

+ 215 - 86
src/views/ventAI/manageAssistent/components/AiAssistantModal.vue

@@ -28,92 +28,122 @@
           @create-task="createNewTask"
           @delete-task="handleDeleteTask"
           @pin-task="handlePinTask"
+          @toggle-skill="toggleSkillPanel"
+          @toggle-schedules="toggleSchedulePanel"
         />
       </div>
 
-      <!-- 中间聊天区域 -->
-      <div class="chat-panel">
-        <!-- 顶部任务选择器 -->
-        <div class="chat-header">
-          <template v-if="panelCollapsed">
-            <div :class="['nav-btn', { disabled: !canGoPrev }]" @click="goToPrev">
-              <div class="nav-icon prev-icon"></div>
-            </div>
-            <div :class="['nav-btn', { disabled: !canGoNext }]" @click="goToNext">
-              <div class="nav-icon next-icon"></div>
+      <!-- 右侧内容区:根据 activePanel 切换显示 -->
+      <!-- 聊天区域 -->
+      <template v-if="activePanel === 'chat'">
+        <div class="chat-panel">
+          <!-- 顶部任务选择器 -->
+          <div class="chat-header">
+            <template v-if="panelCollapsed">
+              <div :class="['nav-btn', { disabled: !canGoPrev }]" @click="goToPrev">
+                <div class="nav-icon prev-icon"></div>
+              </div>
+              <div :class="['nav-btn', { disabled: !canGoNext }]" @click="goToNext">
+                <div class="nav-icon next-icon"></div>
+              </div>
+              <div class="nav-btn" @click="createNewTask">
+                <div class="nav-icon add-icon"></div>
+              </div>
+            </template>
+            <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 class="nav-btn" @click="createNewTask">
-              <div class="nav-icon add-icon"></div>
+            <div v-if="currentWordUrl" class="upload-bg" @click="downloadWordReport">
+              <div class="download-icon"></div>
             </div>
-          </template>
-          <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>
+
+          <!-- 附件列表面板 -->
+          <FileListPanel :visible="showFileList" :files="getCurrentTaskFiles()" @close="showFileList = false" @file-click="openFilePreview" />
+
+          <!-- 消息列表 -->
+          <ChatMessages
+            ref="chatMessagesRef"
+            :messages="messages"
+            @file-preview="openFilePreview"
+            @download-word="downloadWordFile"
+            @approve="handleApprove"
+            @reject="handleReject"
+          />
+
+          <!-- 下载确认条 -->
+          <DownloadConfirmBar
+            v-if="downloadConfirm && downloadConfirmTaskId === currentTaskId"
+            :filename="downloadConfirm.filename"
+            @confirm="doDownload"
+            @cancel="clearDownloadConfirm"
+          />
+
+          <!-- 任务进度条 -->
+          <TodoListBar v-if="showTodoBar" :todos="currentTodos" />
+
+          <!-- 输入区域 -->
+          <ChatInputArea
+            ref="chatInputRef"
+            v-model="inputMessage"
+            :pendingFile="pendingFile"
+            :loading="loading || !!pendingApprovalSessionId"
+            :streaming="streaming"
+            :contextUsed="contextUsed"
+            :contextMax="contextMax"
+            :contextPercent="contextPercent"
+            :contextBreakdown="contextBreakdown"
+            :initialThinkLevel="thinkLevel"
+            :initialEditMode="editMode"
+            :taskList="taskList"
+            :currentTaskId="currentTaskId"
+            @send="handleSendMessage"
+            @stop="stopStreaming"
+            @file-upload="triggerFileUpload"
+            @file-change="handleFileUpload"
+            @remove-pending-file="removePendingFile"
+            @think-level-change="handleThinkLevelChange"
+            @edit-mode-change="handleEditModeChange"
+            @update:selected-sessions="handleUpdateSelectedSessions"
+            @update:selected-skill="handleUpdateSelectedSkill"
+          />
         </div>
 
-        <!-- 附件列表面板 -->
-        <FileListPanel :visible="showFileList" :files="getCurrentTaskFiles()" @close="showFileList = false" @file-click="openFilePreview" />
-
-        <!-- 消息列表 -->
-        <ChatMessages
-          ref="chatMessagesRef"
-          :messages="messages"
-          @file-preview="openFilePreview"
-          @download-word="downloadWordFile"
-          @approve="handleApprove"
-          @reject="handleReject"
-        />
+        <!-- 文件预览面板(与 chat-panel 并列) -->
+        <div v-if="previewFile" class="right-panel">
+          <FilePreviewPanel :file="previewFile" @close="removePreviewFile" />
+        </div>
 
-        <!-- 下载确认条 -->
-        <DownloadConfirmBar
-          v-if="downloadConfirm && downloadConfirmTaskId === currentTaskId"
-          :filename="downloadConfirm.filename"
-          @confirm="doDownload"
-          @cancel="clearDownloadConfirm"
-        />
+        <!-- 定时任务详情面板(与 chat-panel 并列) -->
+        <div v-if="currentScheduleTask" class="right-panel narrow">
+          <ScheduleDetailPanel
+            :task="currentScheduleTask"
+            @close="showScheduleDetail = false"
+            @deleted="handleScheduleDeleted"
+            @updated="fetchScheduleListForDetail"
+          />
+        </div>
+      </template>
 
-        <!-- 任务进度条 -->
-        <TodoListBar v-if="showTodoBar" :todos="currentTodos" />
-
-        <!-- 输入区域 -->
-        <ChatInputArea
-          ref="chatInputRef"
-          v-model="inputMessage"
-          :pendingFile="pendingFile"
-          :loading="loading || !!pendingApprovalSessionId"
-          :streaming="streaming"
-          :contextUsed="contextUsed"
-          :contextMax="contextMax"
-          :contextPercent="contextPercent"
-          :contextBreakdown="contextBreakdown"
-          :initialThinkLevel="thinkLevel"
-          :initialEditMode="editMode"
-          :taskList="taskList"
-          :currentTaskId="currentTaskId"
-          @send="handleSendMessage"
-          @stop="stopStreaming"
-          @file-upload="triggerFileUpload"
-          @file-change="handleFileUpload"
-          @remove-pending-file="removePendingFile"
-          @think-level-change="handleThinkLevelChange"
-          @edit-mode-change="handleEditModeChange"
-          @update:selected-sessions="handleUpdateSelectedSessions"
-          @update:selected-skill="handleUpdateSelectedSkill"
-        />
+      <!-- 技能面板 -->
+      <div v-if="activePanel === 'skill'" class="right-panel full">
+        <SkillPanel @close="activePanel = 'chat'" />
       </div>
 
-      <!-- 右侧文件预览面板 -->
-      <div v-if="previewFile" class="right-panel">
-        <FilePreviewPanel :file="previewFile" @close="removePreviewFile" />
+      <!-- 定时任务面板 -->
+      <div v-if="activePanel === 'schedules'" class="right-panel full">
+        <SchedulePanel
+          ref="schedulePanelRef"
+          @close="activePanel = 'chat'"
+          @open-session="handleOpenScheduleSession"
+          @data-changed="fetchScheduleListForDetail"
+        />
       </div>
     </div>
   </a-modal>
@@ -145,6 +175,10 @@
   import FilePreviewPanel from './chatModal/FilePreviewPanel.vue';
   import TodoListBar from './chatModal/TodoListBar.vue';
   import DownloadConfirmBar from './chatModal/DownloadConfirmBar.vue';
+  import SkillPanel from './chatModal/SkillPanel.vue';
+  import SchedulePanel from './chatModal/SchedulePanel.vue';
+  import ScheduleDetailPanel from './chatModal/ScheduleDetailPanel.vue';
+  import { getScheduleList } from '../api';
   import type { AttachedFile, Message, Task } from './chatModal/types';
   import { isTextFile } from './chatModal/utils';
 
@@ -207,6 +241,25 @@
 
   const previewFile = ref<AttachedFile | null>(null);
   const showFileList = ref(false);
+  const activePanel = ref<'chat' | 'skill' | 'schedules'>('chat');
+  const scheduleListForDetail = ref<any[]>([]);
+  const showScheduleDetail = ref(false);
+
+  const currentScheduleTask = computed(() => {
+    if (!showScheduleDetail.value || activePanel.value !== 'chat') return null;
+    const sessionId = currentTask.value?.sessionId;
+    if (!sessionId) return null;
+    return scheduleListForDetail.value.find((t: any) => t.feedback_session_id === sessionId) || null;
+  });
+
+  const fetchScheduleListForDetail = async () => {
+    try {
+      const res = await getScheduleList();
+      scheduleListForDetail.value = res?.tasks || res?.data || (Array.isArray(res) ? res : []);
+    } catch {
+      scheduleListForDetail.value = [];
+    }
+  };
   const pendingFile = ref<AttachedFile | null>(null);
 
   const messages = ref<Message[]>([]);
@@ -265,6 +318,55 @@
     panelCollapsed.value = !panelCollapsed.value;
   };
 
+  const toggleSkillPanel = () => {
+    if (activePanel.value === 'skill') {
+      activePanel.value = 'chat';
+    } else {
+      activePanel.value = 'skill';
+      previewFile.value = null;
+    }
+  };
+
+  const toggleSchedulePanel = () => {
+    if (activePanel.value === 'schedules') {
+      activePanel.value = 'chat';
+    } else {
+      activePanel.value = 'schedules';
+      previewFile.value = null;
+    }
+  };
+
+  const handleOpenScheduleSession = async (sessionId: string) => {
+    activePanel.value = 'chat';
+    previewFile.value = null;
+    showScheduleDetail.value = true;
+    await fetchSessionList();
+    await fetchScheduleListForDetail();
+    const existing = taskList.value.find((t) => t.sessionId === sessionId);
+    if (existing) {
+      await switchTask(existing.id);
+    } else {
+      const newTask: Task = {
+        id: sessionId,
+        name: '定时任务',
+        sessionId,
+        messages: [],
+        attachedFiles: [],
+      };
+      taskList.value.unshift(newTask);
+      historyLoadedTasks.value.delete(sessionId);
+      await switchTask(newTask.id);
+    }
+  };
+
+  const schedulePanelRef = ref();
+
+  const handleScheduleDeleted = () => {
+    showScheduleDetail.value = false;
+    fetchScheduleListForDetail();
+    schedulePanelRef.value?.fetchScheduleList?.();
+  };
+
   const editingTitle = ref(false);
   const editTitleValue = ref('');
 
@@ -366,6 +468,7 @@
   };
 
   const switchTask = async (taskId: string) => {
+    activePanel.value = 'chat';
     const currentTask = taskList.value.find((t) => t.id === currentTaskId.value);
     if (currentTask) {
       currentTask.messages = [...messages.value];
@@ -409,6 +512,13 @@
     messages.value = newTask.messages && newTask.messages.length > 0 ? [...newTask.messages] : getDefaultMessages();
     syncTodoBarFromMessages();
 
+    // 切换任务时检测是否为定时任务会话
+    if (newTask.sessionId && scheduleListForDetail.value.some((s: any) => s.feedback_session_id === newTask.sessionId)) {
+      showScheduleDetail.value = true;
+    } else {
+      showScheduleDetail.value = false;
+    }
+
     await nextTick();
     chatMessagesRef.value?.forceScrollToBottom();
   };
@@ -938,20 +1048,16 @@
   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;
-      }
+      contextUsed.value = res?.current_usage ?? 0;
+      contextMax.value = res?.total_limit ?? 0;
+      contextPercent.value = res?.usage_pct ?? 0;
+      contextBreakdown.value = res?.breakdown ?? { messages: 0, mcp: 0, skills: 0, system_prompt: 0, other: 0 };
     } catch (e) {
       console.error('获取上下文容量失败:', e);
+      contextUsed.value = 0;
+      contextMax.value = 0;
+      contextPercent.value = 0;
+      contextBreakdown.value = { messages: 0, mcp: 0, skills: 0, system_prompt: 0, other: 0 };
     }
   };
 
@@ -1131,6 +1237,7 @@
           ? (aiMsg.baseDurationMs || 0) + data.duration_ms
           : (aiMsg.baseDurationMs || 0) + (aiMsg.generateStartTime ? Date.now() - aiMsg.generateStartTime : 0);
         aiMsg.isLoading = false;
+        streamingTask.isStreaming = false;
         break;
 
       case 'system':
@@ -1421,6 +1528,7 @@
       } else {
         await fetchSessionList();
         fetchModelMsg();
+        fetchScheduleListForDetail();
 
         // 去除重复的空白任务,只保留一个
         taskList.value = taskList.value.filter((task, index) => {
@@ -1798,6 +1906,17 @@
     background-repeat: no-repeat;
     background-size: 100% 100%;
     border-radius: 4px;
+
+    &.full {
+      flex: 1;
+      width: auto;
+      min-width: 0;
+    }
+
+    &.narrow {
+      width: 320px;
+      flex-shrink: 0;
+    }
     flex-shrink: 0;
   }
 
@@ -1996,6 +2115,16 @@
       .chat-input-area {
         margin: 24px;
       }
+      .right-panel {
+        background-image: var(--img-chat-fs-panel-bg) !important;
+      }
+      .schedule-panel,
+      .skill-panel {
+        background-image: var(--img-chat-fs-panel-bg) !important;
+      }
+      .schedule-detail-panel {
+        background-image: var(--img-chat-fs-task-panel-bg) !important;
+      }
     }
 
     .zxm-modal-header {

+ 0 - 2
src/views/ventAI/manageAssistent/components/chatModal/ChatInputArea.vue

@@ -1063,7 +1063,6 @@
           gap: 4px;
           cursor: pointer;
           transition: all 0.2s;
-          border: 1px solid rgba(255, 255, 255, 0.12);
 
           &:hover {
             border-color: #3a8fd4;
@@ -1390,7 +1389,6 @@
           gap: 4px;
           cursor: pointer;
           transition: all 0.2s;
-          border: 1px solid rgba(255, 255, 255, 0.12);
 
           &:hover {
             border-color: #3a8fd4;

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

@@ -482,7 +482,7 @@
         }
 
         .message-body {
-          padding: 12px;
+          padding: 12px 12px 4px 12px;
           border-radius: 6px;
           width: calc(100% - 50px);
           min-width: 0;
@@ -1105,7 +1105,7 @@
       }
 
       .ai-message .message-actions {
-        padding: 4px 0 0 10px;
+        padding: 0 0 0 10px;
       }
 
       .user-message .message-actions {

+ 459 - 0
src/views/ventAI/manageAssistent/components/chatModal/CreateScheduleModal.vue

@@ -0,0 +1,459 @@
+<template>
+  <a-modal
+    :visible="visible"
+    :title="isEdit ? '编辑定时任务' : '创建定时任务'"
+    :width="600"
+    :footer="null"
+    destroyOnClose
+    @cancel="emit('close')"
+    class="create-schedule-modal"
+  >
+    <div class="schedule-form">
+      <div class="form-item">
+        <label class="form-label">任务名称 <span class="required">*</span></label>
+        <a-input v-model:value="form.name" placeholder="请输入任务名称(1-100字)" :maxlength="100" />
+      </div>
+
+      <div class="form-item">
+        <label class="form-label">调度方式</label>
+        <div class="mode-switch">
+          <div :class="['mode-btn', { active: mode === 'friendly' }]" @click="switchMode('friendly')">友好模式</div>
+          <div :class="['mode-btn', { active: mode === 'cron' }]" @click="switchMode('cron')">专业模式</div>
+        </div>
+      </div>
+
+      <!-- 友好模式 -->
+      <template v-if="mode === 'friendly'">
+        <div class="form-item">
+          <label class="form-label">执行频率 <span class="required">*</span></label>
+          <a-select v-model:value="form.schedule.freq" class="form-select" @change="onFreqChange">
+            <a-select-option value="hourly">每小时</a-select-option>
+            <a-select-option value="daily">每天</a-select-option>
+            <a-select-option value="weekly">每周</a-select-option>
+            <a-select-option value="monthly">每月</a-select-option>
+          </a-select>
+        </div>
+
+        <div class="form-item" v-if="form.schedule.freq === 'weekly'">
+          <label class="form-label">星期 <span class="required">*</span></label>
+          <a-select v-model:value="form.schedule.day_of_week" class="form-select" placeholder="请选择">
+            <a-select-option :value="1">周一</a-select-option>
+            <a-select-option :value="2">周二</a-select-option>
+            <a-select-option :value="3">周三</a-select-option>
+            <a-select-option :value="4">周四</a-select-option>
+            <a-select-option :value="5">周五</a-select-option>
+            <a-select-option :value="6">周六</a-select-option>
+            <a-select-option :value="7">周日</a-select-option>
+          </a-select>
+        </div>
+
+        <div class="form-item" v-if="form.schedule.freq === 'monthly'">
+          <label class="form-label">日期 <span class="required">*</span></label>
+          <a-input-number v-model:value="form.schedule.day_of_month" :min="1" :max="31" placeholder="1-31" class="form-input-number" />
+        </div>
+
+        <div class="form-row" v-if="form.schedule.freq !== 'hourly'">
+          <div class="form-item form-col">
+            <label class="form-label">小时 <span class="required">*</span></label>
+            <a-input-number v-model:value="form.schedule.hour" :min="0" :max="23" placeholder="0-23" class="form-input-number" />
+          </div>
+          <div class="form-item form-col">
+            <label class="form-label">分钟 <span class="required">*</span></label>
+            <a-input-number v-model:value="form.schedule.minute" :min="0" :max="59" placeholder="0-59" class="form-input-number" />
+          </div>
+        </div>
+
+        <div class="form-item" v-else>
+          <label class="form-label">分钟 <span class="required">*</span></label>
+          <a-input-number v-model:value="form.schedule.minute" :min="0" :max="59" placeholder="0-59" class="form-input-number" />
+        </div>
+
+        <div v-if="humanDesc" class="schedule-preview">
+          <span class="preview-label">执行计划:</span>
+          <span class="preview-value">{{ humanDesc }}</span>
+        </div>
+      </template>
+
+      <!-- 专业模式 -->
+      <template v-if="mode === 'cron'">
+        <div class="form-item">
+          <label class="form-label">Cron 表达式 <span class="required">*</span></label>
+          <a-input v-model:value="form.cron_expr" placeholder="如:30 20 * * * (每天20:30)" class="cron-input" />
+          <div class="form-hint">标准5段cron:分 时 日 月 周(1-7,1=周一)</div>
+        </div>
+      </template>
+
+      <div class="form-item">
+        <label class="form-label">任务指令 <span class="required">*</span></label>
+        <a-textarea v-model:value="form.prompt" placeholder="到点后智能体执行的具体指令,如:总结今天的工作" :rows="4" :maxlength="2000" />
+      </div>
+
+      <div class="form-item">
+        <label class="form-label">Webhook 通知 <span class="form-optional">(选填)</span></label>
+        <a-input v-model:value="form.notify_webhook" placeholder="执行完成后推送通知的 URL" />
+      </div>
+
+      <div class="form-actions">
+        <a-button @click="emit('close')">取消</a-button>
+        <a-button type="primary" :loading="submitting" :disabled="!canSubmit" @click="handleSubmit">{{ isEdit ? '保存' : '创建' }}</a-button>
+      </div>
+    </div>
+  </a-modal>
+</template>
+
+<script setup lang="ts">
+  import { ref, reactive, computed, watch } from 'vue';
+  import { createSchedule, updateSchedule } from '../../api';
+  import { message } from 'ant-design-vue';
+
+  interface ScheduleData {
+    id: number;
+    name: string;
+    cron_expr: string;
+    prompt: string;
+    notify_webhook?: string;
+    schedule?: { freq: string; hour?: number; minute?: number; day_of_week?: number; day_of_month?: number; human?: string };
+  }
+
+  const props = defineProps<{ visible: boolean; editData?: ScheduleData | null }>();
+  const emit = defineEmits<{
+    (e: 'close'): void;
+    (e: 'created'): void;
+    (e: 'updated'): void;
+  }>();
+
+  const mode = ref<'friendly' | 'cron'>('friendly');
+  const submitting = ref(false);
+
+  const defaultForm = () => ({
+    name: '',
+    schedule: {
+      freq: 'daily' as string,
+      hour: 20 as number | undefined,
+      minute: 30 as number | undefined,
+      day_of_week: undefined as number | undefined,
+      day_of_month: undefined as number | undefined,
+    },
+    cron_expr: '',
+    prompt: '',
+    notify_webhook: '',
+  });
+
+  const form = reactive(defaultForm());
+
+  const isEdit = computed(() => !!props.editData);
+
+  watch(
+    () => props.visible,
+    (val) => {
+      if (val) {
+        Object.assign(form, defaultForm());
+        submitting.value = false;
+        if (props.editData) {
+          const d = props.editData;
+          form.name = d.name;
+          form.prompt = d.prompt;
+          form.notify_webhook = d.notify_webhook || '';
+          form.cron_expr = d.cron_expr || '';
+          if (d.schedule && d.schedule.freq && d.schedule.freq !== 'other') {
+            mode.value = 'friendly';
+            form.schedule.freq = d.schedule.freq;
+            form.schedule.hour = d.schedule.hour;
+            form.schedule.minute = d.schedule.minute;
+            form.schedule.day_of_week = d.schedule.day_of_week;
+            form.schedule.day_of_month = d.schedule.day_of_month;
+          } else {
+            mode.value = 'cron';
+          }
+        } else {
+          mode.value = 'friendly';
+        }
+      }
+    },
+  );
+
+  const onFreqChange = (freq: string) => {
+    form.schedule.day_of_week = undefined;
+    form.schedule.day_of_month = undefined;
+    if (freq === 'hourly') {
+      form.schedule.hour = undefined;
+      form.schedule.minute = 0;
+    } else if (freq === 'daily') {
+      form.schedule.hour = 20;
+      form.schedule.minute = 30;
+    } else if (freq === 'weekly') {
+      form.schedule.day_of_week = 1;
+      form.schedule.hour = 9;
+      form.schedule.minute = 0;
+    } else if (freq === 'monthly') {
+      form.schedule.day_of_month = 1;
+      form.schedule.hour = 8;
+      form.schedule.minute = 0;
+    }
+  };
+
+  const freqLabelMap: Record<string, string> = {
+    hourly: '每小时',
+    daily: '每天',
+    weekly: '每周',
+    monthly: '每月',
+  };
+  const weekLabelMap: Record<number, string> = {
+    1: '周一',
+    2: '周二',
+    3: '周三',
+    4: '周四',
+    5: '周五',
+    6: '周六',
+    7: '周日',
+  };
+
+  const humanDesc = computed(() => {
+    const f = form.schedule;
+    const freq = freqLabelMap[f.freq] || f.freq;
+    if (f.freq === 'hourly') return `${freq}第${f.minute}分钟`;
+    const time = `${String(f.hour ?? 0).padStart(2, '0')}:${String(f.minute ?? 0).padStart(2, '0')}`;
+    if (f.freq === 'daily') return `${freq} ${time}`;
+    if (f.freq === 'weekly') return `${freq}${weekLabelMap[f.day_of_week ?? 1] || ''} ${time}`;
+    if (f.freq === 'monthly') return `每月${f.day_of_month}日 ${time}`;
+    return '';
+  });
+
+  const scheduleToCron = (): string => {
+    const f = form.schedule;
+    const min = f.minute ?? 0;
+    const hr = f.hour ?? 0;
+    if (f.freq === 'hourly') return `${min} * * * *`;
+    if (f.freq === 'daily') return `${min} ${hr} * * *`;
+    if (f.freq === 'weekly') return `${min} ${hr} * * ${f.day_of_week ?? 1}`;
+    if (f.freq === 'monthly') return `${min} ${hr} ${f.day_of_month ?? 1} * *`;
+    return '';
+  };
+
+  const switchMode = (target: 'friendly' | 'cron') => {
+    if (target === mode.value) return;
+    if (target === 'cron' && mode.value === 'friendly') {
+      form.cron_expr = scheduleToCron();
+    }
+    mode.value = target;
+  };
+
+  const canSubmit = computed(() => {
+    if (!form.name.trim() || !form.prompt.trim()) return false;
+    if (mode.value === 'cron') return !!form.cron_expr.trim();
+    return true;
+  });
+
+  const buildPayload = () => {
+    const payload: Record<string, any> = {
+      name: form.name.trim(),
+      prompt: form.prompt.trim(),
+    };
+    if (form.notify_webhook.trim()) {
+      payload.notify_webhook = form.notify_webhook.trim();
+    } else if (isEdit.value) {
+      payload.notify_webhook = '';
+    }
+    if (mode.value === 'cron') {
+      payload.cron_expr = form.cron_expr.trim();
+    } else {
+      const s: Record<string, any> = { freq: form.schedule.freq };
+      if (form.schedule.hour != null) s.hour = form.schedule.hour;
+      if (form.schedule.minute != null) s.minute = form.schedule.minute;
+      if (form.schedule.day_of_week != null) s.day_of_week = form.schedule.day_of_week;
+      if (form.schedule.day_of_month != null) s.day_of_month = form.schedule.day_of_month;
+      payload.schedule = s;
+    }
+    return payload;
+  };
+
+  const handleSubmit = async () => {
+    if (!canSubmit.value) return;
+    submitting.value = true;
+    try {
+      const payload = buildPayload();
+      if (isEdit.value && props.editData) {
+        await updateSchedule(props.editData.id, payload);
+        message.success(`已更新定时任务「${form.name.trim()}」`);
+        emit('updated');
+      } else {
+        await createSchedule(payload as any);
+        message.success(`已创建定时任务「${form.name.trim()}」`);
+        emit('created');
+      }
+      emit('close');
+    } catch (err: any) {
+      const msg = err?.response?.data?.message || err?.message || (isEdit.value ? '更新失败,请重试' : '创建失败,请重试');
+      message.error(msg);
+    } finally {
+      submitting.value = false;
+    }
+  };
+</script>
+
+<style scoped lang="less">
+  .schedule-form {
+    display: flex;
+    flex-direction: column;
+    gap: 18px;
+  }
+
+  .form-item {
+    display: flex;
+    flex-direction: column;
+    gap: 6px;
+  }
+
+  .form-label {
+    font-size: 13px;
+    color: #e8edf3;
+    font-weight: 500;
+
+    .required {
+      color: #ff4d4f;
+    }
+
+    .form-optional {
+      color: #6a7a8a;
+      font-weight: 400;
+    }
+  }
+
+  .form-row {
+    display: flex;
+    gap: 16px;
+
+    .form-col {
+      flex: 1;
+    }
+  }
+
+  .form-select {
+    width: 100%;
+
+    :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;
+      color: #e0e6ed;
+    }
+  }
+
+  :deep(.zxm-input),
+  :deep(.zxm-input-number),
+  :deep(textarea.zxm-input) {
+    background: rgba(10, 132, 255, 0.08);
+    border: 1px solid rgba(63, 80, 106, 0.4);
+    border-radius: 6px;
+    color: #e0e6ed;
+
+    &:hover,
+    &:focus {
+      border-color: rgba(10, 132, 255, 0.5);
+    }
+  }
+
+  .form-input-number {
+    width: 100%;
+  }
+
+  .cron-input {
+    font-family: 'Courier New', monospace;
+  }
+
+  .form-hint {
+    font-size: 12px;
+    color: #6a7a8a;
+    margin-top: 2px;
+  }
+
+  .mode-switch {
+    display: flex;
+    gap: 0;
+    border-radius: 6px;
+    overflow: hidden;
+    border: 1px solid rgba(63, 80, 106, 0.4);
+    width: fit-content;
+
+    .mode-btn {
+      padding: 6px 20px;
+      font-size: 13px;
+      color: #8b949e;
+      cursor: pointer;
+      transition: all 0.2s;
+      background: rgba(10, 132, 255, 0.04);
+
+      &:hover {
+        color: #e0e6ed;
+      }
+
+      &.active {
+        background: rgba(10, 132, 255, 0.2);
+        color: #5cb8ff;
+      }
+    }
+  }
+
+  .schedule-preview {
+    padding: 10px 14px;
+    background: rgba(10, 132, 255, 0.06);
+    border: 1px solid rgba(10, 132, 255, 0.15);
+    border-radius: 6px;
+    font-size: 13px;
+
+    .preview-label {
+      color: #6a7a8a;
+    }
+
+    .preview-value {
+      color: #5cb8ff;
+      font-weight: 500;
+    }
+  }
+
+  .form-actions {
+    display: flex;
+    justify-content: flex-end;
+    gap: 10px;
+    padding-top: 8px;
+  }
+</style>
+
+<style lang="less">
+  .create-schedule-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;
+    }
+
+    .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;
+    }
+
+    .zxm-modal-body {
+      padding: 24px;
+    }
+
+    .zxm-modal-close {
+      .zxm-modal-close-x {
+        color: #8b949e;
+      }
+    }
+  }
+</style>

+ 5 - 4
src/views/ventAI/manageAssistent/components/chatModal/FilePreviewPanel.vue

@@ -70,6 +70,7 @@
     justify-content: space-between;
     align-items: center;
     flex-shrink: 0;
+    border-bottom: 1px solid rgba(63, 80, 106, 0.25);
 
     .panel-title {
       font-size: 15px;
@@ -87,9 +88,9 @@
       cursor: pointer;
       width: 36px;
       height: 36px;
-      background-image: var(--img-chat-close-bg);
-      background-repeat: no-repeat;
-      background-size: 100% 100%;
+      // background-image: var(--img-chat-close-bg);
+      // background-repeat: no-repeat;
+      // background-size: 100% 100%;
       display: flex;
       align-items: center;
       justify-content: center;
@@ -100,7 +101,7 @@
       .btn-close-icon {
         width: 20px;
         height: 20px;
-        background-image: var(--img-chat-minimize-icon);
+        background-image: var(--img-chat-close-icon-btn);
         background-repeat: no-repeat;
         background-size: 100% 100%;
       }

+ 602 - 0
src/views/ventAI/manageAssistent/components/chatModal/ScheduleDetailPanel.vue

@@ -0,0 +1,602 @@
+<template>
+  <div class="schedule-detail-panel">
+    <div class="schedule-detail-header">
+      <span class="schedule-detail-title">定时任务</span>
+      <div class="schedule-detail-close" @click="emit('close')">
+        <div class="close-icon"></div>
+      </div>
+    </div>
+
+    <div class="schedule-detail-body" v-if="task">
+      <!-- 任务卡片 -->
+      <div class="detail-task-card">
+        <div class="detail-task-name">
+          <div class="detail-task-icon"></div>
+          <span class="detail-task-name-text">{{ task.name }}</span>
+          <div class="detail-task-actions">
+            <a-switch :checked="task.enabled" size="small" class="schedule-switch" @change="handleToggle" />
+            <a-popover v-model:open="moreVisible" trigger="click" placement="bottomRight" overlayClassName="schedule-more-popover">
+              <div class="detail-more-btn"></div>
+              <template #content>
+                <div class="more-menu">
+                  <div class="more-menu-item" @click="handleAction('run')">
+                    <div class="more-menu-icon run-icon"></div>
+                    <span>立即执行</span>
+                  </div>
+                  <div class="more-menu-item" @click="handleAction('edit')">
+                    <div class="more-menu-icon edit-icon"></div>
+                    <span>编辑</span>
+                  </div>
+                  <div class="more-menu-item delete" @click="handleAction('delete')">
+                    <div class="more-menu-icon delete-icon"></div>
+                    <span>删除</span>
+                  </div>
+                </div>
+              </template>
+            </a-popover>
+          </div>
+        </div>
+        <div class="detail-task-prompt">{{ task.prompt }}</div>
+        <div class="detail-task-meta">
+          <div class="detail-task-cron">
+            <span class="info-label">执行计划:</span>
+            <span class="info-value">{{ task.cron_human || task.cron_expr }}</span>
+          </div>
+          <div v-if="task.next_run_at" class="detail-task-next">
+            <span class="info-label">下次执行:</span>
+            <span class="info-value">{{ task.next_run_at }}</span>
+          </div>
+        </div>
+      </div>
+
+      <!-- 执行历史 -->
+      <div class="detail-history-section">
+        <div class="detail-history-title">
+          <span>执行历史</span>
+          <span v-if="historyStats" class="history-stats">
+            <span class="stats-total">共{{ historyStats.total }}次</span>
+            <span class="stats-sep">·</span>
+            <span class="stats-success">{{ historyStats.success }}次成功</span>
+            <span class="stats-sep">·</span>
+            <span class="stats-failed">{{ historyStats.failed }}次失败</span>
+          </span>
+        </div>
+        <div class="detail-history-list" v-if="!historyLoading">
+          <template v-if="historyList.length">
+            <div v-for="run in historyList" :key="run.id" class="history-item">
+              <div class="history-item-header">
+                <a-tag :color="runStatusColor(run.status)" class="history-status">{{ runStatusLabel(run.status) }}</a-tag>
+                <span class="history-time">{{ run.started_at }}</span>
+                <span v-if="run.duration_ms" class="history-duration">{{ (run.duration_ms / 1000).toFixed(0) }}s</span>
+              </div>
+              <div v-if="run.output" class="history-output">{{ run.output }}</div>
+              <div v-if="run.error" class="history-error">{{ run.error }}</div>
+            </div>
+          </template>
+          <div v-else class="history-empty">暂无执行记录</div>
+        </div>
+        <div v-else class="detail-history-loading">
+          <a-spin size="small" />
+        </div>
+      </div>
+    </div>
+
+    <CreateScheduleModal :visible="showEditModal" :editData="editData" @close="showEditModal = false" @updated="refreshHistory" />
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { ref, reactive, watch } from 'vue';
+  import { message, Modal } from 'ant-design-vue';
+  import { getScheduleRuns, pauseSchedule, resumeSchedule, deleteSchedule, runScheduleNow } from '../../api';
+  import CreateScheduleModal from './CreateScheduleModal.vue';
+
+  interface ScheduleTask {
+    id: number;
+    name: string;
+    cron_expr: string;
+    cron_human: string;
+    prompt: string;
+    enabled: boolean;
+    next_run_at: string | null;
+    feedback_session_id?: string;
+    schedule?: { freq: string; human?: string };
+  }
+
+  const props = defineProps<{ task: ScheduleTask | null }>();
+  const emit = defineEmits<{
+    (e: 'close'): void;
+    (e: 'deleted'): void;
+    (e: 'updated'): void;
+  }>();
+
+  const moreVisible = ref(false);
+  const showEditModal = ref(false);
+  const editData = ref<ScheduleTask | null>(null);
+  const runningIds = reactive(new Set<number>());
+
+  watch(showEditModal, (val) => {
+    if (!val) editData.value = null;
+  });
+
+  const handleToggle = async () => {
+    if (!props.task) return;
+    try {
+      if (props.task.enabled) {
+        await pauseSchedule(props.task.id);
+        message.success(`已暂停任务「${props.task.name}」`);
+      } else {
+        await resumeSchedule(props.task.id);
+        message.success(`已恢复任务「${props.task.name}」`);
+      }
+      props.task.enabled = !props.task.enabled; // eslint-disable-line vue/no-mutating-props
+    } catch {
+      message.error('操作失败');
+    }
+  };
+
+  const handleRunNow = async () => {
+    if (!props.task) return;
+    runningIds.add(props.task.id);
+    try {
+      await runScheduleNow(props.task.id);
+      message.success(`任务「${props.task.name}」已开始执行`);
+    } catch {
+      message.error('执行失败');
+    } finally {
+      runningIds.delete(props.task.id);
+    }
+  };
+
+  const handleEdit = () => {
+    if (!props.task) return;
+    editData.value = props.task;
+    showEditModal.value = true;
+  };
+
+  const handleDelete = () => {
+    if (!props.task) return;
+    const taskName = props.task.name;
+    const taskId = props.task.id;
+    Modal.confirm({
+      title: '提示',
+      content: `是否删除定时任务「${taskName}」?`,
+      okText: '确认',
+      cancelText: '取消',
+      wrapClassName: 'delete-task-confirm-modal',
+      onOk: async () => {
+        try {
+          await deleteSchedule(taskId);
+          message.success('删除成功');
+          emit('deleted');
+        } catch {
+          message.error('删除失败');
+        }
+      },
+    });
+  };
+
+  const handleAction = (action: 'run' | 'edit' | 'delete') => {
+    moreVisible.value = false;
+    if (action === 'run') handleRunNow();
+    else if (action === 'edit') handleEdit();
+    else if (action === 'delete') handleDelete();
+  };
+
+  const refreshHistory = () => {
+    if (props.task) fetchHistory(props.task.id);
+    emit('updated');
+  };
+
+  const historyList = ref<any[]>([]);
+  const historyLoading = ref(false);
+  const historyStats = ref<{ total: number; success: number; failed: number } | null>(null);
+
+  const fetchHistory = async (taskId: number) => {
+    historyLoading.value = true;
+    try {
+      const res = await getScheduleRuns(taskId);
+      const runs = res?.runs || (Array.isArray(res) ? res : []);
+      historyList.value = runs;
+      historyStats.value = {
+        total: res?.total ?? runs.length,
+        success: runs.filter((r: any) => r.status === 'success').length,
+        failed: runs.filter((r: any) => r.status === 'failed').length,
+      };
+    } catch {
+      historyList.value = [];
+      historyStats.value = null;
+    } finally {
+      historyLoading.value = false;
+    }
+  };
+
+  const runStatusLabel = (status: string) => {
+    const map: Record<string, string> = { running: '执行中', success: '成功', failed: '失败', skipped: '跳过' };
+    return map[status] || status;
+  };
+
+  const runStatusColor = (status: string) => {
+    const map: Record<string, string> = { running: 'blue', success: 'green', failed: 'red', skipped: 'default' };
+    return map[status] || 'default';
+  };
+
+  watch(
+    () => props.task,
+    (newTask) => {
+      if (newTask) {
+        fetchHistory(newTask.id);
+      } else {
+        historyList.value = [];
+        historyStats.value = null;
+      }
+    },
+    { immediate: true }
+  );
+</script>
+
+<style scoped lang="less">
+  .schedule-detail-panel {
+    height: 100%;
+    display: flex;
+    flex-direction: column;
+    overflow: hidden;
+    background-image: var(--img-chat-panel-bg);
+    background-repeat: no-repeat;
+    background-size: 100% 100%;
+    border-radius: 4px;
+  }
+
+  .schedule-detail-header {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: 12px 16px;
+    border-bottom: 1px solid rgba(63, 80, 106, 0.25);
+    flex-shrink: 0;
+
+    .schedule-detail-title {
+      font-size: 16px;
+      font-weight: 600;
+      color: #e8edf3;
+      letter-spacing: 1px;
+    }
+
+    .schedule-detail-close {
+      width: 28px;
+      height: 28px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      cursor: pointer;
+      border-radius: 4px;
+      transition: background 0.2s;
+
+      &:hover {
+        background: rgba(255, 77, 79, 0.15);
+      }
+
+      .close-icon {
+        width: 16px;
+        height: 16px;
+        background-image: var(--img-chat-close-icon-btn);
+        background-repeat: no-repeat;
+        background-size: 100% 100%;
+      }
+    }
+  }
+
+  .schedule-detail-body {
+    flex: 1;
+    overflow: hidden;
+    padding: 16px;
+    display: flex;
+    flex-direction: column;
+    gap: 16px;
+  }
+
+  /* 任务卡片 */
+  .detail-task-card {
+    padding: 12px;
+    border-radius: 8px;
+    border: 1px solid rgba(63, 80, 106, 0.15);
+    background: rgba(10, 132, 255, 0.03);
+    display: flex;
+    flex-direction: column;
+    gap: 10px;
+  }
+
+  .detail-task-name {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+    font-size: 15px;
+    font-weight: 600;
+    color: #e8edf3;
+
+    .detail-task-name-text {
+      flex: 1;
+      min-width: 0;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+    }
+  }
+
+  .detail-task-actions {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    flex-shrink: 0;
+  }
+
+  .detail-more-btn {
+    width: 24px;
+    height: 24px;
+    cursor: pointer;
+    border-radius: 4px;
+    background-image: var(--img-chat-schedule-more-icon);
+    background-repeat: no-repeat;
+    background-size: 14px 14px;
+    background-position: center;
+    transition: background-color 0.2s;
+
+    &:hover {
+      background-color: rgba(10, 132, 255, 0.15);
+    }
+  }
+
+  .schedule-switch {
+    :deep(.zxm-switch) {
+      min-width: 36px;
+      height: 20px;
+      background: rgba(63, 80, 106, 0.5);
+
+      &::after {
+        width: 16px;
+        height: 16px;
+        top: 2px;
+        left: 2px;
+        border-radius: 50%;
+        background: #fff;
+        box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
+        transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
+      }
+
+      &.zxm-switch-checked {
+        background: rgba(10, 132, 255, 0.6);
+
+        &::after {
+          left: calc(100% - 18px);
+        }
+      }
+    }
+  }
+
+  .detail-task-icon {
+    width: 20px;
+    height: 20px;
+    background-image: var(--img-chat-options-schedule-icon);
+    background-repeat: no-repeat;
+    background-size: 100% 100%;
+    background-position: center;
+    opacity: 0.8;
+  }
+
+  .detail-task-meta {
+    display: flex;
+    align-items: center;
+    gap: 20px;
+    font-size: 13px;
+  }
+
+  .detail-task-cron,
+  .detail-task-next {
+    .info-label {
+      color: #6a7a8a;
+    }
+
+    .info-value {
+      color: #5cb8ff;
+    }
+  }
+
+  .detail-task-prompt {
+    font-size: 13px;
+    color: #8b949e;
+    line-height: 1.6;
+  }
+
+  /* 执行历史 */
+  .detail-history-section {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    gap: 10px;
+    overflow: hidden;
+  }
+
+  .detail-history-title {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    font-size: 14px;
+    font-weight: 600;
+    color: #e8edf3;
+
+    .history-stats {
+      display: inline-flex;
+      align-items: center;
+      gap: 0;
+      font-size: 11px;
+      font-weight: 400;
+      line-height: 1.5;
+
+      .stats-total {
+        color: #8b949e;
+      }
+
+      .stats-sep {
+        color: #4a5568;
+        margin: 0 4px;
+      }
+
+      .stats-success {
+        color: #52c41a;
+      }
+
+      .stats-failed {
+        color: #ff7875;
+      }
+    }
+  }
+
+  .detail-history-list {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    gap: 8px;
+    overflow-y: auto;
+
+    &::-webkit-scrollbar {
+      width: 4px;
+    }
+
+    &::-webkit-scrollbar-thumb {
+      background: rgba(63, 80, 106, 0.4);
+      border-radius: 2px;
+    }
+  }
+
+  .detail-history-loading {
+    display: flex;
+    justify-content: center;
+    padding: 20px 0;
+  }
+
+  .history-item {
+    padding: 10px 12px;
+    background: rgba(10, 132, 255, 0.04);
+    border: 1px solid rgba(63, 80, 106, 0.15);
+    border-radius: 6px;
+  }
+
+  .history-item-header {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    margin-bottom: 6px;
+
+    .history-status {
+      font-size: 12px;
+    }
+
+    .history-time {
+      font-size: 12px;
+      color: #6a7a8a;
+    }
+
+    .history-duration {
+      font-size: 12px;
+      color: #8b949e;
+      margin-left: auto;
+    }
+  }
+
+  .history-output {
+    font-size: 12px;
+    color: #c9d1d9;
+    line-height: 1.6;
+    max-height: 60px;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    display: -webkit-box;
+    -webkit-line-clamp: 3;
+    -webkit-box-orient: vertical;
+  }
+
+  .history-error {
+    font-size: 12px;
+    color: #ff7875;
+    line-height: 1.6;
+  }
+
+  .history-empty {
+    font-size: 12px;
+    color: #6a7a8a;
+    text-align: center;
+    padding: 8px 0;
+  }
+</style>
+
+<style lang="less">
+  .schedule-more-popover {
+    .zxm-popover-inner {
+      background: linear-gradient(135deg, #0a1628 0%, #0d2137 50%, #0a1e35 100%);
+      border: 1px solid rgba(63, 80, 106, 0.4);
+      border-radius: 8px;
+      padding: 4px;
+    }
+
+    .zxm-popover-arrow::before {
+      background: #0d2137;
+      border-color: rgba(63, 80, 106, 0.4);
+    }
+    .zxm-popover-inner-content {
+      padding: 0;
+    }
+  }
+</style>
+
+<style scoped lang="less">
+  .more-menu {
+    display: flex;
+    flex-direction: column;
+    gap: 2px;
+
+    .more-menu-item {
+      display: flex;
+      align-items: center;
+      gap: 8px;
+      padding: 8px 12px;
+      border-radius: 6px;
+      cursor: pointer;
+      font-size: 13px;
+      color: #e0e6ed;
+      transition: background 0.2s;
+
+      &:hover {
+        background: rgba(10, 132, 255, 0.15);
+      }
+
+      &.delete {
+        color: #ff7875;
+
+        &:hover {
+          background: rgba(255, 77, 79, 0.1);
+        }
+      }
+
+      .more-menu-icon {
+        width: 16px;
+        height: 16px;
+        flex-shrink: 0;
+        background-repeat: no-repeat;
+        background-size: 14px 14px;
+        background-position: center;
+
+        &.run-icon {
+          background-image: var(--img-chat-schedule-run-icon);
+        }
+
+        &.edit-icon {
+          background-image: var(--img-chat-edit-icon);
+        }
+
+        &.delete-icon {
+          background-image: var(--img-chat-delete-icon);
+        }
+      }
+    }
+  }
+</style>

+ 672 - 0
src/views/ventAI/manageAssistent/components/chatModal/SchedulePanel.vue

@@ -0,0 +1,672 @@
+<template>
+  <div class="schedule-panel">
+    <div class="schedule-panel-header">
+      <span class="schedule-panel-title">定时任务</span>
+      <div class="schedule-panel-close" @click="emit('close')">
+        <div class="close-icon"></div>
+      </div>
+    </div>
+
+    <div class="schedule-panel-body" v-if="!loading">
+      <div class="schedule-panel-desc">
+        <span class="schedule-desc-text">让"通安智风"大模型按计划自动执行任务,并把结果定时送达</span>
+        <a-button type="primary" class="schedule-create-btn" @click="handleCreate">
+          <template #icon><PlusOutlined /></template>
+          创建
+        </a-button>
+      </div>
+
+      <!-- 有任务列表 -->
+      <div v-if="scheduleList.length > 0" class="schedule-list">
+        <div v-for="item in scheduleList" :key="item.id" class="schedule-item">
+          <div class="schedule-item-header">
+            <div class="schedule-item-name">
+              <div class="schedule-item-icon"></div>
+              <span>{{ item.name }}</span>
+            </div>
+            <div class="schedule-item-actions">
+              <a-switch :checked="item.enabled" size="small" class="schedule-switch" @change="handleToggle(item)" />
+              <div class="schedule-run-btn" @click.stop="handleRunNow(item)" :class="{ running: runningIds.has(item.id) }" title="立即执行"></div>
+              <div class="schedule-item-edit" @click.stop="handleEdit(item)" title="编辑"></div>
+              <div class="schedule-item-delete" @click.stop="handleDelete(item)" title="删除"></div>
+            </div>
+          </div>
+          <div class="schedule-item-info">
+            <div class="schedule-item-prompt">{{ item.prompt }}</div>
+            <div class="schedule-item-meta">
+              <div class="schedule-item-cron">
+                <span class="info-label">执行计划:</span>
+                <span class="info-value">{{ item.cron_human || item.cron_expr }}</span>
+              </div>
+              <div v-if="item.next_run_at" class="schedule-item-next">
+                <span class="info-label">下次执行:</span>
+                <span class="info-value">{{ item.next_run_at }}</span>
+              </div>
+            </div>
+            <div class="schedule-item-history-toggle" @click.stop="toggleHistory(item.id)">
+              <span>{{ expandedIds.has(item.id) ? '收起历史' : '执行历史' }}</span>
+              <div :class="['toggle-arrow', { expanded: expandedIds.has(item.id) }]"></div>
+              <template v-if="historyStatsMap[item.id]">
+                <span class="history-stats">
+                  <span class="stats-total">共{{ historyStatsMap[item.id].total }}次</span>
+                  <span class="stats-sep">·</span>
+                  <span class="stats-success">{{ historyStatsMap[item.id].success }}次成功</span>
+                  <span class="stats-sep">·</span>
+                  <span class="stats-failed">{{ historyStatsMap[item.id].failed }}次失败</span>
+                </span>
+              </template>
+            </div>
+            <div v-if="expandedIds.has(item.id)" class="schedule-history">
+              <a-spin v-if="historyLoadingMap[item.id]" size="small" />
+              <template v-else-if="historyMap[item.id]?.length">
+                <div v-for="run in historyMap[item.id]" :key="run.id" class="history-item">
+                  <div class="history-item-header">
+                    <a-tag :color="runStatusColor(run.status)" class="history-status">{{ runStatusLabel(run.status) }}</a-tag>
+                    <span class="history-time">{{ run.started_at }}</span>
+                    <span v-if="run.duration_ms" class="history-duration">{{ (run.duration_ms / 1000).toFixed(0) }}s</span>
+                  </div>
+                  <div v-if="run.output" class="history-output">{{ run.output }}</div>
+                  <div v-if="run.error" class="history-error">{{ run.error }}</div>
+                </div>
+              </template>
+              <div v-else class="history-empty">暂无执行记录</div>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <!-- 空状态 -->
+      <div v-else class="schedule-empty" @click="handleCreate">
+        <div class="schedule-empty-icon"></div>
+        <span class="schedule-empty-text">创建定时任务</span>
+      </div>
+    </div>
+
+    <div class="schedule-panel-loading" v-else>
+      <a-spin />
+    </div>
+
+    <CreateScheduleModal
+      :visible="showCreateModal"
+      :editData="editData"
+      @close="showCreateModal = false"
+      @created="refreshAndNotify"
+      @updated="refreshAndNotify"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { ref, reactive, onMounted } from 'vue';
+  import { PlusOutlined } from '@ant-design/icons-vue';
+  import { getScheduleList, deleteSchedule, pauseSchedule, resumeSchedule, getScheduleRuns, runScheduleNow } from '../../api';
+  import { message, Modal } from 'ant-design-vue';
+  import CreateScheduleModal from './CreateScheduleModal.vue';
+
+  const emit = defineEmits<{ (e: 'close'): void; (e: 'open-session', sessionId: string): void; (e: 'data-changed'): void }>();
+
+  interface ScheduleItem {
+    id: number;
+    name: string;
+    cron_expr: string;
+    cron_human: string;
+    prompt: string;
+    enabled: boolean;
+    next_run_at: string | null;
+    feedback_session_id?: string;
+    schedule?: { freq: string; human?: string };
+  }
+
+  const scheduleList = ref<ScheduleItem[]>([]);
+  const loading = ref(false);
+  const showCreateModal = ref(false);
+  const editData = ref<ScheduleItem | null>(null);
+  const runningIds = ref(new Set<number>());
+  const expandedIds = ref(new Set<number>());
+  const historyMap = reactive<Record<number, any[]>>({});
+  const historyLoadingMap = reactive<Record<number, boolean>>({});
+  const historyStatsMap = reactive<Record<number, { total: number; success: number; failed: number }>>({});
+
+  const fetchScheduleList = async () => {
+    loading.value = true;
+    try {
+      const res = await getScheduleList();
+      scheduleList.value = res?.tasks || res?.data || (Array.isArray(res) ? res : []);
+    } catch {
+      scheduleList.value = [];
+    } finally {
+      loading.value = false;
+    }
+  };
+
+  const refreshAndNotify = async () => {
+    await fetchScheduleList();
+    emit('data-changed');
+  };
+
+  const handleCreate = () => {
+    editData.value = null;
+    showCreateModal.value = true;
+  };
+
+  const handleEdit = (item: ScheduleItem) => {
+    editData.value = item;
+    showCreateModal.value = true;
+  };
+
+  const handleToggle = async (item: ScheduleItem) => {
+    try {
+      if (item.enabled) {
+        await pauseSchedule(item.id);
+        message.success(`已暂停任务「${item.name}」`);
+      } else {
+        await resumeSchedule(item.id);
+        message.success(`已恢复任务「${item.name}」`);
+      }
+      item.enabled = !item.enabled;
+    } catch {
+      message.error('操作失败');
+    }
+  };
+
+  const handleDelete = (item: ScheduleItem) => {
+    Modal.confirm({
+      title: '提示',
+      content: `是否删除定时任务「${item.name}」?`,
+      okText: '确认',
+      cancelText: '取消',
+      wrapClassName: 'delete-task-confirm-modal',
+      onOk: async () => {
+        try {
+          await deleteSchedule(item.id);
+          message.success('删除成功');
+          refreshAndNotify();
+        } catch {
+          message.error('删除失败');
+        }
+      },
+    });
+  };
+
+  const handleRunNow = async (item: ScheduleItem) => {
+    runningIds.value.add(item.id);
+    try {
+      await runScheduleNow(item.id);
+      message.success(`任务「${item.name}」已开始执行`);
+      if (item.feedback_session_id) {
+        emit('open-session', item.feedback_session_id);
+      }
+    } catch {
+      message.error('执行失败');
+    } finally {
+      runningIds.value.delete(item.id);
+    }
+  };
+
+  const toggleHistory = async (taskId: number) => {
+    if (expandedIds.value.has(taskId)) {
+      expandedIds.value.delete(taskId);
+      return;
+    }
+    expandedIds.value.add(taskId);
+    if (!historyMap[taskId]) {
+      historyLoadingMap[taskId] = true;
+      try {
+        const res = await getScheduleRuns(taskId);
+        const runs = res?.runs || (Array.isArray(res) ? res : []);
+        historyMap[taskId] = runs;
+        historyStatsMap[taskId] = {
+          total: res?.total ?? runs.length,
+          success: runs.filter((r: any) => r.status === 'success').length,
+          failed: runs.filter((r: any) => r.status === 'failed').length,
+        };
+      } catch {
+        historyMap[taskId] = [];
+      } finally {
+        historyLoadingMap[taskId] = false;
+      }
+    }
+  };
+
+  const runStatusLabel = (status: string) => {
+    const map: Record<string, string> = { running: '执行中', success: '成功', failed: '失败', skipped: '跳过' };
+    return map[status] || status;
+  };
+
+  const runStatusColor = (status: string) => {
+    const map: Record<string, string> = { running: 'blue', success: 'green', failed: 'red', skipped: 'default' };
+    return map[status] || 'default';
+  };
+
+  defineExpose({ fetchScheduleList });
+
+  onMounted(() => {
+    fetchScheduleList();
+  });
+</script>
+
+<style scoped lang="less">
+  .schedule-panel {
+    height: 100%;
+    display: flex;
+    flex-direction: column;
+    overflow: hidden;
+    background-image: var(--img-chat-panel-bg);
+    background-repeat: no-repeat;
+    background-size: 100% 100%;
+    border-radius: 4px;
+  }
+
+  .schedule-panel-header {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: 14px 20px;
+    border-bottom: 1px solid rgba(63, 80, 106, 0.25);
+    flex-shrink: 0;
+
+    .schedule-panel-title {
+      font-size: 16px;
+      font-weight: 600;
+      color: #e8edf3;
+      letter-spacing: 1px;
+    }
+
+    .schedule-panel-close {
+      width: 28px;
+      height: 28px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      cursor: pointer;
+      border-radius: 4px;
+      transition: background 0.2s;
+
+      &:hover {
+        background: rgba(255, 77, 79, 0.15);
+      }
+
+      .close-icon {
+        width: 16px;
+        height: 16px;
+        background-image: var(--img-chat-close-icon-btn);
+        background-repeat: no-repeat;
+        background-size: 100% 100%;
+      }
+    }
+  }
+
+  .schedule-panel-desc {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    gap: 16px;
+    margin-bottom: 16px;
+
+    .schedule-desc-text {
+      font-size: 13px;
+      color: #6a7a8a;
+      line-height: 1.5;
+    }
+
+    .schedule-create-btn {
+      flex-shrink: 0;
+      height: 32px;
+      border-radius: 6px;
+      font-size: 13px;
+      display: flex;
+      align-items: center;
+      gap: 4px;
+    }
+  }
+
+  .schedule-panel-body {
+    flex: 1;
+    overflow-y: auto;
+    padding: 16px 20px;
+  }
+
+  .schedule-panel-loading {
+    flex: 1;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+  }
+
+  /* 任务列表 */
+  .schedule-list {
+    display: flex;
+    flex-direction: column;
+    gap: 10px;
+  }
+
+  .schedule-item {
+    padding: 16px 18px;
+    border-radius: 8px;
+    border: 1px solid rgba(63, 80, 106, 0.15);
+    background: rgba(10, 132, 255, 0.03);
+    transition: all 0.25s ease;
+
+    &:hover {
+      background: rgba(10, 132, 255, 0.08);
+      border-color: rgba(10, 132, 255, 0.25);
+    }
+  }
+
+  .schedule-item-header {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    margin-bottom: 10px;
+  }
+
+  .schedule-item-name {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+    font-size: 15px;
+    font-weight: 600;
+    color: #e8edf3;
+  }
+
+  .schedule-item-icon {
+    width: 20px;
+    height: 20px;
+    background-image: var(--img-chat-options-schedule-icon);
+    background-repeat: no-repeat;
+    background-size: 100% 100%;
+    background-position: center;
+    opacity: 0.8;
+  }
+
+  .schedule-item-actions {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+  }
+
+  .schedule-item-edit {
+    width: 24px;
+    height: 24px;
+    cursor: pointer;
+    border-radius: 4px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    transition: background 0.2s;
+    background-image: var(--img-chat-edit-icon);
+    background-repeat: no-repeat;
+    background-size: 14px 14px;
+    background-position: center;
+
+    &:hover {
+      background-color: rgba(10, 132, 255, 0.15);
+    }
+  }
+
+  .schedule-item-delete {
+    width: 24px;
+    height: 24px;
+    cursor: pointer;
+    border-radius: 4px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    transition: background 0.2s;
+    background-image: var(--img-chat-delete-icon);
+    background-repeat: no-repeat;
+    background-size: 14px 14px;
+    background-position: center;
+
+    &:hover {
+      background-color: rgba(255, 77, 79, 0.15);
+    }
+  }
+
+  .schedule-item-info {
+    display: flex;
+    flex-direction: column;
+    gap: 6px;
+    padding-left: 30px;
+  }
+
+  .schedule-item-meta {
+    display: flex;
+    align-items: center;
+    gap: 20px;
+    font-size: 13px;
+  }
+
+  .schedule-item-cron,
+  .schedule-item-next {
+    .info-label {
+      color: #6a7a8a;
+    }
+
+    .info-value {
+      color: #5cb8ff;
+    }
+  }
+
+  .schedule-item-prompt {
+    font-size: 13px;
+    color: #8b949e;
+    line-height: 1.6;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+  }
+
+  .schedule-switch {
+    :deep(.zxm-switch) {
+      min-width: 36px;
+      height: 20px;
+      background: rgba(63, 80, 106, 0.5);
+
+      &::after {
+        width: 16px;
+        height: 16px;
+        top: 2px;
+        left: 2px;
+        border-radius: 50%;
+        background: #fff;
+        box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
+        transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
+      }
+
+      &.zxm-switch-checked {
+        background: rgba(10, 132, 255, 0.6);
+
+        &::after {
+          left: calc(100% - 18px);
+        }
+      }
+    }
+  }
+
+  .schedule-run-btn {
+    width: 24px;
+    height: 24px;
+    cursor: pointer;
+    border-radius: 4px;
+    background-image: var(--img-chat-schedule-run-icon);
+    background-repeat: no-repeat;
+    background-size: 14px 14px;
+    background-position: center;
+    transition: background-color 0.2s;
+
+    &:hover {
+      background-color: rgba(10, 132, 255, 0.15);
+    }
+
+    &.running {
+      opacity: 0.5;
+      pointer-events: none;
+    }
+  }
+
+  .schedule-item-history-toggle {
+    display: flex;
+    align-items: center;
+    gap: 4px;
+    font-size: 12px;
+    color: #5cb8ff;
+    cursor: pointer;
+    margin-top: 4px;
+    transition: opacity 0.2s;
+    width: fit-content;
+
+    &:hover {
+      opacity: 0.8;
+    }
+
+    .toggle-arrow {
+      width: 0;
+      height: 0;
+      border-left: 4px solid transparent;
+      border-right: 4px solid transparent;
+      border-top: 5px solid #5cb8ff;
+      transition: transform 0.2s;
+
+      &.expanded {
+        transform: rotate(180deg);
+      }
+    }
+
+    .history-stats {
+      display: inline-flex;
+      align-items: center;
+      gap: 0;
+      margin-left: 6px;
+      font-size: 11px;
+      line-height: 1.5;
+
+      .stats-total {
+        color: #8b949e;
+      }
+
+      .stats-sep {
+        color: #4a5568;
+        margin: 0 4px;
+      }
+
+      .stats-success {
+        color: #52c41a;
+      }
+
+      .stats-failed {
+        color: #ff7875;
+      }
+    }
+  }
+
+  .schedule-history {
+    margin-top: 10px;
+    padding: 12px;
+    background: rgba(0, 0, 0, 0.15);
+    border: 1px solid rgba(63, 80, 106, 0.18);
+    border-radius: 6px;
+    display: flex;
+    flex-direction: column;
+    gap: 10px;
+    max-height: 170px;
+    overflow-y: auto;
+
+    &::-webkit-scrollbar {
+      width: 4px;
+    }
+
+    &::-webkit-scrollbar-thumb {
+      background: rgba(63, 80, 106, 0.4);
+      border-radius: 2px;
+    }
+  }
+
+  .history-item {
+    padding: 10px 12px;
+    background: rgba(10, 132, 255, 0.04);
+    border: 1px solid rgba(63, 80, 106, 0.15);
+    border-radius: 6px;
+  }
+
+  .history-item-header {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    margin-bottom: 6px;
+
+    .history-status {
+      font-size: 12px;
+    }
+
+    .history-time {
+      font-size: 12px;
+      color: #6a7a8a;
+    }
+
+    .history-duration {
+      font-size: 12px;
+      color: #8b949e;
+      margin-left: auto;
+    }
+  }
+
+  .history-output {
+    font-size: 12px;
+    color: #c9d1d9;
+    line-height: 1.6;
+    max-height: 60px;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    display: -webkit-box;
+    -webkit-line-clamp: 3;
+    -webkit-box-orient: vertical;
+  }
+
+  .history-error {
+    font-size: 12px;
+    color: #ff7875;
+    line-height: 1.6;
+  }
+
+  .history-empty {
+    font-size: 12px;
+    color: #6a7a8a;
+    text-align: center;
+    padding: 8px 0;
+  }
+
+  /* 空状态 */
+  .schedule-empty {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    height: 100%;
+    gap: 16px;
+    cursor: pointer;
+    padding: 40px;
+    border-radius: 12px;
+    transition: background 0.25s;
+
+    &:hover {
+      background: rgba(10, 132, 255, 0.06);
+    }
+
+    .schedule-empty-icon {
+      width: 120px;
+      height: 120px;
+      background-image: var(--img-chat-schedule-empty-icon);
+      background-repeat: no-repeat;
+      background-size: 100% 100%;
+      background-position: center;
+      opacity: 0.8;
+    }
+
+    .schedule-empty-text {
+      font-size: 15px;
+      color: #fff;
+      letter-spacing: 0.5px;
+    }
+  }
+</style>

+ 523 - 0
src/views/ventAI/manageAssistent/components/chatModal/SkillPanel.vue

@@ -0,0 +1,523 @@
+<template>
+  <div class="skill-panel">
+    <div class="skill-panel-header">
+      <span class="skill-panel-title">模型技能</span>
+      <div class="skill-panel-close" @click="emit('close')">
+        <div class="close-icon"></div>
+      </div>
+    </div>
+    <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>
+  </div>
+</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 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;
+    }
+  };
+
+  fetchSkills();
+</script>
+
+<style scoped lang="less">
+  .skill-panel {
+    height: 100%;
+    display: flex;
+    flex-direction: column;
+    overflow: hidden;
+    background-image: var(--img-chat-panel-bg);
+    background-repeat: no-repeat;
+    background-size: 100% 100%;
+    border-radius: 4px;
+  }
+
+  .skill-panel-header {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: 14px 20px;
+    border-bottom: 1px solid rgba(63, 80, 106, 0.25);
+    flex-shrink: 0;
+
+    .skill-panel-title {
+      font-size: 16px;
+      font-weight: 600;
+      color: #e8edf3;
+      letter-spacing: 1px;
+    }
+
+    .skill-panel-close {
+      width: 28px;
+      height: 28px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      cursor: pointer;
+      border-radius: 4px;
+      transition: background 0.2s;
+
+      &:hover {
+        background: rgba(255, 77, 79, 0.15);
+      }
+
+      .close-icon {
+        width: 16px;
+        height: 16px;
+        background-image: var(--img-chat-close-icon-btn);
+        background-repeat: no-repeat;
+        background-size: 100% 100%;
+      }
+    }
+  }
+
+  .skill-filter-bar {
+    display: flex;
+    gap: 12px;
+    margin: 16px 20px 0;
+    flex-shrink: 0;
+
+    .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: 12px 20px 0;
+    flex-shrink: 0;
+    font-size: 13px;
+
+    .skill-summary-label {
+      color: #e8edf3;
+      font-weight: 500;
+    }
+
+    .skill-summary-count {
+      color: #6a7a8a;
+    }
+  }
+
+  .skill-list {
+    flex: 1;
+    overflow-y: auto;
+    padding: 12px 20px 20px;
+
+    &::-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 {
+    flex: 1;
+    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-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>

+ 13 - 14
src/views/ventAI/manageAssistent/components/chatModal/TaskListPanel.vue

@@ -2,13 +2,7 @@
   <div class="task-list-panel">
     <!-- 操作区域(顶部) -->
     <div class="options-area">
-      <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 v-for="item in optionItems" :key="item.type" :class="['options-area-item', `options-${item.type}`]" @click="handleOptionClick(item.type)">
         <div class="options-img">
           <div class="options-icon"></div>
         </div>
@@ -56,15 +50,12 @@
       <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[];
@@ -76,13 +67,14 @@
     (e: 'create-task'): void;
     (e: 'delete-task', task: Task, index: number): void;
     (e: 'pin-task', task: Task): void;
+    (e: 'toggle-skill'): void;
+    (e: 'toggle-schedules'): void;
   }>();
 
-  type OptionType = 'add' | 'search' | 'skill';
+  type OptionType = 'add' | 'search' | 'skill' | 'schedules';
 
   const activeOption = ref<OptionType>('add');
   const searchKeyword = ref('');
-  const skillVisible = ref(false);
 
   const pinnedTaskList = computed(() => {
     let list = props.taskList.filter((task) => task.sortOrder === 1);
@@ -108,8 +100,9 @@
 
   const optionItems = [
     { type: 'add' as OptionType, label: '新建任务' },
-    { type: 'search' as OptionType, label: '搜索' },
+    // { type: 'search' as OptionType, label: '搜索' },
     { type: 'skill' as OptionType, label: '技能' },
+    { type: 'schedules' as OptionType, label: '定时任务' },
   ];
 
   const formatRelativeTime = (dateStr?: string) => {
@@ -127,7 +120,9 @@
     if (type === 'add') {
       emit('create-task');
     } else if (type === 'skill') {
-      skillVisible.value = true;
+      emit('toggle-skill');
+    } else if (type === 'schedules') {
+      emit('toggle-schedules');
     } else {
       activeOption.value = type;
       searchKeyword.value = '';
@@ -365,6 +360,10 @@
           background-image: var(--img-chat-options-skill-icon);
         }
 
+        &.options-schedules .options-icon {
+          background-image: var(--img-chat-options-schedule-icon);
+        }
+
         .zxm-btn {
           font-size: 14px;
           padding: 0;