Explorar o código

[Feat 0000]AI智能助手添加子智能体功能

wangkeyi hai 3 semanas
pai
achega
fa5b0ec7eb

+ 1 - 0
src/assets/images/ventAI/chatModalBlue/4-12.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="1787100187243" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9029" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M663.79341465 104.15625049a50.43682089 50.43682089 0 0 1 99.4677372 16.69511513l-20.20987529 120.81991026h52.28206963A166.33606553 166.33606553 0 0 1 961.84515078 407.91947276v232.58931151a166.33606553 166.33606553 0 0 1-166.51180459 166.24819688h-566.7552167A166.33606553 166.33606553 0 0 1 62.06632578 640.50878428V407.91947276a166.33606553 166.33606553 0 0 1 166.51180372-166.24819688h52.01846367l-20.20987617-120.81991026a50.34895224 50.34895224 0 0 1 99.4677372-16.69511513L382.78827002 241.67127588h257.89559063l23.02168534-137.51502539z m131.53993154 227.40503906h-566.7552167a76.44605273 76.44605273 0 0 0-76.62179004 76.35818321v232.58931151c0 42.17713242 34.26892031 76.44605273 76.62179004 76.44605274h566.7552167c42.26500107 0 76.62179092-34.26892031 76.62179092-76.44605274V407.91947276c0-42.17713242-34.26892031-76.44605273-76.62179092-76.44605274v0.08786953zM333.23013916 431.11689541a50.43682089 50.43682089 0 0 1 50.4368209 50.4368209v44.19812021a50.43682089 50.43682089 0 0 1-100.96151045 0v-44.19812021a50.34895224 50.34895224 0 0 1 50.52468955-50.4368209z m357.62693555 0a50.43682089 50.43682089 0 0 1 50.43682089 50.4368209v44.19812021a50.34895224 50.34895224 0 0 1-100.87364179 0v-44.19812021a50.34895224 50.34895224 0 0 1 50.4368209-50.4368209z m96.30445253 485.91571289c0 24.77906543-20.12200664 44.90107208-44.90107206 44.90107295H281.91462823a44.98894161 44.98894161 0 1 1-1e-8-89.89001367H742.17258565a44.98894161 44.98894161 0 0 1 44.98894159 44.98894072z" fill="#ffffff" p-id="9030"></path></svg>

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

@@ -76,6 +76,7 @@ html[data-theme='deepblue'] {
   --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-sub-agent-icon: url('/src/assets/images/ventAI/chatModalBlue/4-12.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');

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

@@ -84,6 +84,7 @@ html {
   --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-options-sub-agent-icon: url('/src/assets/images/ventAI/chatModalBlue/4-12.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');

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

@@ -393,3 +393,48 @@ export const getScheduleRuns = (task_id: number, limit = 50) =>
 
 export const runScheduleNow = (task_id: number) =>
   defHttp.post({ url: `${Api.schedules}/${task_id}/run` }, { isTransformResponse: false });
+
+/**
+ * 获取子智能体列表
+ */
+export const getSubAgents = () => defHttp.get({ url: '/ventAI/api/subagents' }, { isTransformResponse: false });
+
+/**
+ * 获取工具注册表与模型列表(表单用)
+ */
+export const getSubAgentTools = () => defHttp.get({ url: '/ventAI/api/subagents/tools' }, { isTransformResponse: false });
+
+/**
+ * 创建子智能体
+ */
+export const createSubAgent = (data: {
+  name: string;
+  description: string;
+  system_prompt: string;
+  tools?: string[];
+  model?: string;
+  enabled?: boolean;
+}) => defHttp.post({ url: '/ventAI/api/subagents', data }, { isTransformResponse: false });
+
+/**
+ * 获取子智能体详情
+ */
+export const getSubAgentDetail = (name: string) => defHttp.get({ url: `/ventAI/api/subagents/${name}` }, { isTransformResponse: false });
+
+/**
+ * 更新子智能体
+ */
+export const updateSubAgent = (
+  name: string,
+  data: { description?: string; system_prompt?: string; tools?: string[]; model?: string; enabled?: boolean }
+) => defHttp.put({ url: `/ventAI/api/subagents/${name}`, data }, { isTransformResponse: false });
+
+/**
+ * 删除子智能体
+ */
+export const deleteSubAgent = (name: string) => defHttp.delete({ url: `/ventAI/api/subagents/${name}` }, { isTransformResponse: false });
+
+/**
+ * 启用/禁用子智能体
+ */
+export const toggleSubAgent = (name: string) => defHttp.post({ url: `/ventAI/api/subagents/${name}/toggle` }, { isTransformResponse: false });

+ 19 - 2
src/views/ventAI/manageAssistent/components/AiAssistantModal.vue

@@ -29,6 +29,7 @@
           @delete-task="handleDeleteTask"
           @pin-task="handlePinTask"
           @toggle-skill="toggleSkillPanel"
+          @toggle-sub-agents="toggleSubAgentPanel"
           @toggle-schedules="toggleSchedulePanel"
         />
       </div>
@@ -136,6 +137,11 @@
         <SkillPanel @close="activePanel = 'chat'" />
       </div>
 
+      <!-- 子智能体面板 -->
+      <div v-if="activePanel === 'sub-agents'" class="right-panel full">
+        <SubAgentPanel @close="activePanel = 'chat'" />
+      </div>
+
       <!-- 定时任务面板 -->
       <div v-if="activePanel === 'schedules'" class="right-panel full">
         <SchedulePanel
@@ -176,6 +182,7 @@
   import TodoListBar from './chatModal/TodoListBar.vue';
   import DownloadConfirmBar from './chatModal/DownloadConfirmBar.vue';
   import SkillPanel from './chatModal/SkillPanel.vue';
+  import SubAgentPanel from './chatModal/SubAgentPanel.vue';
   import SchedulePanel from './chatModal/SchedulePanel.vue';
   import ScheduleDetailPanel from './chatModal/ScheduleDetailPanel.vue';
   import { getScheduleList } from '../api';
@@ -260,7 +267,7 @@
 
   const previewFile = ref<AttachedFile | null>(null);
   const showFileList = ref(false);
-  const activePanel = ref<'chat' | 'skill' | 'schedules'>('chat');
+  const activePanel = ref<'chat' | 'skill' | 'sub-agents' | 'schedules'>('chat');
   const scheduleListForDetail = ref<any[]>([]);
   const showScheduleDetail = ref(false);
 
@@ -355,6 +362,15 @@
     }
   };
 
+  const toggleSubAgentPanel = () => {
+    if (activePanel.value === 'sub-agents') {
+      activePanel.value = 'chat';
+    } else {
+      activePanel.value = 'sub-agents';
+      previewFile.value = null;
+    }
+  };
+
   const handleOpenScheduleSession = async (sessionId: string) => {
     activePanel.value = 'chat';
     previewFile.value = null;
@@ -2153,7 +2169,8 @@
         background-image: var(--img-chat-fs-panel-bg) !important;
       }
       .schedule-panel,
-      .skill-panel {
+      .skill-panel,
+      .sub-agent-panel {
         background-image: var(--img-chat-fs-panel-bg) !important;
       }
       .schedule-detail-panel {

+ 649 - 0
src/views/ventAI/manageAssistent/components/chatModal/CreateSubAgentModal.vue

@@ -0,0 +1,649 @@
+<template>
+  <a-modal
+    :visible="visible"
+    :title="isEdit ? '编辑子智能体' : '创建子智能体'"
+    :width="700"
+    :footer="null"
+    destroyOnClose
+    @cancel="emit('close')"
+    class="create-sub-agent-modal"
+  >
+    <div class="sub-agent-form" v-if="!toolsLoading">
+      <div class="form-item">
+        <label class="form-label">名称 <span class="required">*</span></label>
+        <a-input
+          v-model:value="form.name"
+          placeholder="英文字母、数字、_、-(1-50字符)"
+          :maxlength="50"
+          :disabled="isEdit"
+        />
+        <div class="form-hint" v-if="isEdit">名称创建后不可修改</div>
+      </div>
+
+      <div class="form-item">
+        <label class="form-label">描述 <span class="required">*</span></label>
+        <a-textarea v-model:value="form.description" placeholder="主 Agent 据此判断何时委派任务" :rows="2" :maxlength="500" />
+      </div>
+
+      <div class="form-item">
+        <label class="form-label">系统提示词 <span class="required">*</span></label>
+        <a-textarea v-model:value="form.system_prompt" placeholder="定义子智能体的角色、能力和行为规范" :rows="5" :maxlength="5000" />
+      </div>
+
+      <div class="form-item">
+        <label class="form-label">可用工具</label>
+        <div class="form-hint">不选择任何工具时,自动继承对话助手的全部工具</div>
+        <div class="tools-selector">
+          <div class="tools-selector-sidebar">
+            <div
+              v-for="group in toolGroups"
+              :key="group.key"
+              :class="['sidebar-tab', { active: activeGroupKey === group.key }]"
+              @click="activeGroupKey = group.key"
+            >
+              <span class="sidebar-tab-name">{{ group.display }}</span>
+              <span v-if="getGroupSelectedCount(group) > 0" class="sidebar-tab-badge">{{ getGroupSelectedCount(group) }}</span>
+            </div>
+          </div>
+          <div class="tools-selector-main">
+            <div class="tools-search-bar">
+              <a-input v-model:value="toolSearch" placeholder="搜索工具..." allow-clear size="small" class="tools-search-input" />
+            </div>
+            <div class="tools-tags-area" v-if="activeGroup">
+              <div class="tools-tags-header">
+                <span class="tools-tags-title">{{ activeGroup.display }}</span>
+                <span class="tools-tags-count">已选 {{ getGroupSelectedCount(activeGroup) }}/{{ activeGroup.tools.length }}</span>
+                <div class="tools-tags-ops">
+                  <span class="tools-op-link" @click="selectAllInGroup(activeGroup)">全选</span>
+                  <span class="tools-op-sep">|</span>
+                  <span class="tools-op-link" @click="clearGroupTools(activeGroup)">清空</span>
+                </div>
+              </div>
+              <div class="tools-tags-wrap">
+                <div
+                  v-for="tool in filteredActiveTools"
+                  :key="tool.name"
+                  :class="['tool-tag-item', { selected: form.tools.includes(tool.name) }]"
+                  @click="toggleTool(tool.name)"
+                  :title="tool.description"
+                >
+                  <span class="tool-tag-name">{{ tool.display_name }}</span>
+                  <div v-if="form.tools.includes(tool.name)" class="tool-tag-check"></div>
+                </div>
+                <div v-if="filteredActiveTools.length === 0" class="tools-empty-hint">无匹配工具</div>
+              </div>
+            </div>
+          </div>
+        </div>
+        <div v-if="form.tools.length > 0" class="tools-selected-summary">
+          <span class="summary-label">已选 {{ form.tools.length }} 项</span>
+          <span class="summary-clear" @click="form.tools = []">清空全部</span>
+        </div>
+      </div>
+
+      <div class="form-item">
+        <label class="form-label">模型</label>
+        <a-select v-model:value="form.model" class="form-select" placeholder="留空则跟随对话助手当前模型" allow-clear>
+          <a-select-option v-for="m in modelList" :key="m" :value="m">{{ m }}</a-select-option>
+        </a-select>
+      </div>
+
+      <div class="form-item form-switch-item">
+        <label class="form-label">启用</label>
+        <a-switch v-model:checked="form.enabled" />
+      </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>
+
+    <div class="form-loading" v-else>
+      <a-spin />
+    </div>
+  </a-modal>
+</template>
+
+<script setup lang="ts">
+  import { ref, reactive, computed, watch } from 'vue';
+  import { createSubAgent, updateSubAgent, getSubAgentTools } from '../../api';
+  import { message } from 'ant-design-vue';
+
+  interface ToolInfo {
+    name: string;
+    display_name: string;
+    description?: string;
+  }
+
+  interface ToolGroup {
+    key: string;
+    display: string;
+    tools: ToolInfo[];
+  }
+
+  interface SubAgentData {
+    name: string;
+    description: string;
+    system_prompt: string;
+    tools: string[];
+    tool_infos?: ToolInfo[];
+    model: string;
+    enabled: boolean;
+  }
+
+  const props = defineProps<{ visible: boolean; editData?: SubAgentData | null }>();
+  const emit = defineEmits<{
+    (e: 'close'): void;
+    (e: 'created'): void;
+    (e: 'updated'): void;
+  }>();
+
+  const submitting = ref(false);
+  const toolsLoading = ref(false);
+  const toolGroups = ref<ToolGroup[]>([]);
+  const modelList = ref<string[]>([]);
+  const activeGroupKey = ref('');
+  const toolSearch = ref('');
+
+  const activeGroup = computed(() => toolGroups.value.find((g) => g.key === activeGroupKey.value));
+
+  const filteredActiveTools = computed(() => {
+    if (!activeGroup.value) return [];
+    const keyword = toolSearch.value.trim().toLowerCase();
+    if (!keyword) return activeGroup.value.tools;
+    return activeGroup.value.tools.filter(
+      (t) => t.display_name.toLowerCase().includes(keyword) || t.name.toLowerCase().includes(keyword)
+    );
+  });
+
+  const defaultForm = () => ({
+    name: '',
+    description: '',
+    system_prompt: '',
+    tools: [] as string[],
+    model: '',
+    enabled: true,
+  });
+
+  const form = reactive(defaultForm());
+
+  const isEdit = computed(() => !!props.editData);
+
+  const fetchToolsAndModels = async () => {
+    toolsLoading.value = true;
+    try {
+      const res = await getSubAgentTools();
+      toolGroups.value = res?.groups || [];
+      modelList.value = res?.models || [];
+      if (toolGroups.value.length > 0 && !activeGroupKey.value) {
+        activeGroupKey.value = toolGroups.value[0].key;
+      }
+    } catch {
+      toolGroups.value = [];
+      modelList.value = [];
+    } finally {
+      toolsLoading.value = false;
+    }
+  };
+
+  watch(
+    () => props.visible,
+    (val) => {
+      if (val) {
+        Object.assign(form, defaultForm());
+        submitting.value = false;
+        activeGroupKey.value = '';
+        toolSearch.value = '';
+        fetchToolsAndModels();
+        if (props.editData) {
+          const d = props.editData;
+          form.name = d.name;
+          form.description = d.description || '';
+          form.system_prompt = d.system_prompt || '';
+          form.tools = [...(d.tools || [])];
+          form.model = d.model || '';
+          form.enabled = d.enabled !== false;
+        }
+      }
+    },
+  );
+
+  const getGroupSelectedCount = (group: ToolGroup) => {
+    return group.tools.filter((t) => form.tools.includes(t.name)).length;
+  };
+
+  const toggleTool = (name: string) => {
+    const idx = form.tools.indexOf(name);
+    if (idx >= 0) {
+      form.tools.splice(idx, 1);
+    } else {
+      form.tools.push(name);
+    }
+  };
+
+  const selectAllInGroup = (group: ToolGroup) => {
+    for (const tool of group.tools) {
+      if (!form.tools.includes(tool.name)) {
+        form.tools.push(tool.name);
+      }
+    }
+  };
+
+  const clearGroupTools = (group: ToolGroup) => {
+    const names = new Set(group.tools.map((t) => t.name));
+    form.tools = form.tools.filter((t) => !names.has(t));
+  };
+
+  const nameRegex = /^[a-zA-Z0-9_-]{1,50}$/;
+
+  const canSubmit = computed(() => {
+    if (!form.name.trim() || !form.description.trim() || !form.system_prompt.trim()) return false;
+    if (!isEdit.value && !nameRegex.test(form.name.trim())) return false;
+    return true;
+  });
+
+  const handleSubmit = async () => {
+    if (!canSubmit.value) return;
+    submitting.value = true;
+    try {
+      if (isEdit.value) {
+        await updateSubAgent(form.name, {
+          description: form.description.trim(),
+          system_prompt: form.system_prompt.trim(),
+          tools: form.tools,
+          model: form.model || '',
+          enabled: form.enabled,
+        });
+        message.success(`已更新子智能体「${form.name}」`);
+        emit('updated');
+      } else {
+        await createSubAgent({
+          name: form.name.trim(),
+          description: form.description.trim(),
+          system_prompt: form.system_prompt.trim(),
+          tools: form.tools,
+          model: form.model || '',
+          enabled: form.enabled,
+        });
+        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">
+  .sub-agent-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-hint {
+    font-size: 12px;
+    color: #6a7a8a;
+    margin-top: -2px;
+  }
+
+  :deep(.zxm-input),
+  :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);
+    }
+
+    &[disabled] {
+      opacity: 0.6;
+      cursor: not-allowed;
+    }
+  }
+
+  .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-select-selection-placeholder) {
+      color: #6a7a8a;
+    }
+  }
+
+  .form-switch-item {
+    flex-direction: row;
+    align-items: center;
+    gap: 12px;
+  }
+
+  .tools-selector {
+    display: flex;
+    border: 1px solid rgba(63, 80, 106, 0.3);
+    border-radius: 6px;
+    background: rgba(10, 132, 255, 0.03);
+    height: 220px;
+    overflow: hidden;
+  }
+
+  .tools-selector-sidebar {
+    width: 140px;
+    flex-shrink: 0;
+    border-right: 1px solid rgba(63, 80, 106, 0.2);
+    overflow-y: auto;
+
+    &::-webkit-scrollbar {
+      width: 4px;
+    }
+
+    &::-webkit-scrollbar-thumb {
+      background: rgba(63, 80, 106, 0.3);
+      border-radius: 2px;
+    }
+
+    .sidebar-tab {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      padding: 10px 12px;
+      cursor: pointer;
+      transition: all 0.2s;
+      border-left: 2px solid transparent;
+
+      &:hover {
+        background: rgba(10, 132, 255, 0.06);
+      }
+
+      &.active {
+        background: rgba(10, 132, 255, 0.1);
+        border-left-color: #0a84ff;
+
+        .sidebar-tab-name {
+          color: #5cb8ff;
+        }
+      }
+
+      .sidebar-tab-name {
+        font-size: 13px;
+        color: #c9d1d9;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        white-space: nowrap;
+      }
+
+      .sidebar-tab-badge {
+        min-width: 18px;
+        height: 18px;
+        line-height: 18px;
+        text-align: center;
+        font-size: 11px;
+        color: #fff;
+        background: rgba(10, 132, 255, 0.7);
+        border-radius: 9px;
+        padding: 0 5px;
+        flex-shrink: 0;
+      }
+    }
+  }
+
+  .tools-selector-main {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    min-width: 0;
+    overflow: hidden;
+  }
+
+  .tools-search-bar {
+    padding: 8px 12px;
+    border-bottom: 1px solid rgba(63, 80, 106, 0.15);
+    flex-shrink: 0;
+
+    .tools-search-input {
+      height: 30px;
+      background: rgba(10, 132, 255, 0.06);
+      border: 1px solid rgba(63, 80, 106, 0.3);
+
+      :deep(.zxm-input) {
+        background: transparent;
+        color: #e0e6ed;
+        font-size: 12px;
+      }
+    }
+  }
+
+  .tools-tags-area {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    overflow: hidden;
+  }
+
+  .tools-tags-header {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    padding: 8px 12px 0;
+    flex-shrink: 0;
+
+    .tools-tags-title {
+      font-size: 12px;
+      font-weight: 500;
+      color: #8b949e;
+    }
+
+    .tools-tags-count {
+      font-size: 11px;
+      color: #6a7a8a;
+    }
+
+    .tools-tags-ops {
+      margin-left: auto;
+      display: flex;
+      align-items: center;
+      gap: 6px;
+    }
+
+    .tools-op-link {
+      font-size: 11px;
+      color: #5cb8ff;
+      cursor: pointer;
+      transition: opacity 0.2s;
+
+      &:hover {
+        opacity: 0.8;
+      }
+    }
+
+    .tools-op-sep {
+      color: rgba(63, 80, 106, 0.4);
+      font-size: 11px;
+    }
+  }
+
+  .tools-tags-wrap {
+    flex: 1;
+    overflow-y: auto;
+    padding: 8px 12px 12px;
+    display: flex;
+    flex-wrap: wrap;
+    gap: 8px;
+    align-content: flex-start;
+
+    &::-webkit-scrollbar {
+      width: 4px;
+    }
+
+    &::-webkit-scrollbar-thumb {
+      background: rgba(63, 80, 106, 0.3);
+      border-radius: 2px;
+    }
+  }
+
+  .tool-tag-item {
+    display: inline-flex;
+    align-items: center;
+    gap: 4px;
+    padding: 5px 12px;
+    border: 1px solid rgba(63, 80, 106, 0.3);
+    border-radius: 4px;
+    cursor: pointer;
+    transition: all 0.2s;
+    background: rgba(10, 132, 255, 0.02);
+
+    &:hover {
+      border-color: rgba(10, 132, 255, 0.4);
+      background: rgba(10, 132, 255, 0.06);
+    }
+
+    &.selected {
+      border-color: rgba(10, 132, 255, 0.6);
+      background: rgba(10, 132, 255, 0.12);
+
+      .tool-tag-name {
+        color: #5cb8ff;
+      }
+    }
+
+    .tool-tag-name {
+      font-size: 12px;
+      color: #c9d1d9;
+      white-space: nowrap;
+    }
+
+    .tool-tag-check {
+      width: 14px;
+      height: 14px;
+      flex-shrink: 0;
+      position: relative;
+
+      &::after {
+        content: '';
+        position: absolute;
+        left: 2px;
+        top: 0;
+        width: 5px;
+        height: 8px;
+        border: solid #5cb8ff;
+        border-width: 0 2px 2px 0;
+        transform: rotate(45deg);
+      }
+    }
+  }
+
+  .tools-empty-hint {
+    font-size: 12px;
+    color: #6a7a8a;
+    padding: 20px 0;
+    text-align: center;
+    width: 100%;
+  }
+
+  .tools-selected-summary {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    padding-top: 8px;
+
+    .summary-label {
+      font-size: 12px;
+      color: #5cb8ff;
+    }
+
+    .summary-clear {
+      font-size: 12px;
+      color: #ff7875;
+      cursor: pointer;
+      transition: opacity 0.2s;
+
+      &:hover {
+        opacity: 0.8;
+      }
+    }
+  }
+
+  .form-loading {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    padding: 60px 0;
+  }
+
+  .form-actions {
+    display: flex;
+    justify-content: flex-end;
+    gap: 10px;
+    padding-top: 8px;
+  }
+</style>
+
+<style lang="less">
+  .create-sub-agent-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;
+      max-height: 70vh;
+      overflow-y: auto;
+    }
+
+    .zxm-modal-close {
+      .zxm-modal-close-x {
+        color: #8b949e;
+      }
+    }
+  }
+</style>

+ 190 - 115
src/views/ventAI/manageAssistent/components/chatModal/SchedulePanel.vue

@@ -1,85 +1,92 @@
 <template>
   <div class="schedule-panel">
     <div class="schedule-panel-header">
-      <span class="schedule-panel-title">定时任务</span>
+      <div class="schedule-panel-header-left">
+        <span class="schedule-panel-title">定时任务</span>
+        <span class="schedule-panel-desc">让大模型按计划自动执行任务,并把结果定时送达</span>
+      </div>
       <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 class="schedule-filter-bar" v-if="!loading">
+      <a-input v-model:value="searchKeyword" placeholder="搜索定时任务..." allow-clear size="small" class="schedule-search-input" />
+      <a-select v-model:value="filterStatus" size="small" class="schedule-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="schedule-summary" v-if="!loading">
+      <span class="schedule-summary-label">定时任务</span>
+      <span class="schedule-summary-count">{{ filteredScheduleList.length }}项</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 class="schedule-list" v-if="!loading">
+      <div v-for="item in filteredScheduleList" :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-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 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 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 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 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>
+          <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>
-              </template>
-              <div v-else class="history-empty">暂无执行记录</div>
-            </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 v-else class="schedule-empty" @click="handleCreate">
-        <div class="schedule-empty-icon"></div>
-        <span class="schedule-empty-text">创建定时任务</span>
-      </div>
+      <template v-if="filteredScheduleList.length === 0">
+        <div v-if="searchKeyword" class="schedule-empty-hint">未找到匹配的任务</div>
+        <a-empty v-else description="暂无定时任务" />
+      </template>
     </div>
 
     <div class="schedule-panel-loading" v-else>
@@ -97,7 +104,7 @@
 </template>
 
 <script setup lang="ts">
-  import { ref, reactive, onMounted } from 'vue';
+  import { ref, reactive, computed, 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';
@@ -119,6 +126,17 @@
 
   const scheduleList = ref<ScheduleItem[]>([]);
   const loading = ref(false);
+  const searchKeyword = ref('');
+  const filterStatus = ref<'all' | 'enabled' | 'disabled'>('all');
+
+  const filteredScheduleList = computed(() => {
+    return scheduleList.value.filter((item) => {
+      const keyword = searchKeyword.value.trim().toLowerCase();
+      const matchKeyword = !keyword || item.name.toLowerCase().includes(keyword) || item.prompt.toLowerCase().includes(keyword);
+      const matchStatus = filterStatus.value === 'all' || (filterStatus.value === 'enabled' ? item.enabled : !item.enabled);
+      return matchKeyword && matchStatus;
+    });
+  });
   const showCreateModal = ref(false);
   const editData = ref<ScheduleItem | null>(null);
   const runningIds = ref(new Set<number>());
@@ -265,11 +283,28 @@
     border-bottom: 1px solid rgba(63, 80, 106, 0.25);
     flex-shrink: 0;
 
+    .schedule-panel-header-left {
+      display: flex;
+      align-items: baseline;
+      gap: 10px;
+      min-width: 0;
+      flex: 1;
+    }
+
     .schedule-panel-title {
       font-size: 16px;
       font-weight: 600;
       color: #e8edf3;
       letter-spacing: 1px;
+      flex-shrink: 0;
+    }
+
+    .schedule-panel-desc {
+      font-size: 12px;
+      color: #6a7a8a;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
     }
 
     .schedule-panel-close {
@@ -296,17 +331,66 @@
     }
   }
 
-  .schedule-panel-desc {
+  .schedule-filter-bar {
+    display: flex;
+    gap: 12px;
+    margin: 16px 20px 0;
+    flex-shrink: 0;
+
+    .schedule-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;
+      }
+    }
+
+    .schedule-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;
+      }
+    }
+  }
+
+  .schedule-summary {
     display: flex;
     align-items: center;
-    justify-content: space-between;
-    gap: 16px;
-    margin-bottom: 16px;
+    gap: 6px;
+    padding: 12px 20px 0;
+    flex-shrink: 0;
+    font-size: 13px;
 
-    .schedule-desc-text {
-      font-size: 13px;
+    .schedule-summary-label {
+      color: #e8edf3;
+      font-weight: 500;
+    }
+
+    .schedule-summary-count {
       color: #6a7a8a;
-      line-height: 1.5;
     }
 
     .schedule-create-btn {
@@ -317,31 +401,45 @@
       display: flex;
       align-items: center;
       gap: 4px;
+      margin-left: auto;
     }
   }
 
-  .schedule-panel-body {
-    flex: 1;
-    overflow-y: auto;
-    padding: 16px 20px;
-  }
-
   .schedule-panel-loading {
     flex: 1;
     display: flex;
     justify-content: center;
     align-items: center;
+    padding: 80px 0;
   }
 
   /* 任务列表 */
   .schedule-list {
-    display: flex;
-    flex-direction: column;
-    gap: 10px;
+    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);
+      }
+    }
   }
 
   .schedule-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);
@@ -351,6 +449,10 @@
       background: rgba(10, 132, 255, 0.08);
       border-color: rgba(10, 132, 255, 0.25);
     }
+
+    &:last-child {
+      margin-bottom: 0;
+    }
   }
 
   .schedule-item-header {
@@ -636,37 +738,10 @@
     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;
-    }
+  .schedule-empty-hint {
+    text-align: center;
+    color: #8b949e;
+    font-size: 13px;
+    padding: 20px 0;
   }
 </style>

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

@@ -1,5 +1,5 @@
 <template>
-  <a-modal :visible="visible" title="模型技能" :width="1000" :footer="null" destroyOnClose @cancel="emit('close')" class="skill-modal">
+  <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">

+ 45 - 15
src/views/ventAI/manageAssistent/components/chatModal/SkillPanel.vue

@@ -1,7 +1,10 @@
 <template>
   <div class="skill-panel">
     <div class="skill-panel-header">
-      <span class="skill-panel-title">模型技能</span>
+      <div class="skill-panel-header-left">
+        <span class="skill-panel-title">技能</span>
+        <span class="skill-panel-desc">管理模型技能,启用后可在聊天里通过$skill-name使用</span>
+      </div>
       <div class="skill-panel-close" @click="emit('close')">
         <div class="close-icon"></div>
       </div>
@@ -25,9 +28,7 @@
             <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>
+          <a-switch :checked="skill.enabled" size="small" class="skill-switch" @change="handleToggleSkill(skill)" />
         </div>
         <div class="skill-desc">{{ skill.description }}</div>
         <div v-if="skill.agents && skill.agents.length > 0" class="skill-agents">
@@ -150,11 +151,28 @@
     border-bottom: 1px solid rgba(63, 80, 106, 0.25);
     flex-shrink: 0;
 
+    .skill-panel-header-left {
+      display: flex;
+      align-items: baseline;
+      gap: 10px;
+      min-width: 0;
+      flex: 1;
+    }
+
     .skill-panel-title {
       font-size: 16px;
       font-weight: 600;
       color: #e8edf3;
       letter-spacing: 1px;
+      flex-shrink: 0;
+    }
+
+    .skill-panel-desc {
+      font-size: 12px;
+      color: #6a7a8a;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
     }
 
     .skill-panel-close {
@@ -326,18 +344,30 @@
     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;
+  .skill-switch {
+    :deep(.zxm-switch) {
+      min-width: 36px;
+      height: 20px;
+      background: rgba(63, 80, 106, 0.5);
 
-    &:hover {
-      opacity: 0.85;
-      transform: scale(1.02);
+      &::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);
+        }
+      }
     }
   }
 

+ 609 - 0
src/views/ventAI/manageAssistent/components/chatModal/SubAgentPanel.vue

@@ -0,0 +1,609 @@
+<template>
+  <div class="sub-agent-panel">
+    <div class="sub-agent-panel-header">
+      <div class="sub-agent-panel-header-left">
+        <span class="sub-agent-panel-title">子智能体</span>
+        <span class="sub-agent-panel-desc">管理模型子智能体,启用后可在聊天里使用</span>
+      </div>
+      <div class="sub-agent-panel-close" @click="emit('close')">
+        <div class="close-icon"></div>
+      </div>
+    </div>
+    <div class="sub-agent-filter-bar">
+      <a-input v-model:value="searchKeyword" placeholder="搜索子智能体..." allow-clear size="small" class="sub-agent-search-input" />
+      <a-select v-model:value="filterStatus" size="small" class="sub-agent-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="sub-agent-summary">
+      <span class="sub-agent-summary-label">子智能体</span>
+      <span class="sub-agent-summary-count">{{ filteredAgents.length }}项</span>
+      <a-button type="primary" class="sub-agent-create-btn" @click="handleCreate">
+        <template #icon><PlusOutlined /></template>
+        创建
+      </a-button>
+    </div>
+    <div class="sub-agent-list" v-if="!loading">
+      <div v-for="agent in filteredAgents" :key="agent.name" class="sub-agent-item">
+        <div class="sub-agent-header">
+          <div class="sub-agent-name-wrapper">
+            <div class="sub-agent-item-icon"></div>
+            <span class="sub-agent-name">{{ agent.name }}</span>
+          </div>
+          <div class="sub-agent-actions">
+            <a-switch :checked="agent.enabled" size="small" class="sub-agent-switch" @change="handleToggle(agent)" />
+            <div class="action-btn edit-btn" @click.stop="handleEdit(agent)" title="编辑"></div>
+            <div class="action-btn delete-btn" @click.stop="handleDelete(agent)" title="删除"></div>
+          </div>
+        </div>
+        <div class="sub-agent-desc">{{ agent.description }}</div>
+        <div class="sub-agent-detail-toggle" @click.stop="toggleExpand(agent.name)">
+          <span>{{ expandedNames.has(agent.name) ? '收起详情' : '查看详情' }}</span>
+          <div :class="['toggle-arrow', { expanded: expandedNames.has(agent.name) }]"></div>
+        </div>
+        <div v-if="expandedNames.has(agent.name)" class="sub-agent-detail">
+          <div v-if="agent.system_prompt" class="detail-section">
+            <div class="detail-label">系统提示词</div>
+            <div class="detail-prompt">{{ agent.system_prompt }}</div>
+          </div>
+          <div class="detail-section">
+            <div class="detail-label">工具</div>
+            <div class="detail-tools" v-if="agent.tool_infos && agent.tool_infos.length > 0">
+              <span v-for="tool in agent.tool_infos" :key="tool.name" :class="['tool-tag', { missing: !tool.exists }]">
+                {{ tool.display_name || tool.name }}
+              </span>
+            </div>
+            <div class="detail-tools-empty" v-else>继承主 Agent 全部工具</div>
+          </div>
+          <div class="detail-section" v-if="agent.model">
+            <div class="detail-label">模型</div>
+            <div class="detail-model">{{ agent.model }}</div>
+          </div>
+          <div class="detail-section" v-if="agent.created_at">
+            <div class="detail-label">创建时间</div>
+            <div class="detail-time">{{ agent.created_at }}</div>
+          </div>
+        </div>
+      </div>
+      <a-empty v-if="filteredAgents.length === 0" description="暂无子智能体" />
+    </div>
+    <div class="sub-agent-loading" v-else>
+      <a-spin />
+    </div>
+
+    <CreateSubAgentModal
+      :visible="showCreateModal"
+      :editData="editData"
+      @close="showCreateModal = false"
+      @created="handleCreatedOrUpdated"
+      @updated="handleCreatedOrUpdated"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { ref, computed } from 'vue';
+  import { PlusOutlined } from '@ant-design/icons-vue';
+  import { getSubAgents, deleteSubAgent, toggleSubAgent } from '../../api';
+  import { message, Modal } from 'ant-design-vue';
+  import CreateSubAgentModal from './CreateSubAgentModal.vue';
+
+  interface ToolInfo {
+    name: string;
+    display_name: string;
+    exists: boolean;
+  }
+
+  interface SubAgent {
+    name: string;
+    description: string;
+    system_prompt: string;
+    tools: string[];
+    tool_infos: ToolInfo[];
+    model: string;
+    enabled: boolean;
+    created_at: string;
+    updated_at: string;
+  }
+
+  const emit = defineEmits<{ (e: 'close'): void }>();
+
+  const agents = ref<SubAgent[]>([]);
+  const loading = ref(false);
+  const searchKeyword = ref('');
+  const filterStatus = ref<'all' | 'enabled' | 'disabled'>('all');
+  const showCreateModal = ref(false);
+  const editData = ref<SubAgent | null>(null);
+  const expandedNames = ref(new Set<string>());
+
+  const filteredAgents = computed(() => {
+    return agents.value.filter((agent) => {
+      const keyword = searchKeyword.value.trim().toLowerCase();
+      const matchKeyword = !keyword || agent.name.toLowerCase().includes(keyword) || agent.description.toLowerCase().includes(keyword);
+      const matchStatus = filterStatus.value === 'all' || (filterStatus.value === 'enabled' ? agent.enabled : !agent.enabled);
+      return matchKeyword && matchStatus;
+    });
+  });
+
+  const fetchSubAgents = async () => {
+    loading.value = true;
+    try {
+      const res = await getSubAgents();
+      agents.value = res?.subagents || res?.data || [];
+    } catch {
+      agents.value = [];
+    } finally {
+      loading.value = false;
+    }
+  };
+
+  const toggleExpand = (name: string) => {
+    if (expandedNames.value.has(name)) {
+      expandedNames.value.delete(name);
+    } else {
+      expandedNames.value.add(name);
+    }
+  };
+
+  const handleCreate = () => {
+    editData.value = null;
+    showCreateModal.value = true;
+  };
+
+  const handleEdit = (agent: SubAgent) => {
+    editData.value = agent;
+    showCreateModal.value = true;
+  };
+
+  const handleCreatedOrUpdated = async () => {
+    await fetchSubAgents();
+  };
+
+  const handleToggle = async (agent: SubAgent) => {
+    try {
+      await toggleSubAgent(agent.name);
+      agent.enabled = !agent.enabled;
+      message.success(`子智能体「${agent.name}」已${agent.enabled ? '启用' : '禁用'}`);
+    } catch {
+      message.error('操作失败,请重试');
+    }
+  };
+
+  const handleDelete = (agent: SubAgent) => {
+    Modal.confirm({
+      title: '提示',
+      content: `是否删除子智能体「${agent.name}」?`,
+      okText: '确认',
+      cancelText: '取消',
+      wrapClassName: 'delete-task-confirm-modal',
+      onOk: async () => {
+        try {
+          await deleteSubAgent(agent.name);
+          message.success(`子智能体「${agent.name}」已删除`);
+          expandedNames.value.delete(agent.name);
+          await fetchSubAgents();
+        } catch {
+          message.error('删除失败,请重试');
+        }
+      },
+    });
+  };
+
+  fetchSubAgents();
+</script>
+
+<style scoped lang="less">
+  .sub-agent-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;
+  }
+
+  .sub-agent-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;
+
+    .sub-agent-panel-header-left {
+      display: flex;
+      align-items: baseline;
+      gap: 10px;
+      min-width: 0;
+      flex: 1;
+    }
+
+    .sub-agent-panel-title {
+      font-size: 16px;
+      font-weight: 600;
+      color: #e8edf3;
+      letter-spacing: 1px;
+      flex-shrink: 0;
+    }
+
+    .sub-agent-panel-desc {
+      font-size: 12px;
+      color: #6a7a8a;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+    }
+
+    .sub-agent-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%;
+      }
+    }
+  }
+
+  .sub-agent-filter-bar {
+    display: flex;
+    gap: 12px;
+    margin: 16px 20px 0;
+    flex-shrink: 0;
+
+    .sub-agent-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;
+      }
+    }
+
+    .sub-agent-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;
+      }
+    }
+  }
+
+  .sub-agent-summary {
+    display: flex;
+    align-items: center;
+    gap: 6px;
+    padding: 12px 20px 0;
+    flex-shrink: 0;
+    font-size: 13px;
+
+    .sub-agent-summary-label {
+      color: #e8edf3;
+      font-weight: 500;
+    }
+
+    .sub-agent-summary-count {
+      color: #6a7a8a;
+    }
+
+    .sub-agent-create-btn {
+      flex-shrink: 0;
+      height: 32px;
+      border-radius: 6px;
+      font-size: 13px;
+      display: flex;
+      align-items: center;
+      gap: 4px;
+      margin-left: auto;
+    }
+  }
+
+  .sub-agent-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);
+      }
+    }
+  }
+
+  .sub-agent-loading {
+    flex: 1;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    padding: 80px 0;
+  }
+
+  .sub-agent-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);
+    transition: all 0.25s ease;
+
+    &:hover {
+      background: rgba(10, 132, 255, 0.08);
+      border-color: rgba(10, 132, 255, 0.25);
+    }
+
+    &:last-child {
+      margin-bottom: 0;
+    }
+  }
+
+  .sub-agent-header {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    margin-bottom: 10px;
+  }
+
+  .sub-agent-name-wrapper {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+    flex: 1;
+    min-width: 0;
+  }
+
+  .sub-agent-item-icon {
+    width: 24px;
+    height: 24px;
+    flex-shrink: 0;
+    background-image: var(--img-chat-options-sub-agent-icon);
+    background-repeat: no-repeat;
+    background-size: 100% 100%;
+    background-position: center;
+    filter: drop-shadow(0 0 4px rgba(10, 132, 255, 0.3));
+  }
+
+  .sub-agent-name {
+    font-size: 15px;
+    font-weight: 600;
+    color: #e8edf3;
+    letter-spacing: 0.3px;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+  }
+
+  .sub-agent-actions {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    flex-shrink: 0;
+  }
+
+  .sub-agent-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);
+        }
+      }
+    }
+  }
+
+  .action-btn {
+    width: 24px;
+    height: 24px;
+    cursor: pointer;
+    border-radius: 4px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    transition: background 0.2s;
+    background-repeat: no-repeat;
+    background-size: 14px 14px;
+    background-position: center;
+
+    &.edit-btn {
+      background-image: var(--img-chat-edit-icon);
+
+      &:hover {
+        background-color: rgba(10, 132, 255, 0.15);
+      }
+    }
+
+    &.delete-btn {
+      background-image: var(--img-chat-delete-icon);
+
+      &:hover {
+        background-color: rgba(255, 77, 79, 0.15);
+      }
+    }
+  }
+
+  .sub-agent-desc {
+    font-size: 13px;
+    color: #8b949e;
+    line-height: 1.7;
+    margin-bottom: 6px;
+    padding-left: 34px;
+  }
+
+  .sub-agent-detail-toggle {
+    display: flex;
+    align-items: center;
+    gap: 4px;
+    font-size: 12px;
+    color: #5cb8ff;
+    cursor: pointer;
+    padding-left: 34px;
+    width: fit-content;
+    transition: opacity 0.2s;
+
+    &: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);
+      }
+    }
+  }
+
+  .sub-agent-detail {
+    margin-top: 10px;
+    margin-left: 34px;
+    padding: 12px 14px;
+    background: rgba(0, 0, 0, 0.12);
+    border: 1px solid rgba(63, 80, 106, 0.15);
+    border-radius: 6px;
+    display: flex;
+    flex-direction: column;
+    gap: 10px;
+  }
+
+  .detail-section {
+    display: flex;
+    flex-direction: column;
+    gap: 4px;
+  }
+
+  .detail-label {
+    font-size: 11px;
+    color: #6a7a8a;
+    font-weight: 500;
+    text-transform: uppercase;
+    letter-spacing: 0.5px;
+  }
+
+  .detail-prompt {
+    font-size: 12px;
+    color: #c9d1d9;
+    line-height: 1.6;
+    white-space: pre-wrap;
+    word-break: break-word;
+    max-height: 120px;
+    overflow-y: auto;
+
+    &::-webkit-scrollbar {
+      width: 4px;
+    }
+
+    &::-webkit-scrollbar-thumb {
+      background: rgba(63, 80, 106, 0.4);
+      border-radius: 2px;
+    }
+  }
+
+  .detail-tools {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 4px;
+  }
+
+  .tool-tag {
+    display: inline-flex;
+    align-items: center;
+    padding: 2px 8px;
+    font-size: 11px;
+    color: #5cb8ff;
+    background: rgba(10, 132, 255, 0.1);
+    border: 1px solid rgba(10, 132, 255, 0.2);
+    border-radius: 4px;
+
+    &.missing {
+      color: #ff7875;
+      background: rgba(255, 77, 79, 0.08);
+      border-color: rgba(255, 77, 79, 0.2);
+    }
+  }
+
+  .detail-tools-empty {
+    font-size: 12px;
+    color: #6a7a8a;
+    font-style: italic;
+  }
+
+  .detail-model {
+    font-size: 12px;
+    color: #5cb8ff;
+    font-family: 'Courier New', monospace;
+  }
+
+  .detail-time {
+    font-size: 12px;
+    color: #8b949e;
+  }
+</style>

+ 9 - 1
src/views/ventAI/manageAssistent/components/chatModal/TaskListPanel.vue

@@ -69,9 +69,10 @@
     (e: 'pin-task', task: Task): void;
     (e: 'toggle-skill'): void;
     (e: 'toggle-schedules'): void;
+    (e: 'toggle-sub-agents'): void;
   }>();
 
-  type OptionType = 'add' | 'search' | 'skill' | 'schedules';
+  type OptionType = 'add' | 'search' | 'skill' | 'schedules' | 'sub-agents';
 
   const activeOption = ref<OptionType>('add');
   const searchKeyword = ref('');
@@ -102,6 +103,7 @@
     { type: 'add' as OptionType, label: '新建任务' },
     // { type: 'search' as OptionType, label: '搜索' },
     { type: 'skill' as OptionType, label: '技能' },
+    { type: 'sub-agents' as OptionType, label: '子智能体' },
     { type: 'schedules' as OptionType, label: '定时任务' },
   ];
 
@@ -121,6 +123,8 @@
       emit('create-task');
     } else if (type === 'skill') {
       emit('toggle-skill');
+    } else if (type === 'sub-agents') {
+      emit('toggle-sub-agents');
     } else if (type === 'schedules') {
       emit('toggle-schedules');
     } else {
@@ -364,6 +368,10 @@
           background-image: var(--img-chat-options-schedule-icon);
         }
 
+        &.options-sub-agents .options-icon {
+          background-image: var(--img-chat-options-sub-agent-icon);
+        }
+
         .zxm-btn {
           font-size: 14px;
           padding: 0;