Просмотр исходного кода

fix: 调整内容

Co-authored-by: Copilot <copilot@github.com>
jiaxing.liao недель назад: 4
Родитель
Сommit
0f78ae27e1

+ 0 - 0
.pnpm-store/v11/.pnpm-needs-build-marker


BIN
.pnpm-store/v11/index.db


+ 10 - 4
.vscode/settings.json

@@ -1,5 +1,6 @@
 {
   "editor.formatOnSave": true,
+  "editor.tabSize": 2,
   "[typescript]": {
     "editor.defaultFormatter": "esbenp.prettier-vscode"
   },
@@ -12,13 +13,18 @@
   "i18n-ally.localesPaths": [
     "apps/web/src/i18n/locales"
   ],
-  "i18n-ally.enabledParsers": ["ts", "json"],
+  "i18n-ally.enabledParsers": [
+    "ts",
+    "json"
+  ],
   "i18n-ally.displayLanguage": "zh-cn",
   "i18n-ally.sourceLanguage": "zh-cn",
   "i18n-ally.keystyle": "nested",
-  "i18n-ally.enabledFrameworks": ["vue"],
+  "i18n-ally.enabledFrameworks": [
+    "vue"
+  ],
   "[vue]": {
-    "editor.defaultFormatter": "esbenp.prettier-vscode"
+    "editor.defaultFormatter": "Vue.volar"
   },
   "[typescriptreact]": {
     "editor.defaultFormatter": "esbenp.prettier-vscode"
@@ -26,4 +32,4 @@
   "[javascriptreact]": {
     "editor.defaultFormatter": "esbenp.prettier-vscode"
   }
-}
+}

+ 4 - 0
apps/web/components.d.ts

@@ -22,6 +22,7 @@ declare module 'vue' {
     ElButton: typeof import('element-plus/es')['ElButton']
     ElCard: typeof import('element-plus/es')['ElCard']
     ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
+    ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
     ElCheckTag: typeof import('element-plus/es')['ElCheckTag']
     ElCol: typeof import('element-plus/es')['ElCol']
     ElCollapse: typeof import('element-plus/es')['ElCollapse']
@@ -57,6 +58,7 @@ declare module 'vue' {
     ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
     ElRow: typeof import('element-plus/es')['ElRow']
     ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
+    ElSegmented: typeof import('element-plus/es')['ElSegmented']
     ElSelect: typeof import('element-plus/es')['ElSelect']
     ElSlider: typeof import('element-plus/es')['ElSlider']
     ElSpace: typeof import('element-plus/es')['ElSpace']
@@ -98,6 +100,7 @@ declare global {
   const ElButton: typeof import('element-plus/es')['ElButton']
   const ElCard: typeof import('element-plus/es')['ElCard']
   const ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
+  const ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
   const ElCheckTag: typeof import('element-plus/es')['ElCheckTag']
   const ElCol: typeof import('element-plus/es')['ElCol']
   const ElCollapse: typeof import('element-plus/es')['ElCollapse']
@@ -133,6 +136,7 @@ declare global {
   const ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
   const ElRow: typeof import('element-plus/es')['ElRow']
   const ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
+  const ElSegmented: typeof import('element-plus/es')['ElSegmented']
   const ElSelect: typeof import('element-plus/es')['ElSelect']
   const ElSlider: typeof import('element-plus/es')['ElSlider']
   const ElSpace: typeof import('element-plus/es')['ElSpace']

+ 120 - 96
apps/web/src/components/Sidebar/index.vue

@@ -4,13 +4,10 @@
 			<div class="brand" v-if="!collapsed">
 				<img :src="logo" class="w-full" alt="logo" />
 			</div>
-			<div
-				class="top-icons"
-				:style="{
-					flexDirection: collapsed ? 'column' : 'row',
-					width: collapsed ? '100%' : 'auto'
-				}"
-			>
+			<div class="top-icons" :style="{
+				flexDirection: collapsed ? 'column' : 'row',
+				width: collapsed ? '100%' : 'auto'
+			}">
 				<el-dropdown placement="bottom-start" trigger="click">
 					<span style="cursor: pointer">
 						<SvgIcon name="Plus" />
@@ -68,7 +65,9 @@
 		<el-menu :default-active="activeMenu" router class="el-menu-vertical-demo main-menu">
 			<el-menu-item index="/">
 				<el-tooltip v-if="collapsed" :content="t('sidebar.menu.overview')" placement="right">
-					<span><SvgIcon name="home" /></span>
+					<span>
+						<SvgIcon name="home" />
+					</span>
 				</el-tooltip>
 				<SvgIcon v-else name="home" />
 				<span v-if="!collapsed" class="label">{{ t('sidebar.menu.overview') }}</span>
@@ -84,7 +83,9 @@
 
 			<el-menu-item index="/workflow">
 				<el-tooltip v-if="collapsed" :content="t('sidebar.menu.orchestration')" placement="right">
-					<span><SvgIcon name="workflow" /></span>
+					<span>
+						<SvgIcon name="workflow" />
+					</span>
 				</el-tooltip>
 				<SvgIcon v-else name="workflow" />
 				<span v-if="!collapsed" class="label">{{ t('sidebar.menu.orchestration') }}</span>
@@ -92,7 +93,9 @@
 
 			<el-menu-item index="/agent">
 				<el-tooltip v-if="collapsed" :content="t('sidebar.menu.management')" placement="right">
-					<span><SvgIcon name="platForm" /></span>
+					<span>
+						<SvgIcon name="platForm" />
+					</span>
 				</el-tooltip>
 				<SvgIcon v-else name="platForm" />
 				<span v-if="!collapsed" class="label">{{ t('sidebar.menu.management') }}</span>
@@ -100,7 +103,9 @@
 
 			<el-menu-item index="/knowledge">
 				<el-tooltip v-if="collapsed" :content="t('sidebar.menu.knowledge')" placement="right">
-					<span><SvgIcon name="book" /></span>
+					<span>
+						<SvgIcon name="book" />
+					</span>
 				</el-tooltip>
 				<SvgIcon v-else name="book" />
 				<span v-if="!collapsed" class="label">{{ t('sidebar.menu.knowledge') }}</span>
@@ -108,7 +113,9 @@
 
 			<el-menu-item index="/execution">
 				<el-tooltip v-if="collapsed" :content="t('sidebar.menu.execution')" placement="right">
-					<span><SvgIcon name="play" /></span>
+					<span>
+						<SvgIcon name="play" />
+					</span>
 				</el-tooltip>
 				<SvgIcon v-else name="play" />
 				<span v-if="!collapsed" class="label">{{ t('sidebar.menu.execution') }}</span>
@@ -116,7 +123,9 @@
 
 			<el-menu-item index="/chat">
 				<el-tooltip v-if="collapsed" :content="t('sidebar.menu.chat')" placement="right">
-					<span><SvgIcon name="chatMessage" /></span>
+					<span>
+						<SvgIcon name="chatMessage" />
+					</span>
 				</el-tooltip>
 				<SvgIcon v-else name="chatMessage" />
 				<span v-if="!collapsed" class="label">
@@ -128,85 +137,80 @@
 		<div class="spacer"></div>
 
 		<div class="bottom-menu">
-			<div
-				class="bottom-item"
-				:class="{ active: isBottomMenuActive('/models') }"
-				@click="$router.push('/models')"
-			>
-				<el-tooltip v-if="collapsed" content="模型管理" placement="right">
-					<span><SvgIcon name="model" /></span>
-				</el-tooltip>
-				<SvgIcon v-else name="model" />
-				<span v-if="!collapsed" class="label">模型管理</span>
-			</div>
 
-			<div
-				class="bottom-item"
-				:class="{ active: isBottomMenuActive('/ollama') }"
-				@click="$router.push('/ollama')"
-			>
+			<div class="bottom-item" :class="{ active: isBottomMenuActive('/ollama') }"
+				@click="$router.push('/ollama')">
 				<el-tooltip v-if="collapsed" content="Ollama" placement="right">
-					<span><SvgIcon name="ollama" /></span>
+					<span>
+						<SvgIcon name="ollama" />
+					</span>
 				</el-tooltip>
 				<SvgIcon v-else name="ollama" />
 				<span v-if="!collapsed" class="label">Ollama</span>
 			</div>
 
-			<div
-				class="bottom-item"
-				:class="{ active: isBottomMenuActive('/prompts') }"
-				@click="$router.push('/prompts')"
-			>
-				<el-tooltip v-if="collapsed" content="提示词模板" placement="right">
-					<span><SvgIcon name="prompts" /></span>
+			<div class="bottom-item" :class="{ active: isBottomMenuActive('/models') }"
+				@click="$router.push('/models')">
+				<el-tooltip v-if="collapsed" content="模型管理" placement="right">
+					<span>
+						<SvgIcon name="model" />
+					</span>
 				</el-tooltip>
-				<SvgIcon v-else name="prompts" />
-				<span v-if="!collapsed" class="label">提示词模板</span>
+				<SvgIcon v-else name="model" />
+				<span v-if="!collapsed" class="label">模型管理</span>
 			</div>
 
-			<div
-				class="bottom-item"
-				:class="{ active: isBottomMenuActive('/storage') }"
-				@click="$router.push('/storage')"
-			>
+			<div class="bottom-item" :class="{ active: isBottomMenuActive('/web-search') }"
+				@click="$router.push('/web-search')">
+				<el-tooltip v-if="collapsed" content="网络搜索" placement="right">
+					<span>
+						<SvgIcon name="websearch" />
+					</span>
+				</el-tooltip>
+				<SvgIcon v-else name="websearch" />
+				<span v-if="!collapsed" class="label">网络搜索</span>
+			</div>
+
+			<div class="bottom-item" :class="{ active: isBottomMenuActive('/storage') }"
+				@click="$router.push('/storage')">
 				<el-tooltip v-if="collapsed" content="存储引擎" placement="right">
-					<span><SvgIcon name="storage" /></span>
+					<span>
+						<SvgIcon name="storage" />
+					</span>
 				</el-tooltip>
 				<SvgIcon v-else name="storage" />
 				<span v-if="!collapsed" class="label">存储引擎</span>
 			</div>
 
-			<div
-				class="bottom-item"
-				:class="{ active: isBottomMenuActive('/web-search') }"
-				@click="$router.push('/web-search')"
-			>
-				<el-tooltip v-if="collapsed" content="网络搜索" placement="right">
-					<span><SvgIcon name="websearch" /></span>
-				</el-tooltip>
-				<SvgIcon v-else name="websearch" />
-				<span v-if="!collapsed" class="label">网络搜索</span>
-			</div>
 
-			<div
-				class="bottom-item"
-				:class="{ active: isBottomMenuActive('/mcp') }"
-				@click="$router.push('/mcp')"
-			>
+
+			<div class="bottom-item" :class="{ active: isBottomMenuActive('/mcp') }" @click="$router.push('/mcp')">
 				<el-tooltip v-if="collapsed" content="MCP服务" placement="right">
-					<span><SvgIcon name="mcp" /></span>
+					<span>
+						<SvgIcon name="mcp" />
+					</span>
 				</el-tooltip>
 				<SvgIcon v-else name="mcp" />
 				<span v-if="!collapsed" class="label">MCP服务</span>
 			</div>
 
-			<div
-				class="bottom-item"
-				:class="{ active: isBottomMenuActive('/skills') }"
-				@click="$router.push('/skills')"
-			>
+			<div class="bottom-item" :class="{ active: isBottomMenuActive('/prompts') }"
+				@click="$router.push('/prompts')">
+				<el-tooltip v-if="collapsed" content="提示词模板" placement="right">
+					<span>
+						<SvgIcon name="prompts" />
+					</span>
+				</el-tooltip>
+				<SvgIcon v-else name="prompts" />
+				<span v-if="!collapsed" class="label">提示词模板</span>
+			</div>
+
+			<div class="bottom-item" :class="{ active: isBottomMenuActive('/skills') }"
+				@click="$router.push('/skills')">
 				<el-tooltip v-if="collapsed" content="Skills技能" placement="right">
-					<span><SvgIcon name="skills" /></span>
+					<span>
+						<SvgIcon name="skills" />
+					</span>
 				</el-tooltip>
 				<SvgIcon v-else name="skills" />
 				<span v-if="!collapsed" class="label">Skills技能</span>
@@ -214,19 +218,20 @@
 
 			<div class="bottom-item" :class="{ active: showTemplateModal }" @click="handleTemplateClick">
 				<el-tooltip v-if="collapsed" :content="t('sidebar.menu.templates')" placement="right">
-					<span><SvgIcon name="box" /></span>
+					<span>
+						<SvgIcon name="box" />
+					</span>
 				</el-tooltip>
 				<SvgIcon v-else name="box" />
 				<span v-if="!collapsed" class="label">{{ t('sidebar.menu.templates') }}</span>
 			</div>
 
-			<div
-				class="bottom-item"
-				:class="{ active: router.currentRoute.value.path === '/statistics' }"
-				@click="$router.push('/statistics')"
-			>
+			<div class="bottom-item" :class="{ active: router.currentRoute.value.path === '/statistics' }"
+				@click="$router.push('/statistics')">
 				<el-tooltip v-if="collapsed" :content="t('sidebar.menu.statistics')" placement="right">
-					<span><SvgIcon name="line" /></span>
+					<span>
+						<SvgIcon name="line" />
+					</span>
 				</el-tooltip>
 				<SvgIcon v-else name="line" />
 				<span v-if="!collapsed" class="label">{{ t('sidebar.menu.statistics') }}</span>
@@ -234,7 +239,9 @@
 
 			<div class="bottom-item" @click="handleSettingsClick($event, 'help')">
 				<el-tooltip v-if="collapsed" :content="t('sidebar.menu.help')" placement="right">
-					<span><SvgIcon name="help" /></span>
+					<span>
+						<SvgIcon name="help" />
+					</span>
 				</el-tooltip>
 				<SvgIcon v-else name="help" />
 				<span v-if="!collapsed" class="label">{{ t('sidebar.menu.help') }}</span>
@@ -243,7 +250,9 @@
 
 			<div class="bottom-item" @click="handleSettingsClick($event, 'settings')">
 				<el-tooltip v-if="collapsed" :content="t('sidebar.menu.settings')" placement="right">
-					<span><SvgIcon name="setting" /></span>
+					<span>
+						<SvgIcon name="setting" />
+					</span>
 				</el-tooltip>
 				<SvgIcon v-else name="setting" />
 				<span v-if="!collapsed" class="label">{{ t('sidebar.menu.settings') }}</span>
@@ -253,24 +262,13 @@
 	</div>
 
 	<!-- 搜索对话框 -->
-	<SearchDialog
-		:is-open="showSearchDialog"
-		@close="showSearchDialog = false"
-		@select="handleSearchSelect"
-	/>
+	<SearchDialog :is-open="showSearchDialog" @close="showSearchDialog = false" @select="handleSearchSelect" />
 
 	<!-- 模板弹窗 -->
-	<TemplateModal
-		:visible="showTemplateModal"
-		@close="showTemplateModal = false"
-		@select="handleTemplateSelect"
-	/>
+	<TemplateModal :visible="showTemplateModal" @close="showTemplateModal = false" @select="handleTemplateSelect" />
 	<!-- 新建工作流弹窗 -->
-	<CreateWorkflowModal
-		:visible="createModalVisible"
-		@close="createModalVisible = false"
-		@success="handleCreateSuccess"
-	/>
+	<CreateWorkflowModal :visible="createModalVisible" @close="createModalVisible = false"
+		@success="handleCreateSuccess" />
 </template>
 
 <script setup lang="ts">
@@ -381,6 +379,7 @@ const toggleTheme = () => {
 * {
 	color: var(--text-secondary);
 }
+
 .sidebar {
 	width: 200px;
 	height: 100%;
@@ -392,9 +391,11 @@ const toggleTheme = () => {
 	transition: width 0.18s ease;
 	overflow: hidden;
 }
+
 .sidebar.collapsed {
 	width: 41px;
 }
+
 .top-bar {
 	display: flex;
 	align-items: center;
@@ -402,16 +403,19 @@ const toggleTheme = () => {
 	padding: 8px 10px;
 	border-bottom: 1px solid var(--border-light);
 }
+
 .brand {
 	display: flex;
 	justify-content: center;
 	align-items: center;
 }
+
 .top-icons {
 	display: flex;
 	gap: 10px;
 	align-items: center;
 }
+
 .top-icons svg {
 	cursor: pointer;
 	padding: 6px 8px;
@@ -419,22 +423,26 @@ const toggleTheme = () => {
 	border-radius: 4px;
 	transition: all 0.2s ease;
 }
+
 .top-icons svg:hover {
 	color: #810042;
 	background-color: var(--bg-container);
 }
-.top-icons > span {
+
+.top-icons>span {
 	display: flex;
 	align-items: center;
 	justify-content: center;
 }
-.top-icons > span svg {
+
+.top-icons>span svg {
 	padding: 6px 8px;
 	margin: -6px -8px;
 	border-radius: 4px;
 	transition: all 0.2s ease;
 }
-.top-icons > span:hover svg {
+
+.top-icons>span:hover svg {
 	color: #810042;
 	background-color: var(--bg-container);
 }
@@ -443,6 +451,7 @@ const toggleTheme = () => {
 	padding-top: 8px;
 	border-right: none;
 }
+
 .el-menu-vertical-demo .el-menu-item {
 	display: flex;
 	align-items: center;
@@ -450,26 +459,32 @@ const toggleTheme = () => {
 	padding: 0 16px;
 	height: 32px;
 }
+
 .el-menu-vertical-demo .el-menu-item:hover {
 	background: var(--bg-container);
 }
+
 .el-menu-vertical-demo .el-menu-item.is-active {
 	background: var(--bg-container) !important;
 	color: var(--el-color-primary) !important;
 	border-right: 3px solid var(--el-color-primary);
 }
+
 .label {
 	font-size: 13px;
 	flex: 1;
 }
+
 .beta {
 	font-size: 10px;
 	color: var(--text-tertiary);
 	margin-left: 6px;
 }
+
 .spacer {
 	flex: 1 1 auto;
 }
+
 .bottom-menu {
 	padding-bottom: 8px;
 	border-top: 1px solid var(--border-light);
@@ -477,6 +492,7 @@ const toggleTheme = () => {
 	flex-direction: column;
 	gap: 6px;
 }
+
 .bottom-item {
 	display: flex;
 	align-items: center;
@@ -489,10 +505,12 @@ const toggleTheme = () => {
 	border-radius: 4px;
 	transition: all 0.2s ease;
 }
+
 .bottom-item:hover {
 	background: var(--bg-container);
 	color: #810042;
 }
+
 .bottom-item.active {
 	background: var(--bg-container);
 	color: #810042;
@@ -533,23 +551,28 @@ const toggleTheme = () => {
 		}
 	}
 }
+
 .sidebar.collapsed .label {
 	display: none;
 }
+
 .sidebar.collapsed .el-menu-vertical-demo .el-menu-item,
 .sidebar.collapsed .bottom-item {
 	justify-content: center;
 	padding-left: 0;
 	padding-right: 0;
 }
+
 .sidebar.collapsed .top-icons {
 	gap: 10px;
 	flex-direction: column;
 }
-.sidebar.collapsed .top-icons > span {
+
+.sidebar.collapsed .top-icons>span {
 	width: 100%;
 	justify-content: center;
 }
+
 :deep(.el-button + .el-button) {
 	margin-left: 0;
 }
@@ -589,6 +612,7 @@ const toggleTheme = () => {
 		font-size: 11px;
 	}
 }
+
 :deep(.el-menu-item *) {
 	vertical-align: initial;
 }

Разница между файлами не показана из-за своего большого размера
+ 467 - 394
apps/web/src/views/agent/components/EditModal.vue


+ 239 - 0
apps/web/src/views/agent/components/param.md

@@ -0,0 +1,239 @@
+basic_config (基础配置)
+
+// 系统提示词,支持使用占位符
+// 通过/api/ai/prompt-template/config接口返回的system_prompts字段
+public String        system_prompt;
+
+// 上下文模板(仅 quick-answer 模式使用)
+// 通过/api/ai/prompt-template/config接口返回的context_templates字段
+public String        context_template;
+2.
+model_config (模型设置)
+
+// 对话模型 ID
+// 选择智能体使用的大语言模型
+@VerifyRequired
+public String    model_id;
+
+// 重排序模型 ID
+@VerifyRequired
+public String    rerank_model_id;
+
+// 温度
+// 控制输出的随机性,0 最确定,1 最随机
+// 温度参数(0-1)
+// 默认:0.7
+@VerifyRange(min_value = 0.00, max_value = 1.00)
+public Float     temperature;
+
+// 最大生成 token 数
+// 模型生成回复的最大Token数量
+// 默认:2048
+public Integer   max_completion_tokens;
+
+// 是否启用思考模式(适用于支持扩展思考的模型)
+public boolean   thinking;
+3.
+setting_config (Agent模式设置)
+
+// ReAct 最大迭代次数
+// Agent 执行任务时的最大推理步骤数
+// 默认:10
+@VerifyRange(min_value = 1, max_value = 50)
+public Integer       max_iterations;
+
+// LLM 调用超时
+// 单次 LLM 调用的最大等待时间(秒),超过此时间后调用将被中止
+// 0 表示无限等待(不推荐)
+// 默认:120
+@VerifyRange(min_value = 0)
+public Integer       llm_call_timeout;
+
+// 允许使用的工具列表
+// 通过使用/api/ai/agent/tools返回列表中项中的name字段
+public List<String>  allowed_tools;
+
+// MCP 服务选择模式:all/selected/none
+@VerifyEnum(items = {WeknoraConst.SELECT_MODE_ALL, WeknoraConst.SELECT_MODE_SELECTED, WeknoraConst.SELECT_MODE_NONE})
+public String        mcp_selection_mode;
+
+// 选中的 MCP 服务 ID 列表
+public List<String>  mcp_services;
+
+// Skills 选择模式:all/selected/none
+@VerifyEnum(items = {WeknoraConst.SELECT_MODE_ALL, WeknoraConst.SELECT_MODE_SELECTED, WeknoraConst.SELECT_MODE_NONE})
+public String        skills_selection_mode;
+
+// 选中的 Skill 名称列表(mode 为 selected 时)
+// 通过访问/api/ai/skills/list接口,获取返回列表中项的name字段
+public List<String>  selected_skills;
+4.
+kb_config (知识库设置)
+
+// 知识库选择模式:all/selected/none
+@VerifyEnum(items = {WeknoraConst.SELECT_MODE_ALL, WeknoraConst.SELECT_MODE_SELECTED, WeknoraConst.SELECT_MODE_NONE})
+public String        kb_selection_mode;
+
+// 关联的知识库 ID 列表
+public List<String>  knowledge_bases;
+
+// 仅在用户通过 @ 显式提及时才检索知识库
+public boolean       retrieve_kb_only_when_mentioned;
+
+// 支持的文件类型(["pdf", "docx", "txt", "md", "csv", "xlsx", "jpg"])
+// 限制可选择的文件类型,留空表示支持所有类型
+@VerifyEnum(items = {
+        WeknoraConst.FILE_TYPE_PDF,
+        WeknoraConst.FILE_TYPE_DOCX,
+        WeknoraConst.FILE_TYPE_TXT,
+        WeknoraConst.FILE_TYPE_MD,
+        WeknoraConst.FILE_TYPE_CSV,
+        WeknoraConst.FILE_TYPE_XLSX,
+        WeknoraConst.FILE_TYPE_JPG
+})
+public List<String>  supported_file_types;
+5.
+img_vlm_config (图片上传/多模态设置)
+
+// 是否允许上传图片
+// 默认:false
+public boolean   image_upload_enabled;
+
+// 图片分析所用的 VLM 模型 ID
+public String    vlm_model_id;
+
+// 图片存储提供者:local/minio/cos/tos/s3/oss,为空使用全局默认
+@VerifyEnum(items = {
+        WeknoraConst.STORAGE_PROVIDER_TYPE_LOCAL,
+        WeknoraConst.STORAGE_PROVIDER_TYPE_MINIO,
+        WeknoraConst.STORAGE_PROVIDER_TYPE_COS,
+        WeknoraConst.STORAGE_PROVIDER_TYPE_TOS, 
+        WeknoraConst.STORAGE_PROVIDER_TYPE_S3,
+        WeknoraConst.STORAGE_PROVIDER_TYPE_OSS
+})
+public String    image_storage_provider;
+6.
+faq_config (FAQ策略设置)
+
+// 启用 FAQ 优先
+// FAQ 答案将优先于普通文档被引用,提高回答准确性
+// 默认:true
+public boolean    faq_priority_enabled;
+
+// FAQ 直接回答阈值
+// 当问题与 FAQ 相似度超过此值时,直接使用 FAQ 答案 [0-1]
+// 默认:0.9
+@VerifyRange(min_value = 0.00, max_value = 1.00)
+public Float      faq_direct_answer_threshold;
+
+// FAQ 分数加成系数
+// FAQ 结果的相关性分数乘以此系数,使其排序更靠前 [1-2]
+// 默认:1.2
+@VerifyRange(min_value = 1.00, max_value = 2.00)
+public Float      faq_score_boost;
+7.
+web_search_config (网络搜索设置)
+
+// 是否启用网络搜索
+// 启用后智能体可以搜索互联网获取信息
+// 默认:true
+public boolean    web_search_enabled;
+
+// 网络搜索最大结果数
+// 每次搜索返回的最大结果数量 [1-10]
+// 默认:5
+@VerifyRange(min_value =1, max_value = 10)
+public Integer    web_search_max_results;
+
+// 网络搜索提供者 ID,为空使用租户默认提供者
+public String     web_search_provider_id;
+
+// 是否自动获取重排后的搜索结果页面全文
+// 默认:false
+public boolean    web_fetch_enabled;
+
+// 重排后获取全文的最大页面数
+// 默认:3
+public Integer    web_fetch_top_n;
+8.
+multiple_config (多轮对话设置)
+
+// 是否启用多轮对话
+// 开启后将保留历史对话上下文
+// 默认:true
+public boolean   multi_turn_enabled;
+
+// 保留的历史轮次数
+// 保留最近几轮对话作为上下文 [1-20]
+// 默认:5
+@VerifyRange(min_value =1, max_value = 20)
+public Integer   history_turns;
+9.
+search_config (检索策略设置)
+
+// 向量检索 TopK
+// 向量检索返回的最大结果数量 [1-50]
+// 默认:10
+@VerifyRange(min_value =1, max_value = 50)
+public Integer   embedding_top_k;
+
+// 关键词检索阈值
+// 关键词检索的最低相关性分数 [0-1]
+// 默认:0.3
+@VerifyRange(min_value =0, max_value = 1)
+public Float     keyword_threshold;
+
+// 向量检索阈值
+// 向量检索的最低相似度分数 [0-1]
+// 默认:0.5
+@VerifyRange(min_value =0, max_value = 1)
+public Float     vector_threshold;
+
+// 重排序 TopK
+// 重排序后保留的最大结果数量 [1-20]
+// 默认:5
+@VerifyRange(min_value =1, max_value = 20)
+public Integer   rerank_top_k;
+
+// 重排序阈值
+// 重排序的最低相关性分数 [-10-10]
+// 默认:0.5
+@VerifyRange(min_value = -10, max_value = 10)
+public Float     rerank_threshold;
+10.
+advanced_config (高级设置)
+
+// 是否启用查询扩展
+// 自动扩展查询词以提高召回率
+// 默认:true
+public boolean   enable_query_expansion;
+
+// 是否启用多轮对话查询改写
+// 多轮对话时自动改写用户问题,消解指代和补全省略
+// 默认:true
+public boolean   enable_rewrite;
+
+// 改写系统提示词
+// 用于问题改写的系统提示词(留空使用默认)
+// 通过访问/api/ai/prompt-template/config接口返回的rewrites列表项中的content字段
+public String    rewrite_prompt_system;
+
+// 改写用户提示词模板
+// 用于问题改写的用户提示词模板(留空使用默认)
+// 通过访问/api/ai/prompt-template/config接口返回的rewrites列表项中的user字段
+public String    rewrite_prompt_user;
+
+// 回退策略:fixed(固定回复)或 model(模型生成);未设置时在服务端默认为 model
+// 当无法从知识库找到相关内容时的处理方式
+// 默认:model
+@VerifyEnum(items = {WeknoraConst.FALLBACK_STRATEGY_FIXED, WeknoraConst.FALLBACK_STRATEGY_MODEL})
+public String    fallback_strategy;
+
+// 固定回退回复(fallback_strategy 为 fixed 时使用)
+// 当无法回答时返回的固定文本
+public String    fallback_response;
+
+// 回退提示词(fallback_strategy 为 model 时使用)
+// 当无法从知识库找到答案时,引导模型生成回复的提示词
+// 通过访问/api/ai/prompt-template/config接口返回的fall_backs列表项中的content字段
+public String    fallback_prompt;

+ 373 - 202
apps/web/src/views/model/ModelManage.vue

@@ -1,91 +1,99 @@
 <template>
 	<div class="model-manage">
-		<div class="action-bar">
-			<div class="action-bar__left">
-				<el-button type="primary" @click="openCreateModel">
-					<el-icon><Plus /></el-icon> 新建模型
-				</el-button>
-				<el-button @click="getAllModelList()">
-					<el-icon><Refresh /></el-icon> 刷新
-				</el-button>
-			</div>
-			<div class="action-bar__right">
-				<el-input
-					v-model="searchForm.keyword"
-					clearable
-					placeholder="关键词搜索模型名称/标识"
-					style="width: 240px"
-					@keyup.enter="handleSearch"
-				/>
-				<el-select
-					v-model="searchForm.type"
-					clearable
-					placeholder="模型类型"
-					style="width: 160px"
-					@change="handleSearch"
-				>
-					<el-option label="对话模型" value="KnowledgeQA" />
-					<el-option label="Embedding模型" value="Embedding" />
-					<el-option label="Rerank模型" value="Rerank" />
-					<el-option label="视觉模型" value="VLLM" />
-				</el-select>
-				<el-select
-					v-model="searchForm.source"
-					clearable
-					placeholder="来源"
-					style="width: 140px"
-					@change="handleSearch"
-				>
-					<el-option label="本地" value="local" />
-					<el-option label="服务商" value="remote" />
-				</el-select>
-				<el-button type="primary" @click="handleSearch">查询</el-button>
-				<el-button @click="handleResetSearch">重置</el-button>
+		<el-card class="list-card">
+			<div class="action-bar mb-12px">
+				<div class="action-bar__left">
+					<el-button type="primary" @click="openCreateModel">
+						<el-icon>
+							<Plus />
+						</el-icon> 新建模型
+					</el-button>
+					<el-button @click="getAllModelList()">
+						<el-icon>
+							<Refresh />
+						</el-icon> 刷新
+					</el-button>
+				</div>
+				<div class="action-bar__right">
+					<el-input v-model="searchForm.keyword" clearable placeholder="关键词搜索模型名称/标识" class="search-input"
+						@keyup.enter="handleSearch" />
+					<el-select v-model="searchForm.type" clearable placeholder="模型类型" style="width: 160px" @change="handleSearch">
+						<el-option label="对话模型" value="KnowledgeQA" />
+						<el-option label="Embedding模型" value="Embedding" />
+						<el-option label="Rerank模型" value="Rerank" />
+						<el-option label="视觉模型" value="VLLM" />
+					</el-select>
+					<el-select v-model="searchForm.source" clearable placeholder="来源" style="width: 140px" @change="handleSearch">
+						<el-option label="本地" value="local" />
+						<el-option label="服务商" value="remote" />
+					</el-select>
+					<el-button type="primary" @click="handleSearch">查询</el-button>
+					<el-button @click="handleResetSearch">重置</el-button>
+				</div>
 			</div>
-		</div>
 
-		<el-card class="list-card">
-			<template #header>
-				<div class="card-header">
-					<span>模型列表</span>
+			<div v-loading="modelLoading" class="grid">
+				<el-empty v-if="!allModels.length && !modelLoading" description="暂无配置的模型" class="empty" />
+
+				<div v-for="row in allModels" :key="row.id" class="card" @click="openDetailModel(row.id)">
+					<div class="card-head">
+						<div class="card-head__content">
+							<div class="card-head__top">
+								<div class="title-block">
+									<div class="title">{{ row.title || row.name || '未命名模型' }}</div>
+									<div class="subtitle">{{ row.name || '未设置模型标识' }}</div>
+								</div>
+								<div class="actions" @click.stop>
+									<el-dropdown>
+										<span class="actions-trigger">
+											<el-icon>
+												<MoreFilled />
+											</el-icon>
+										</span>
+										<template #dropdown>
+											<el-dropdown-menu>
+												<el-dropdown-item @click="openDetailModel(row.id)">详情</el-dropdown-item>
+												<el-dropdown-item @click="openEditModel(row.id)">编辑</el-dropdown-item>
+												<el-dropdown-item @click="deleteModelConfirm(row.id)" divided>
+													<span class="danger-text">删除</span>
+												</el-dropdown-item>
+											</el-dropdown-menu>
+										</template>
+									</el-dropdown>
+								</div>
+							</div>
+
+							<div class="badge-row">
+								<span class="badge">{{ getModelTypeName(row.type) }}</span>
+								<span class="badge subtle">{{ formatModelSource(row.source) }}</span>
+							</div>
+						</div>
+					</div>
+
+					<div class="desc">{{ row.description || '暂无描述' }}</div>
+
+					<div class="tags">
+						<el-tag v-if="row.is_default" type="success" effect="light">默认</el-tag>
+						<el-tag :type="getModelStatusType(row.status)" effect="light">
+							{{ formatModelStatus(row.status) }}
+						</el-tag>
+						<el-tag v-if="row.provider" type="info" effect="light">
+							{{ row.provider }}
+						</el-tag>
+					</div>
+
+					<div class="card-footer" @click.stop>
+						<el-button link type="primary" @click="openDetailModel(row.id)">详情</el-button>
+						<el-button link type="primary" @click="openEditModel(row.id)">编辑</el-button>
+						<el-button link type="danger" @click="deleteModelConfirm(row.id)">删除</el-button>
+					</div>
 				</div>
-			</template>
-			<el-table :data="allModels" v-loading="modelLoading" border>
-				<el-table-column prop="name" label="模型标识" />
-				<el-table-column prop="title" label="模型名称" width="200" />
-				<el-table-column prop="type" label="模型类型" width="120" />
-				<el-table-column prop="source" label="来源" width="100">
-					<template #default="{ row }">
-						<span>{{ row.source === 'local' ? '本地模型' : '服务商' }}</span>
-					</template>
-				</el-table-column>
-				<el-table-column prop="provider" label="服务商" width="150" />
-				<el-table-column prop="status" label="状态" width="120">
-					<template #default="{ row }">
-						<el-tag v-if="row.status === 'active'" type="success">可用</el-tag>
-						<el-tag v-else type="warning">{{ row.status }}</el-tag>
-					</template>
-				</el-table-column>
-				<el-table-column label="操作" width="220" fixed="right">
-					<template #default="{ row }">
-						<el-button type="primary" plain link @click="openDetailModel(row.id)">详情</el-button>
-						<el-button type="primary" plain link @click="openEditModel(row.id)">编辑</el-button>
-						<el-button type="danger" plain link @click="deleteModelConfirm(row.id)">删除</el-button>
-					</template>
-				</el-table-column>
-				<template #empty>暂无配置的模型</template>
-			</el-table>
+			</div>
+
 			<div class="pagination-wrap">
-				<el-pagination
-					v-model:current-page="pagination.pageIndex"
-					v-model:page-size="pagination.pageSize"
-					background
-					layout="total, sizes, prev, pager, next, jumper"
-					:total="pagination.total"
-					:page-sizes="[10, 20, 50, 100]"
-					@current-change="handlePageChange"
-					@size-change="handleSizeChange"
-				/>
+				<el-pagination v-model:current-page="pagination.pageIndex" v-model:page-size="pagination.pageSize" background
+					layout="total, sizes, prev, pager, next, jumper" :total="pagination.total" :page-sizes="[10, 20, 50, 100]"
+					@current-change="handlePageChange" @size-change="handleSizeChange" />
 			</div>
 		</el-card>
 
@@ -147,21 +155,11 @@
 				</el-descriptions-item>
 				<el-descriptions-item v-if="currentDetailModel.source === 'remote'" label="连接测试">
 					<div class="model-check-box">
-						<el-button
-							type="primary"
-							plain
-							:loading="detailCheckLoading"
-							@click="checkCurrentDetailModel"
-						>
+						<el-button type="primary" plain :loading="detailCheckLoading" @click="checkCurrentDetailModel">
 							测试连接
 						</el-button>
-						<el-alert
-							v-if="detailCheckResult.message"
-							:type="detailCheckResult.success ? 'success' : 'error'"
-							:title="detailCheckResult.message"
-							:closable="false"
-							show-icon
-						/>
+						<el-alert v-if="detailCheckResult.message" :type="detailCheckResult.success ? 'success' : 'error'"
+							:title="detailCheckResult.message" :closable="false" show-icon />
 					</div>
 				</el-descriptions-item>
 				<el-descriptions-item label="创建时间">{{
@@ -176,19 +174,8 @@
 			</template>
 		</el-dialog>
 
-		<el-drawer
-			v-model="showModelDialog"
-			direction="rtl"
-			:title="currentModelId ? '编辑模型' : '新建模型'"
-			width="700px"
-		>
-			<el-form
-				ref="modelFormRef"
-				:model="modelForm"
-				:rules="modelRules"
-				label-width="120px"
-				label-position="top"
-			>
+		<el-drawer v-model="showModelDialog" direction="rtl" :title="currentModelId ? '编辑模型' : '新建模型'" width="700px">
+			<el-form ref="modelFormRef" :model="modelForm" :rules="modelRules" label-width="120px" label-position="top">
 				<el-form-item label="模型来源" prop="source">
 					<el-radio-group v-model="modelForm.source" @change="handleSourceChange">
 						<el-radio label="local">本地Ollama</el-radio>
@@ -197,11 +184,7 @@
 				</el-form-item>
 
 				<el-form-item v-if="modelForm.source === 'remote'" label="模型类型" prop="type">
-					<el-select
-						v-model="modelForm.type"
-						placeholder="请选择模型类型"
-						@change="handleTypeChange"
-					>
+					<el-select v-model="modelForm.type" placeholder="请选择模型类型" @change="handleTypeChange">
 						<el-option label="对话模型" value="KnowledgeQA" />
 						<el-option label="Embedding模型" value="Embedding" />
 						<el-option label="Rerank模型" value="Rerank" />
@@ -211,32 +194,20 @@
 
 				<el-form-item v-if="modelForm.source === 'local'" label="本地模型" prop="name">
 					<el-select v-model="modelForm.name" placeholder="请选择" style="width: 100%">
-						<el-option
-							v-for="model in availableLocalModels"
-							:key="model.name"
-							:label="model.name"
-							:value="model.name"
-						/>
+						<el-option v-for="model in availableLocalModels" :key="model.name" :label="model.name"
+							:value="model.name" />
 					</el-select>
 				</el-form-item>
 
 				<el-form-item v-if="modelForm.source === 'remote'" label="服务商" prop="provider">
-					<el-select
-						v-model="modelForm.provider"
-						placeholder="请选择服务商"
-						@change="handleProviderChange"
-						style="width: 100%"
-					>
+					<el-select v-model="modelForm.provider" placeholder="请选择服务商" @change="handleProviderChange"
+						style="width: 100%">
 						<el-option v-for="p in providers" :key="p.value" :label="p.label" :value="p.value" />
 					</el-select>
 				</el-form-item>
 
 				<el-form-item label="模型名称" prop="name">
-					<el-input
-						v-model="modelForm.name"
-						:readonly="modelForm.source === 'local'"
-						placeholder="如:llama3.1"
-					/>
+					<el-input v-model="modelForm.name" :readonly="modelForm.source === 'local'" placeholder="如:llama3.1" />
 				</el-form-item>
 
 				<el-form-item label="显示名称" prop="title">
@@ -244,62 +215,31 @@
 				</el-form-item>
 
 				<el-form-item label="描述" prop="description">
-					<el-input
-						v-model="modelForm.description"
-						type="textarea"
-						rows="2"
-						placeholder="模型描述..."
-					/>
+					<el-input v-model="modelForm.description" type="textarea" rows="2" placeholder="模型描述..." />
 				</el-form-item>
 
 				<el-form-item v-if="modelForm.source === 'remote'" label="API地址" prop="base_url">
-					<el-autocomplete
-						v-model="modelForm.base_url"
-						:fetch-suggestions="queryBaseUrlSuggestions"
-						clearable
-						placeholder="可从默认地址选择,或手动输入"
-						style="width: 100%"
-					/>
+					<el-autocomplete v-model="modelForm.base_url" :fetch-suggestions="queryBaseUrlSuggestions" clearable
+						placeholder="可从默认地址选择,或手动输入" style="width: 100%" />
 				</el-form-item>
 
 				<el-form-item v-if="modelForm.source === 'remote'" label="API Key" prop="api_key">
 					<el-input v-model="modelForm.api_key" type="password" placeholder="输入你的API密钥" />
 				</el-form-item>
-				<el-form-item
-					v-if="modelForm.source === 'remote' && modelForm.type === 'Embedding'"
-					label="维度"
-					prop="dimension"
-				>
-					<el-input-number
-						v-model="modelForm.dimension"
-						:min="1"
-						:step="1"
-						controls-position="right"
-						style="width: 100%"
-						placeholder="请输入向量维度"
-					/>
+				<el-form-item v-if="modelForm.source === 'remote' && modelForm.type === 'Embedding'" label="维度"
+					prop="dimension">
+					<el-input-number v-model="modelForm.dimension" :min="1" :step="1" controls-position="right"
+						style="width: 100%" placeholder="请输入向量维度" />
 				</el-form-item>
 
-				<el-form-item
-					v-if="modelForm.source === 'remote' && modelForm.type === 'Embedding'"
-					label="截断Tokens"
-					prop="truncate_prompt_tokens"
-				>
-					<el-input-number
-						v-model="modelForm.truncate_prompt_tokens"
-						:min="1"
-						:step="1"
-						controls-position="right"
-						style="width: 100%"
-						placeholder="请输入截断 token 数"
-					/>
+				<el-form-item v-if="modelForm.source === 'remote' && modelForm.type === 'Embedding'" label="截断Tokens"
+					prop="truncate_prompt_tokens">
+					<el-input-number v-model="modelForm.truncate_prompt_tokens" :min="1" :step="1" controls-position="right"
+						style="width: 100%" placeholder="请输入截断 token 数" />
 				</el-form-item>
 
-				<el-form-item
-					v-if="modelForm.source === 'remote' && modelForm.type === 'KnowledgeQA'"
-					label="支持视觉"
-					prop="supports_vision"
-				>
+				<el-form-item v-if="modelForm.source === 'remote' && modelForm.type === 'KnowledgeQA'" label="支持视觉"
+					prop="supports_vision">
 					<el-switch v-model="modelForm.supports_vision" />
 				</el-form-item>
 
@@ -308,26 +248,20 @@
 						<div class="header-config__top">
 							<span>自定义请求头(可选)</span>
 							<el-button type="primary" link @click="addCustomHeader">
-								<el-icon><Plus /></el-icon>添加请求头
+								<el-icon>
+									<Plus />
+								</el-icon>添加请求头
 							</el-button>
 						</div>
 						<div class="header-config__desc">
 							调用远程模型API时附加的HTTP请求头,常用于鉴权、链路追踪等场景
 						</div>
 						<div class="header-config__rows">
-							<div
-								v-for="(item, index) in customHeaderList"
-								:key="index"
-								class="header-config__row"
-							>
+							<div v-for="(item, index) in customHeaderList" :key="index" class="header-config__row">
 								<el-input v-model="item.key" placeholder="Header名称" />
 								<el-input v-model="item.value" placeholder="Header值" />
-								<el-button
-									link
-									type="danger"
-									:disabled="customHeaderList.length === 1"
-									@click="removeCustomHeader(index)"
-								>
+								<el-button link type="danger" :disabled="customHeaderList.length === 1"
+									@click="removeCustomHeader(index)">
 									删除
 								</el-button>
 							</div>
@@ -336,21 +270,11 @@
 				</el-form-item>
 				<el-form-item>
 					<div v-if="modelForm.source === 'remote'" class="model-check-box">
-						<el-button
-							type="primary"
-							plain
-							:loading="formCheckLoading"
-							@click="checkModelFormConnection"
-						>
+						<el-button type="primary" plain :loading="formCheckLoading" @click="checkModelFormConnection">
 							测试连接
 						</el-button>
-						<el-alert
-							v-if="formCheckResult.message"
-							:type="formCheckResult.success ? 'success' : 'error'"
-							:title="formCheckResult.message"
-							:closable="false"
-							show-icon
-						/>
+						<el-alert v-if="formCheckResult.message" :type="formCheckResult.success ? 'success' : 'error'"
+							:title="formCheckResult.message" :closable="false" show-icon />
 					</div>
 				</el-form-item>
 			</el-form>
@@ -365,7 +289,7 @@
 <script setup lang="ts">
 import { computed, ref, reactive, onMounted } from 'vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
-import { Plus, Refresh } from '@element-plus/icons-vue'
+import { Plus, Refresh, MoreFilled } from '@element-plus/icons-vue'
 import { aiModel, ollama } from '@repo/api-service'
 import type {
 	ModelItem,
@@ -520,6 +444,22 @@ function getModelTypeName(type?: string) {
 	return typeLabelMap[type] || type
 }
 
+function formatModelSource(source?: string) {
+	return source === 'local' ? '本地模型' : '服务商'
+}
+
+function formatModelStatus(status?: string) {
+	if (!status) return '未知状态'
+	if (status === 'active') return '可用'
+	return status
+}
+
+function getModelStatusType(status?: string) {
+	if (status === 'active') return 'success'
+	if (!status) return 'info'
+	return 'warning'
+}
+
 function resetDetailCheckResult() {
 	detailCheckResult.success = false
 	detailCheckResult.message = ''
@@ -908,14 +848,15 @@ onMounted(() => {
 .model-manage {
 	width: 100%;
 }
+
 .action-bar {
-	margin-bottom: 16px;
 	display: flex;
 	align-items: center;
 	justify-content: space-between;
 	flex-wrap: wrap;
 	gap: 12px;
 }
+
 .action-bar__left,
 .action-bar__right {
 	display: flex;
@@ -923,21 +864,220 @@ onMounted(() => {
 	gap: 12px;
 	flex-wrap: wrap;
 }
+
 .list-card {
-	border-radius: 8px;
+	border-radius: 18px;
 	margin-bottom: 16px;
 }
+
+.search-input {
+	width: 240px;
+}
+
+.toolbar-meta {
+	display: flex;
+	align-items: center;
+	gap: 10px;
+	flex-wrap: wrap;
+	margin-bottom: 16px;
+}
+
+.pill {
+	padding: 8px 12px;
+	border-radius: 999px;
+	border: 1px solid #e5e7eb;
+	background: #f8fafc;
+	font-size: 12px;
+	color: #64748b;
+}
+
 .pagination-wrap {
 	display: flex;
 	justify-content: flex-end;
 	margin-top: 16px;
 }
+
+.grid {
+	display: grid;
+	grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+	gap: 16px;
+	min-height: 240px;
+}
+
+.card {
+	overflow: hidden;
+	border-radius: 20px;
+	border: 1px solid #e5e7eb;
+	background: #fff;
+	box-shadow: 0 12px 28px rgba(15, 23, 42, 0.06);
+	cursor: pointer;
+	padding: 16px;
+	display: flex;
+	flex-direction: column;
+	gap: 14px;
+	transition:
+		transform 0.22s ease,
+		box-shadow 0.22s ease,
+		border-color 0.22s ease;
+}
+
+.card:hover {
+	transform: translateY(-3px);
+	border-color: #d8dee9;
+	box-shadow: 0 18px 36px rgba(15, 23, 42, 0.1);
+}
+
+.card-head {
+	padding: 14px 16px;
+	border-radius: 18px;
+	background:
+		radial-gradient(circle at top left, rgba(15, 23, 42, 0.06), transparent 38%),
+		linear-gradient(135deg, #f8fafc, #eef2ff 58%, #fff7ed);
+}
+
+.card-head__content {
+	min-width: 0;
+}
+
+.card-head__top {
+	display: flex;
+	align-items: flex-start;
+	justify-content: space-between;
+	gap: 12px;
+}
+
+.title-block {
+	min-width: 0;
+}
+
+.title {
+	font-size: 18px;
+	font-weight: 700;
+	line-height: 1.25;
+	color: #0f172a;
+	word-break: break-word;
+}
+
+.subtitle {
+	margin-top: 6px;
+	font-size: 13px;
+	color: #64748b;
+	word-break: break-all;
+}
+
+.badge-row {
+	margin-top: 10px;
+	display: flex;
+	flex-wrap: wrap;
+	gap: 8px;
+}
+
+.badge {
+	padding: 5px 10px;
+	border-radius: 999px;
+	background: rgba(255, 255, 255, 0.92);
+	font-size: 12px;
+	color: #334155;
+	border: 1px solid rgba(203, 213, 225, 0.9);
+}
+
+.badge.subtle {
+	background: rgba(15, 23, 42, 0.82);
+	color: #f8fafc;
+	border-color: transparent;
+}
+
+.desc {
+	min-height: 44px;
+	color: #64748b;
+	font-size: 14px;
+	line-height: 1.65;
+	display: -webkit-box;
+	-webkit-line-clamp: 2;
+	-webkit-box-orient: vertical;
+	overflow: hidden;
+}
+
+.tags {
+	display: flex;
+	flex-wrap: wrap;
+	gap: 8px;
+}
+
+.meta-list {
+	display: grid;
+	grid-template-columns: repeat(3, minmax(0, 1fr));
+	gap: 10px;
+}
+
+.meta-item {
+	padding: 10px 12px;
+	border-radius: 14px;
+	background: #f8fafc;
+	border: 1px solid #e2e8f0;
+	display: flex;
+	flex-direction: column;
+	gap: 4px;
+	min-width: 0;
+}
+
+.meta-label {
+	font-size: 12px;
+	color: #94a3b8;
+}
+
+.meta-value {
+	font-size: 13px;
+	color: #334155;
+	word-break: break-word;
+}
+
+.card-footer {
+	display: flex;
+	justify-content: flex-end;
+	gap: 6px;
+	margin-top: auto;
+}
+
+.actions {
+	display: flex;
+	justify-content: flex-end;
+}
+
+.actions-trigger {
+	width: 30px;
+	height: 30px;
+	border-radius: 999px;
+	display: grid;
+	place-items: center;
+	color: #475569;
+	background: rgba(255, 255, 255, 0.8);
+	transition:
+		background 0.2s ease,
+		color 0.2s ease;
+}
+
+.actions-trigger:hover {
+	background: rgba(15, 23, 42, 0.08);
+	color: #0f172a;
+}
+
+.danger-text {
+	color: var(--el-color-danger);
+}
+
+.empty {
+	grid-column: 1 / -1;
+	padding: 24px 0;
+}
+
 .card-header {
 	display: flex;
 	align-items: center;
 	justify-content: space-between;
 	font-weight: 600;
 }
+
 pre {
 	margin: 0;
 	padding: 8px;
@@ -945,11 +1085,13 @@ pre {
 	border-radius: 4px;
 	font-size: 12px;
 }
+
 .provider-grid {
 	display: grid;
 	grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
 	gap: 12px;
 }
+
 .provider-card {
 	position: relative;
 	padding: 16px;
@@ -957,10 +1099,12 @@ pre {
 	border: 1px solid #d9dbe1;
 	transition: all 0.2s ease;
 }
+
 .provider-card:hover {
 	transform: translateY(-2px);
 	box-shadow: 0 10px 24px rgba(18, 28, 45, 0.12);
 }
+
 .provider-card__top {
 	display: flex;
 	align-items: center;
@@ -968,12 +1112,14 @@ pre {
 	gap: 10px;
 	margin-bottom: 8px;
 }
+
 .provider-card__label {
 	font-size: 18px;
 	font-weight: 700;
 	line-height: 1.15;
 	color: #1f2937;
 }
+
 .provider-card__desc {
 	font-size: 18px;
 	line-height: 1.5;
@@ -981,63 +1127,88 @@ pre {
 	min-height: 72px;
 	margin-bottom: 12px;
 }
+
 .provider-card__tags {
 	display: flex;
 	flex-wrap: wrap;
 	gap: 8px;
 }
+
 .provider-card__action {
 	opacity: 0;
 	pointer-events: none;
 	transition: opacity 0.2s ease;
 }
+
 .provider-card:hover .provider-card__action {
 	opacity: 1;
 	pointer-events: auto;
 }
+
 .provider-card--tone-1 {
 	background: #f2f4f8;
 }
+
 .provider-card--tone-2 {
 	background: #eef2f7;
 }
+
 .provider-card--tone-3 {
 	background: #f4f6fb;
 }
+
 .provider-card--tone-4 {
 	background: #edf3f8;
 }
+
 .provider-card--tone-5 {
 	background: #f3f5f9;
 }
+
 .header-config {
 	width: 100%;
 }
+
 .header-config__top {
 	display: flex;
 	align-items: center;
 	justify-content: space-between;
 }
+
 .header-config__desc {
 	color: #909399;
 	font-size: 12px;
 	margin: 4px 0 10px;
 }
+
 .header-config__rows {
 	display: flex;
 	flex-direction: column;
 	gap: 8px;
 }
+
 .header-config__row {
 	display: grid;
 	grid-template-columns: 1fr 1fr auto;
 	gap: 8px;
 	align-items: center;
 }
+
 .model-check-box {
 	width: 100%;
 	display: flex;
 	flex-direction: column;
 	gap: 8px;
 }
+
+@media (max-width: 768px) {
+	.search-input {
+		width: 100%;
+	}
+
+	.meta-list,
+	.header-config__row {
+		grid-template-columns: 1fr;
+	}
+}
 </style>

+ 2 - 0
apps/web/src/views/model/types.d.ts

@@ -38,7 +38,9 @@ export interface ModelItem {
 	source: string
 	provider: string
 	is_default: boolean
+	status?: string
 	creationTime: string
+	updateTime?: string
 }
 
 export interface ModelCreateForm {

+ 209 - 102
apps/web/src/views/resource/components/McpPanel.vue

@@ -2,24 +2,14 @@
 	<div class="panel">
 		<div class="toolbar">
 			<div class="toolbar-left">
-				<el-input
-					v-model="keyword"
-					clearable
-					placeholder="搜索 MCP 名称"
-					class="search-input"
-					@keyup.enter="loadList(1)"
-				>
+				<el-input v-model="keyword" clearable placeholder="搜索 MCP 名称" class="search-input" @keyup.enter="loadList(1)">
 					<template #prefix>
-						<el-icon><Search /></el-icon>
+						<el-icon>
+							<Search />
+						</el-icon>
 					</template>
 				</el-input>
-				<el-select
-					v-model="transportType"
-					clearable
-					placeholder="传输类型"
-					style="width: 160px"
-					@change="loadList(1)"
-				>
+				<el-select v-model="transportType" clearable placeholder="传输类型" style="width: 160px" @change="loadList(1)">
 					<el-option label="stdio" value="stdio" />
 					<el-option label="sse" value="sse" />
 					<el-option label="streamable" value="streamable" />
@@ -30,12 +20,20 @@
 			</div>
 			<div class="toolbar-right">
 				<el-button type="primary" @click="openCreate">
-					<el-icon><Plus /></el-icon>
+					<el-icon>
+						<Plus />
+					</el-icon>
 					新建 MCP
 				</el-button>
 			</div>
 		</div>
 
+		<div class="toolbar-meta">
+			<span class="pill">共 {{ pagination.totalCount }} 个服务</span>
+			<span class="pill">已启用 {{ enabledCount }} 个</span>
+			<span class="pill">{{ transportCount }} 种传输类型</span>
+		</div>
+
 		<!-- <div class="summary-grid">
 			<div class="summary-card">
 				<div class="summary-value">{{ pagination.totalCount }}</div>
@@ -51,50 +49,40 @@
 			</div>
 		</div> -->
 
-		<div v-loading="loading" class="table-wrap">
+		<div v-loading="loading" class="grid">
 			<el-empty v-if="!list.length && !loading" description="暂无 MCP 服务" />
-			<el-table v-else :data="list" border>
-				<el-table-column prop="name" label="名称" min-width="180" />
-				<el-table-column prop="transport_type" label="传输类型" width="120" />
-				<el-table-column prop="url" label="地址" min-width="240" />
-				<el-table-column label="启用" width="90">
-					<template #default="{ row }">
-						<el-tag :type="row.enabled ? 'success' : 'info'" effect="light">
-							{{ row.enabled ? '启用' : '禁用' }}
-						</el-tag>
-					</template>
-				</el-table-column>
-				<el-table-column label="操作" width="320" fixed="right">
-					<template #default="{ row }">
-						<el-button link type="primary" @click="checkItem(row.id)">测试</el-button>
-						<!-- <el-button link type="primary" @click="openResources(row.id)">资源</el-button> -->
-						<el-button link type="primary" @click="openTools(row.id)">工具</el-button>
-						<el-button link type="primary" @click="openEditById(row.id)">编辑</el-button>
-						<el-button link type="danger" @click="removeItem(row.id)">删除</el-button>
-					</template>
-				</el-table-column>
-			</el-table>
+			<div v-for="row in list" :key="row.id" class="card">
+				<div class="card-head">
+					<div class="card-head__top">
+						<div class="title-block">
+							<div class="title">{{ row.name || '未命名 MCP' }}</div>
+							<div class="subtitle">{{ row.url || '未配置地址' }}</div>
+						</div>
+					</div>
+					<div class="badge-row">
+						<span class="badge">{{ row.transport_type || '未设置传输类型' }}</span>
+						<span class="badge subtle">{{ row.enabled ? '启用中' : '已禁用' }}</span>
+					</div>
+				</div>
+
+				<div class="desc">{{ row.description || '暂无描述' }}</div>
+
+				<div class="card-footer">
+					<el-button link type="primary" @click="checkItem(row.id)">测试</el-button>
+					<el-button link type="primary" @click="openTools(row.id)">工具</el-button>
+					<el-button link type="primary" @click="openEditById(row.id)">编辑</el-button>
+					<el-button link type="danger" @click="removeItem(row.id)">删除</el-button>
+				</div>
+			</div>
 		</div>
 
 		<div class="pagination">
-			<el-pagination
-				v-model:current-page="pagination.pageIndex"
-				v-model:page-size="pagination.pageSize"
-				background
-				layout="total, sizes, prev, pager, next, jumper"
-				:page-sizes="[10, 20, 50, 100]"
-				:total="pagination.totalCount"
-				@current-change="handlePageChange"
-				@size-change="handleSizeChange"
-			/>
+			<el-pagination v-model:current-page="pagination.pageIndex" v-model:page-size="pagination.pageSize" background
+				layout="total, sizes, prev, pager, next, jumper" :page-sizes="[10, 20, 50, 100]" :total="pagination.totalCount"
+				@current-change="handlePageChange" @size-change="handleSizeChange" />
 		</div>
 
-		<el-drawer
-			v-model="drawerVisible"
-			:title="currentId ? '编辑 MCP' : '新建 MCP'"
-			direction="rtl"
-			size="760px"
-		>
+		<el-drawer v-model="drawerVisible" :title="currentId ? '编辑 MCP' : '新建 MCP'" direction="rtl" size="760px">
 			<el-form ref="formRef" :model="form" :rules="rules" label-position="top">
 				<el-form-item label="名称" prop="name">
 					<el-input v-model="form.name" />
@@ -105,10 +93,8 @@
 				<div class="grid-2">
 					<el-form-item label="传输类型" prop="transport_type">
 						<el-select v-model="form.transport_type" style="width: 100%">
-							<el-option label="stdio" value="stdio" />
-							<el-option label="sse" value="sse" />
-							<el-option label="streamable" value="streamable" />
-							<el-option label="http" value="http" />
+							<el-option label="SSE(Server-Sent Events)" value="sse" />
+							<el-option label="HTTP Streamable" value="http-streamable" />
 						</el-select>
 					</el-form-item>
 					<el-form-item label="启用" prop="enabled">
@@ -123,18 +109,10 @@
 				</el-form-item>
 				<div class="grid-2">
 					<el-form-item label="重试次数" prop="advanced_config.retry_count">
-						<el-input-number
-							v-model="form.advanced_config.retry_count"
-							:min="0"
-							style="width: 100%"
-						/>
+						<el-input-number v-model="form.advanced_config.retry_count" :min="0" style="width: 100%" />
 					</el-form-item>
 					<el-form-item label="重试间隔(秒)" prop="advanced_config.retry_delay">
-						<el-input-number
-							v-model="form.advanced_config.retry_delay"
-							:min="0"
-							style="width: 100%"
-						/>
+						<el-input-number v-model="form.advanced_config.retry_delay" :min="0" style="width: 100%" />
 					</el-form-item>
 				</div>
 				<el-form-item label="Headers">
@@ -142,23 +120,16 @@
 						<div class="kv-config__top">
 							<span>请求头</span>
 							<el-button type="primary" link @click="addHeaderRow">
-								<el-icon><Plus /></el-icon>添加
+								<el-icon>
+									<Plus />
+								</el-icon>添加
 							</el-button>
 						</div>
 						<div class="kv-config__rows">
-							<div
-								v-for="(item, index) in headerList"
-								:key="`header-${index}`"
-								class="kv-config__row"
-							>
+							<div v-for="(item, index) in headerList" :key="`header-${index}`" class="kv-config__row">
 								<el-input v-model="item.key" placeholder="Header 名称" />
 								<el-input v-model="item.value" placeholder="Header 值" />
-								<el-button
-									link
-									type="danger"
-									:disabled="headerList.length === 1"
-									@click="removeHeaderRow(index)"
-								>
+								<el-button link type="danger" :disabled="headerList.length === 1" @click="removeHeaderRow(index)">
 									删除
 								</el-button>
 							</div>
@@ -170,19 +141,16 @@
 						<div class="kv-config__top">
 							<span>鉴权配置</span>
 							<el-button type="primary" link @click="addAuthRow">
-								<el-icon><Plus /></el-icon>添加
+								<el-icon>
+									<Plus />
+								</el-icon>添加
 							</el-button>
 						</div>
 						<div class="kv-config__rows">
 							<div v-for="(item, index) in authList" :key="`auth-${index}`" class="kv-config__row">
 								<el-input v-model="item.key" placeholder="配置项名称" />
 								<el-input v-model="item.value" placeholder="配置项值" />
-								<el-button
-									link
-									type="danger"
-									:disabled="authList.length === 1"
-									@click="removeAuthRow(index)"
-								>
+								<el-button link type="danger" :disabled="authList.length === 1" @click="removeAuthRow(index)">
 									删除
 								</el-button>
 							</div>
@@ -194,19 +162,16 @@
 						<div class="kv-config__top">
 							<span>环境变量</span>
 							<el-button type="primary" link @click="addEnvRow">
-								<el-icon><Plus /></el-icon>添加
+								<el-icon>
+									<Plus />
+								</el-icon>添加
 							</el-button>
 						</div>
 						<div class="kv-config__rows">
 							<div v-for="(item, index) in envList" :key="`env-${index}`" class="kv-config__row">
 								<el-input v-model="item.key" placeholder="变量名" />
 								<el-input v-model="item.value" placeholder="变量值" />
-								<el-button
-									link
-									type="danger"
-									:disabled="envList.length === 1"
-									@click="removeEnvRow(index)"
-								>
+								<el-button link type="danger" :disabled="envList.length === 1" @click="removeEnvRow(index)">
 									删除
 								</el-button>
 							</div>
@@ -218,13 +183,8 @@
 						<el-button :loading="checkLoading" :disabled="!currentId" @click="checkItemByForm">
 							测试连接
 						</el-button>
-						<el-alert
-							v-if="checkMessage"
-							:title="checkMessage"
-							:type="checkSuccess ? 'success' : 'error'"
-							:closable="false"
-							show-icon
-						/>
+						<el-alert v-if="checkMessage" :title="checkMessage" :type="checkSuccess ? 'success' : 'error'"
+							:closable="false" show-icon />
 					</div>
 				</el-form-item>
 			</el-form>
@@ -609,6 +569,22 @@ onMounted(() => {
 	width: 280px;
 }
 
+.toolbar-meta {
+	display: flex;
+	align-items: center;
+	gap: 10px;
+	flex-wrap: wrap;
+}
+
+.pill {
+	padding: 8px 12px;
+	border-radius: 999px;
+	border: 1px solid #e5e7eb;
+	background: #f8fafc;
+	font-size: 12px;
+	color: #64748b;
+}
+
 .summary-grid {
 	display: grid;
 	grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
@@ -634,10 +610,140 @@ onMounted(() => {
 	color: var(--text-secondary);
 }
 
-.table-wrap {
+.grid {
+	display: grid;
+	grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+	gap: 16px;
 	min-height: 240px;
 }
 
+.card {
+	overflow: hidden;
+	border-radius: 20px;
+	border: 1px solid #e5e7eb;
+	background: #fff;
+	box-shadow: 0 12px 28px rgba(15, 23, 42, 0.06);
+	padding: 16px;
+	display: flex;
+	flex-direction: column;
+	gap: 14px;
+	transition:
+		transform 0.22s ease,
+		box-shadow 0.22s ease,
+		border-color 0.22s ease;
+}
+
+.card:hover {
+	transform: translateY(-3px);
+	border-color: #d8dee9;
+	box-shadow: 0 18px 36px rgba(15, 23, 42, 0.1);
+}
+
+.card-head {
+	padding: 14px 16px;
+	border-radius: 18px;
+	background:
+		radial-gradient(circle at top left, rgba(15, 23, 42, 0.06), transparent 38%),
+		linear-gradient(135deg, #f8fafc, #eef2ff 58%, #fff7ed);
+}
+
+.card-head__top {
+	display: flex;
+	align-items: flex-start;
+	justify-content: space-between;
+	gap: 12px;
+}
+
+.title {
+	font-size: 18px;
+	font-weight: 700;
+	line-height: 1.25;
+	color: #0f172a;
+	word-break: break-word;
+}
+
+.subtitle {
+	margin-top: 6px;
+	font-size: 13px;
+	color: #64748b;
+	word-break: break-all;
+}
+
+.badge-row {
+	margin-top: 10px;
+	display: flex;
+	flex-wrap: wrap;
+	gap: 8px;
+}
+
+.badge {
+	padding: 5px 10px;
+	border-radius: 999px;
+	background: rgba(255, 255, 255, 0.92);
+	font-size: 12px;
+	color: #334155;
+	border: 1px solid rgba(203, 213, 225, 0.9);
+}
+
+.badge.subtle {
+	background: rgba(15, 23, 42, 0.82);
+	color: #f8fafc;
+	border-color: transparent;
+}
+
+.desc {
+	min-height: 44px;
+	color: #64748b;
+	font-size: 14px;
+	line-height: 1.65;
+	display: -webkit-box;
+	-webkit-line-clamp: 2;
+	-webkit-box-orient: vertical;
+	overflow: hidden;
+}
+
+.tags {
+	display: flex;
+	flex-wrap: wrap;
+	gap: 8px;
+}
+
+.meta-list {
+	display: grid;
+	grid-template-columns: repeat(2, minmax(0, 1fr));
+	gap: 10px;
+}
+
+.meta-item {
+	padding: 10px 12px;
+	border-radius: 14px;
+	background: #f8fafc;
+	border: 1px solid #e2e8f0;
+	display: flex;
+	flex-direction: column;
+	gap: 4px;
+	min-width: 0;
+}
+
+.meta-label {
+	font-size: 12px;
+	color: #94a3b8;
+}
+
+.meta-value {
+	font-size: 13px;
+	color: #334155;
+	word-break: break-word;
+}
+
+.card-footer {
+	display: flex;
+	justify-content: flex-end;
+	gap: 6px;
+	margin-top: auto;
+	flex-wrap: wrap;
+}
+
 .pagination {
 	display: flex;
 	justify-content: flex-end;
@@ -701,6 +807,7 @@ onMounted(() => {
 	}
 
 	.grid-2,
+	.meta-list,
 	.kv-config__row {
 		grid-template-columns: 1fr;
 	}

+ 326 - 263
apps/web/src/views/resource/components/StorageManager.vue

@@ -1,214 +1,205 @@
 <template>
 	<div class="storage-manager" v-loading="pageLoading">
-		<el-alert
-			title="选择引擎查看配置,支持修改保存和连通测试。"
-			type="info"
-			:closable="false"
-			show-icon
-		/>
+		<el-alert title="以平铺卡片形式查看引擎配置,点击编辑后在弹窗中修改并保存。" type="info" :closable="false" show-icon />
 
 		<div class="storage-actions">
 			<el-button type="primary" :loading="initLoading" @click="handleInit">初始化存储</el-button>
 			<el-button :loading="engineLoading" @click="loadEngines">刷新列表</el-button>
 		</div>
 
-		<div class="storage-layout">
-			<aside class="engine-list">
-				<div class="engine-list__header">引擎列表</div>
-				<div v-if="engines.length" class="engine-list__body">
-					<div
-						v-for="item in engines"
-						:key="item.name"
-						class="engine-card"
-						:class="{ 'engine-card--active': selectedProvider === item.name }"
-						@click="selectProvider(item.name)"
-					>
-						<div class="engine-card__top">
-							<div class="engine-card__name">{{ formatProviderLabel(item.name) }}</div>
-						</div>
-						<div class="engine-card__meta">
-							<el-tag size="small" :type="item.allowed ? 'success' : 'info'" effect="plain">
-								{{ item.allowed ? '允许' : '不允许' }}
-							</el-tag>
-							<el-tag size="small" :type="item.available ? 'primary' : 'warning'" effect="plain">
-								{{ item.available ? '可用' : '不可用' }}
-							</el-tag>
-						</div>
-						<div class="engine-card__desc">
-							{{ item.description || '点击加载配置信息' }}
+		<div v-if="engines.length" class="grid">
+			<div v-for="item in engines" :key="item.name" class="card">
+				<div class="card-head">
+					<div class="card-head__top">
+						<div class="title-block">
+							<div class="title">{{ formatProviderLabel(item.name) }}</div>
+							<div class="subtitle">{{ item.description || '支持对象存储配置与连通测试' }}</div>
 						</div>
 					</div>
 				</div>
-				<el-empty v-else description="暂无引擎" />
-			</aside>
-
-			<section class="config-panel">
-				<template v-if="selectedProvider">
-					<div class="config-panel__header">
-						<div>
-							<div class="config-panel__title">{{ formatProviderLabel(selectedProvider) }} 配置</div>
-							<div class="config-panel__desc">点击左侧引擎后加载配置信息,修改后保存。</div>
-						</div>
-					</div>
 
-					<el-form :model="form" label-position="top">
-						<template v-if="selectedProvider === 'local'">
-							<el-form-item label="存储前缀">
-								<el-input v-model="form.local.path_prefix" placeholder="例如 knowledge/files" />
-							</el-form-item>
-						</template>
-
-						<template v-else-if="selectedProvider === 'minio'">
-							<div class="form-grid">
-								<el-form-item label="Endpoint">
-									<el-input v-model="form.minio.endpoint" placeholder="127.0.0.1:9000" />
-								</el-form-item>
-								<el-form-item label="Mode">
-									<el-input v-model="form.minio.mode" placeholder="docker" />
-								</el-form-item>
-								<el-form-item label="Access Key ID">
-									<el-input v-model="form.minio.access_key_id" />
-								</el-form-item>
-								<el-form-item label="Secret Access Key">
-									<el-input v-model="form.minio.secret_access_key" show-password />
-								</el-form-item>
-								<el-form-item label="Bucket">
-									<el-input v-model="form.minio.bucket_name" />
-								</el-form-item>
-								<el-form-item label="Path Prefix">
-									<el-input v-model="form.minio.path_prefix" />
-								</el-form-item>
-							</div>
-							<el-form-item label="Use SSL">
-								<el-switch v-model="form.minio.use_ssl" />
-							</el-form-item>
-						</template>
-
-						<template v-else-if="selectedProvider === 'cos'">
-							<div class="form-grid">
-								<el-form-item label="App ID">
-									<el-input v-model="form.cos.app_id" />
-								</el-form-item>
-								<el-form-item label="Region">
-									<el-input v-model="form.cos.region" />
-								</el-form-item>
-								<el-form-item label="Secret ID">
-									<el-input v-model="form.cos.secret_id" />
-								</el-form-item>
-								<el-form-item label="Secret Key">
-									<el-input v-model="form.cos.secret_key" show-password />
-								</el-form-item>
-								<el-form-item label="Bucket">
-									<el-input v-model="form.cos.bucket_name" />
-								</el-form-item>
-								<el-form-item label="Path Prefix">
-									<el-input v-model="form.cos.path_prefix" />
-								</el-form-item>
-							</div>
-						</template>
-
-						<template v-else-if="selectedProvider === 'tos'">
-							<div class="form-grid">
-								<el-form-item label="Endpoint">
-									<el-input v-model="form.tos.endpoint" />
-								</el-form-item>
-								<el-form-item label="Region">
-									<el-input v-model="form.tos.region" />
-								</el-form-item>
-								<el-form-item label="Access Key">
-									<el-input v-model="form.tos.access_key" />
-								</el-form-item>
-								<el-form-item label="Secret Key">
-									<el-input v-model="form.tos.secret_key" show-password />
-								</el-form-item>
-								<el-form-item label="Bucket">
-									<el-input v-model="form.tos.bucket_name" />
-								</el-form-item>
-								<el-form-item label="Path Prefix">
-									<el-input v-model="form.tos.path_prefix" />
-								</el-form-item>
-							</div>
-						</template>
-
-						<template v-else-if="selectedProvider === 's3'">
-							<div class="form-grid">
-								<el-form-item label="Endpoint">
-									<el-input v-model="form.s3.endpoint" />
-								</el-form-item>
-								<el-form-item label="Region">
-									<el-input v-model="form.s3.region" />
-								</el-form-item>
-								<el-form-item label="Access Key">
-									<el-input v-model="form.s3.access_key" />
-								</el-form-item>
-								<el-form-item label="Secret Key">
-									<el-input v-model="form.s3.secret_key" show-password />
-								</el-form-item>
-								<el-form-item label="Bucket">
-									<el-input v-model="form.s3.bucket_name" />
-								</el-form-item>
-								<el-form-item label="Path Prefix">
-									<el-input v-model="form.s3.path_prefix" />
-								</el-form-item>
-							</div>
-						</template>
-
-						<template v-else-if="selectedProvider === 'oss'">
-							<div class="form-grid">
-								<el-form-item label="Endpoint">
-									<el-input v-model="form.oss.endpoint" />
-								</el-form-item>
-								<el-form-item label="Region">
-									<el-input v-model="form.oss.region" />
-								</el-form-item>
-								<el-form-item label="Access Key">
-									<el-input v-model="form.oss.access_key" />
-								</el-form-item>
-								<el-form-item label="Secret Key">
-									<el-input v-model="form.oss.secret_key" show-password />
-								</el-form-item>
-								<el-form-item label="Bucket">
-									<el-input v-model="form.oss.bucket_name" />
-								</el-form-item>
-								<el-form-item label="Path Prefix">
-									<el-input v-model="form.oss.path_prefix" />
-								</el-form-item>
-								<el-form-item label="Temp Bucket">
-									<el-input v-model="form.oss.temp_bucket_name" />
-								</el-form-item>
-								<el-form-item label="Temp Region">
-									<el-input v-model="form.oss.temp_region" />
-								</el-form-item>
-							</div>
-							<el-form-item label="Use Temp Bucket">
-								<el-switch v-model="form.oss.use_temp_bucket" />
-							</el-form-item>
-						</template>
-					</el-form>
-
-					<div class="config-panel__footer">
-						<el-button
-							link
-							type="primary"
-							:loading="testingProvider === selectedProvider"
-							@click="testProvider(selectedProvider)"
-						>
-							连通测试
-						</el-button>
-						<el-button @click="reloadSelectedProvider" :loading="providerLoading === selectedProvider">
-							重新加载
-						</el-button>
-						<el-button type="primary" :loading="saving" @click="saveConfig">保存配置</el-button>
-					</div>
-				</template>
-				<div v-else class="config-panel__empty">请先从左侧选择一个引擎。</div>
-			</section>
+				<div class="tags">
+					<el-tag :type="item.allowed ? 'success' : 'info'" effect="light">
+						{{ item.allowed ? '允许' : '不允许' }}
+					</el-tag>
+					<el-tag :type="item.available ? 'primary' : 'warning'" effect="light">
+						{{ item.available ? '可用' : '不可用' }}
+					</el-tag>
+				</div>
+
+				<div class="card-footer">
+					<el-button link type="primary" :loading="providerLoading === item.name" @click="openEditProvider(item.name)">
+						编辑
+					</el-button>
+				</div>
+			</div>
 		</div>
+		<el-empty v-else description="暂无引擎" />
+
+		<el-drawer v-model="drawerVisible"
+			:title="selectedProvider ? `${formatProviderLabel(selectedProvider)} 配置` : '编辑存储引擎'" direction="rtl" size="760px">
+			<template v-if="selectedProvider">
+				<div class="drawer-intro">
+					<div class="drawer-intro__title">{{ formatProviderLabel(selectedProvider) }}</div>
+					<div class="drawer-intro__desc">修改当前存储引擎配置后保存,可直接在此执行重新加载与连通测试。</div>
+				</div>
+
+				<el-form :model="form" label-position="top">
+					<template v-if="selectedProvider === 'local'">
+						<el-form-item label="存储前缀">
+							<el-input v-model="form.local.path_prefix" placeholder="例如 knowledge/files" />
+						</el-form-item>
+					</template>
+
+					<template v-else-if="selectedProvider === 'minio'">
+						<div class="form-grid">
+							<el-form-item label="Endpoint">
+								<el-input v-model="form.minio.endpoint" placeholder="127.0.0.1:9000" />
+							</el-form-item>
+							<el-form-item label="Mode">
+								<el-input v-model="form.minio.mode" placeholder="docker" />
+							</el-form-item>
+							<el-form-item label="Access Key ID">
+								<el-input v-model="form.minio.access_key_id" />
+							</el-form-item>
+							<el-form-item label="Secret Access Key">
+								<el-input v-model="form.minio.secret_access_key" show-password />
+							</el-form-item>
+							<el-form-item label="Bucket">
+								<el-input v-model="form.minio.bucket_name" />
+							</el-form-item>
+							<el-form-item label="Path Prefix">
+								<el-input v-model="form.minio.path_prefix" />
+							</el-form-item>
+						</div>
+						<el-form-item label="Use SSL">
+							<el-switch v-model="form.minio.use_ssl" />
+						</el-form-item>
+					</template>
+
+					<template v-else-if="selectedProvider === 'cos'">
+						<div class="form-grid">
+							<el-form-item label="App ID">
+								<el-input v-model="form.cos.app_id" />
+							</el-form-item>
+							<el-form-item label="Region">
+								<el-input v-model="form.cos.region" />
+							</el-form-item>
+							<el-form-item label="Secret ID">
+								<el-input v-model="form.cos.secret_id" />
+							</el-form-item>
+							<el-form-item label="Secret Key">
+								<el-input v-model="form.cos.secret_key" show-password />
+							</el-form-item>
+							<el-form-item label="Bucket">
+								<el-input v-model="form.cos.bucket_name" />
+							</el-form-item>
+							<el-form-item label="Path Prefix">
+								<el-input v-model="form.cos.path_prefix" />
+							</el-form-item>
+						</div>
+					</template>
+
+					<template v-else-if="selectedProvider === 'tos'">
+						<div class="form-grid">
+							<el-form-item label="Endpoint">
+								<el-input v-model="form.tos.endpoint" />
+							</el-form-item>
+							<el-form-item label="Region">
+								<el-input v-model="form.tos.region" />
+							</el-form-item>
+							<el-form-item label="Access Key">
+								<el-input v-model="form.tos.access_key" />
+							</el-form-item>
+							<el-form-item label="Secret Key">
+								<el-input v-model="form.tos.secret_key" show-password />
+							</el-form-item>
+							<el-form-item label="Bucket">
+								<el-input v-model="form.tos.bucket_name" />
+							</el-form-item>
+							<el-form-item label="Path Prefix">
+								<el-input v-model="form.tos.path_prefix" />
+							</el-form-item>
+						</div>
+					</template>
+
+					<template v-else-if="selectedProvider === 's3'">
+						<div class="form-grid">
+							<el-form-item label="Endpoint">
+								<el-input v-model="form.s3.endpoint" />
+							</el-form-item>
+							<el-form-item label="Region">
+								<el-input v-model="form.s3.region" />
+							</el-form-item>
+							<el-form-item label="Access Key">
+								<el-input v-model="form.s3.access_key" />
+							</el-form-item>
+							<el-form-item label="Secret Key">
+								<el-input v-model="form.s3.secret_key" show-password />
+							</el-form-item>
+							<el-form-item label="Bucket">
+								<el-input v-model="form.s3.bucket_name" />
+							</el-form-item>
+							<el-form-item label="Path Prefix">
+								<el-input v-model="form.s3.path_prefix" />
+							</el-form-item>
+						</div>
+					</template>
+
+					<template v-else-if="selectedProvider === 'oss'">
+						<div class="form-grid">
+							<el-form-item label="Endpoint">
+								<el-input v-model="form.oss.endpoint" />
+							</el-form-item>
+							<el-form-item label="Region">
+								<el-input v-model="form.oss.region" />
+							</el-form-item>
+							<el-form-item label="Access Key">
+								<el-input v-model="form.oss.access_key" />
+							</el-form-item>
+							<el-form-item label="Secret Key">
+								<el-input v-model="form.oss.secret_key" show-password />
+							</el-form-item>
+							<el-form-item label="Bucket">
+								<el-input v-model="form.oss.bucket_name" />
+							</el-form-item>
+							<el-form-item label="Path Prefix">
+								<el-input v-model="form.oss.path_prefix" />
+							</el-form-item>
+							<el-form-item label="Temp Bucket">
+								<el-input v-model="form.oss.temp_bucket_name" />
+							</el-form-item>
+							<el-form-item label="Temp Region">
+								<el-input v-model="form.oss.temp_region" />
+							</el-form-item>
+						</div>
+						<el-form-item label="Use Temp Bucket">
+							<el-switch v-model="form.oss.use_temp_bucket" />
+						</el-form-item>
+					</template>
+				</el-form>
+			</template>
+
+			<template #footer>
+				<div class="drawer-footer">
+					<el-button @click="drawerVisible = false">取消</el-button>
+					<el-button @click="reloadSelectedProvider" :loading="providerLoading === selectedProvider">
+						重新加载
+					</el-button>
+					<el-button type="primary" plain :disabled="!selectedProvider"
+						:loading="selectedProvider ? testingProvider === selectedProvider : false"
+						@click="selectedProvider && testProvider(selectedProvider)">
+						测试连接
+					</el-button>
+					<el-button type="primary" :loading="saving" @click="saveConfig">保存配置</el-button>
+				</div>
+			</template>
+		</el-drawer>
 	</div>
 </template>
 
 <script setup lang="ts">
-import { onMounted, reactive, ref } from 'vue'
+import { computed, onMounted, reactive, ref } from 'vue'
 import { ElMessage } from 'element-plus'
 import { storageProvider } from '@repo/api-service'
 
@@ -280,6 +271,7 @@ const pageLoading = ref(false)
 const engineLoading = ref(false)
 const initLoading = ref(false)
 const saving = ref(false)
+const drawerVisible = ref(false)
 const selectedProvider = ref<StorageProviderName | ''>('')
 const providerLoading = ref<StorageProviderName | ''>('')
 const testingProvider = ref<StorageProviderName | ''>('')
@@ -448,9 +440,6 @@ async function loadEngines() {
 					description: item.description || ''
 				}))
 		}
-		if (!selectedProvider.value && engines.value[0]) {
-			await selectProvider(engines.value[0]!.name)
-		}
 	} catch {
 		ElMessage.error('加载引擎失败')
 	} finally {
@@ -499,6 +488,11 @@ async function selectProvider(provider: StorageProviderName) {
 	await loadProviderConfig(provider)
 }
 
+async function openEditProvider(provider: StorageProviderName) {
+	await selectProvider(provider)
+	drawerVisible.value = true
+}
+
 function buildCheckPayload(provider: StorageProviderName) {
 	switch (provider) {
 		case 'local':
@@ -535,6 +529,10 @@ async function testProvider(provider: StorageProviderName) {
 	}
 }
 
+async function getDefaultEngine() {
+	return storageProvider.()
+}
+
 function buildUpdatePayload() {
 	return {
 		default_provider: form.default_provider,
@@ -547,6 +545,13 @@ function buildUpdatePayload() {
 	}
 }
 
+async function ensureAllConfigsLoaded() {
+	for (const provider of providerNames) {
+		if (loadedProviders.value[provider]) continue
+		await loadProviderConfig(provider)
+	}
+}
+
 async function saveConfig() {
 	if (!selectedProvider.value) {
 		ElMessage.warning('请先选择一个引擎')
@@ -554,10 +559,12 @@ async function saveConfig() {
 	}
 	saving.value = true
 	try {
+		await ensureAllConfigsLoaded()
 		const res = await storageProvider.postStorageProviderUpdate(buildUpdatePayload() as any)
 		if (res?.isSuccess) {
 			ElMessage.success('配置已保存')
 			await loadEngines()
+			drawerVisible.value = false
 			return
 		}
 		ElMessage.error('保存失败')
@@ -588,7 +595,6 @@ onMounted(async () => {
 	display: flex;
 	flex-direction: column;
 	gap: 16px;
-	min-height: calc(100vh - 210px);
 }
 
 .storage-actions {
@@ -598,119 +604,175 @@ onMounted(async () => {
 	flex-wrap: wrap;
 }
 
-.storage-layout {
+.toolbar-meta {
+	display: flex;
+	align-items: center;
+	gap: 10px;
+	flex-wrap: wrap;
+}
+
+.pill {
+	padding: 8px 12px;
+	border-radius: 999px;
+	border: 1px solid #e5e7eb;
+	background: #f8fafc;
+	font-size: 12px;
+	color: #64748b;
+}
+
+.grid {
 	display: grid;
-	grid-template-columns: 280px minmax(0, 1fr);
+	grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
 	gap: 16px;
-	flex: 1;
-	min-height: 0;
+	min-height: 240px;
 }
 
-.engine-list {
+.card {
+	overflow: hidden;
+	padding: 16px;
 	border: 1px solid #e5e7eb;
-	border-radius: 12px;
+	border-radius: 20px;
 	background: #fff;
-	padding: 12px;
 	display: flex;
 	flex-direction: column;
+	gap: 14px;
+	box-shadow: 0 12px 28px rgba(15, 23, 42, 0.06);
+	transition:
+		transform 0.22s ease,
+		box-shadow 0.22s ease,
+		border-color 0.22s ease;
+}
+
+.card:hover {
+	transform: translateY(-3px);
+	border-color: #d8dee9;
+	box-shadow: 0 18px 36px rgba(15, 23, 42, 0.1);
+}
+
+.card-head {
+	padding: 14px 16px;
+	border-radius: 18px;
+	background:
+		radial-gradient(circle at top left, rgba(15, 23, 42, 0.06), transparent 38%),
+		linear-gradient(135deg, #f8fafc, #eef2ff 58%, #fff7ed);
+}
+
+.card-head__top {
+	display: flex;
+	align-items: flex-start;
+	justify-content: space-between;
 	gap: 12px;
-	min-height: 0;
-	overflow: hidden;
 }
 
-.engine-list__header {
-	font-size: 14px;
+.title-block {
+	min-width: 0;
+}
+
+.title {
+	font-size: 18px;
 	font-weight: 700;
-	color: #111827;
+	line-height: 1.25;
+	color: #0f172a;
+	word-break: break-word;
+}
+
+.subtitle {
+	margin-top: 6px;
+	font-size: 13px;
+	color: #64748b;
+	line-height: 1.6;
 }
 
-.engine-list__body {
+.badge-row {
+	margin-top: 10px;
 	display: flex;
-	flex-direction: column;
-	gap: 10px;
-	flex: 1;
-	min-height: 0;
-	overflow-y: auto;
-	padding-right: 4px;
+	flex-wrap: wrap;
+	gap: 8px;
 }
 
-.engine-card {
-	padding: 12px;
-	border: 1px solid #e5e7eb;
-	border-radius: 12px;
-	background: #f8fafc;
-	cursor: pointer;
-	transition: all 0.2s ease;
+.badge {
+	padding: 5px 10px;
+	border-radius: 999px;
+	background: rgba(255, 255, 255, 0.92);
+	font-size: 12px;
+	color: #334155;
+	border: 1px solid rgba(203, 213, 225, 0.9);
 }
 
-.engine-card:hover,
-.engine-card--active {
-	border-color: #3b82f6;
-	background: #eff6ff;
+.badge.subtle {
+	background: rgba(15, 23, 42, 0.82);
+	color: #f8fafc;
+	border-color: transparent;
 }
 
-.engine-card__top {
+.tags {
 	display: flex;
-	align-items: center;
-	justify-content: space-between;
+	flex-wrap: wrap;
 	gap: 8px;
-	margin-bottom: 8px;
 }
 
-.engine-card__name {
-	font-weight: 700;
-	color: #111827;
+.meta-list {
+	display: grid;
+	grid-template-columns: repeat(2, minmax(0, 1fr));
+	gap: 10px;
 }
 
-.engine-card__meta {
+.meta-item {
+	padding: 10px 12px;
+	border-radius: 14px;
+	background: #f8fafc;
+	border: 1px solid #e2e8f0;
 	display: flex;
-	flex-wrap: wrap;
-	gap: 6px;
-	margin-bottom: 8px;
+	flex-direction: column;
+	gap: 4px;
+	min-width: 0;
 }
 
-.engine-card__desc {
+.meta-label {
 	font-size: 12px;
-	line-height: 1.5;
-	color: #6b7280;
+	color: #94a3b8;
 }
 
-.config-panel {
-	border: 1px solid #e5e7eb;
-	border-radius: 12px;
-	background: #fff;
-	padding: 16px;
+.meta-value {
+	font-size: 13px;
+	color: #334155;
+	word-break: break-word;
+}
+
+.card-footer {
 	display: flex;
-	flex-direction: column;
-	gap: 16px;
-	min-height: 0;
-	overflow: auto;
+	justify-content: flex-end;
+	gap: 6px;
+	margin-top: auto;
+	flex-wrap: wrap;
+}
+
+.drawer-intro {
+	padding: 16px;
+	border-radius: 18px;
+	background:
+		radial-gradient(circle at top left, rgba(15, 23, 42, 0.06), transparent 38%),
+		linear-gradient(135deg, #f8fafc, #eef2ff 58%, #fff7ed);
+	margin-bottom: 16px;
 }
 
-.config-panel__title {
+.drawer-intro__title {
 	font-size: 16px;
 	font-weight: 700;
 	color: #111827;
 }
 
-.config-panel__desc {
+.drawer-intro__desc {
 	margin-top: 6px;
 	font-size: 13px;
 	color: #6b7280;
 }
 
-.config-panel__footer {
+.drawer-footer {
 	display: flex;
 	justify-content: flex-end;
 	gap: 8px;
-}
-
-.config-panel__empty {
-	display: flex;
-	align-items: center;
-	justify-content: center;
-	min-height: 320px;
-	color: #9ca3af;
+	flex-wrap: wrap;
 }
 
 .form-grid {
@@ -720,7 +782,8 @@ onMounted(async () => {
 }
 
 @media (max-width: 960px) {
-	.storage-layout,
+
+	.meta-list,
 	.form-grid {
 		grid-template-columns: minmax(0, 1fr);
 	}

+ 250 - 84
apps/web/src/views/resource/components/WebSearchPanel.vue

@@ -2,102 +2,89 @@
 	<div class="panel">
 		<div class="toolbar">
 			<div class="toolbar-left">
-				<el-input
-					v-model="keyword"
-					clearable
-					placeholder="搜索提供者名称"
-					class="search-input"
-					@keyup.enter="loadList(1)"
-				>
+				<el-input v-model="keyword" clearable placeholder="搜索提供者名称" class="search-input" @keyup.enter="loadList(1)">
 					<template #prefix>
-						<el-icon><Search /></el-icon>
+						<el-icon>
+							<Search />
+						</el-icon>
 					</template>
 				</el-input>
-				<el-select
-					v-model="provider"
-					clearable
-					placeholder="服务商"
-					style="width: 180px"
-					@change="loadList(1)"
-				>
-					<el-option
-						v-for="item in engineOptions"
-						:key="item.id"
-						:label="item.name"
-						:value="item.id"
-					/>
+				<el-select v-model="provider" clearable placeholder="服务商" style="width: 180px" @change="loadList(1)">
+					<el-option v-for="item in engineOptions" :key="item.id" :label="item.name" :value="item.id" />
 				</el-select>
 				<el-button @click="loadList(1)">查询</el-button>
 				<el-button @click="handleReset">重置</el-button>
 			</div>
 			<div class="toolbar-right">
 				<el-button type="primary" @click="openCreate">
-					<el-icon><Plus /></el-icon>
+					<el-icon>
+						<Plus />
+					</el-icon>
 					新建网络搜索
 				</el-button>
 			</div>
 		</div>
 
-		<div class="tip-box">
-			<div class="tip-title">支持的搜索引擎</div>
-			<div class="engine-tags">
-				<el-tag v-for="item in engineOptions" :key="item.id" effect="light">
-					{{ item.name }}
-				</el-tag>
-			</div>
-		</div>
-
-		<div v-loading="loading" class="list-wrap">
+		<div v-loading="loading" class="grid">
 			<el-empty v-if="!list.length && !loading" description="暂无网络搜索配置" />
-			<el-table v-else :data="list" border>
-				<el-table-column prop="name" label="名称" min-width="180" />
-				<el-table-column prop="provider" label="服务商" width="160" />
-				<el-table-column prop="description" label="描述" min-width="220" />
-				<el-table-column label="默认" width="90">
-					<template #default="{ row }">
-						<el-tag :type="row.is_default ? 'success' : 'info'" effect="light">
-							{{ row.is_default ? '是' : '否' }}
-						</el-tag>
-					</template>
-				</el-table-column>
-				<el-table-column label="操作" width="260" fixed="right">
-					<template #default="{ row }">
-						<el-button link type="primary" @click="checkItem(row.id)">测试</el-button>
-						<el-button link type="primary" @click="openEditById(row.id)">编辑</el-button>
-						<el-button link type="danger" @click="removeItem(row.id)">删除</el-button>
-					</template>
-				</el-table-column>
-			</el-table>
+			<div v-for="row in list" :key="row.id" class="card">
+				<div class="card-head">
+					<div class="flex items-center justify-between">
+						<div class="title">{{ row.name || '未命名网络搜索' }}</div>
+						<div class="actions" @click.stop>
+							<el-dropdown>
+								<span class="actions-trigger">
+									<el-icon>
+										<MoreFilled />
+									</el-icon>
+								</span>
+								<template #dropdown>
+									<el-dropdown-menu>
+										<el-dropdown-item @click="openEditById(row.id)">编辑</el-dropdown-item>
+										<el-dropdown-item @click="removeItem(row.id)" divided>
+											<span class="danger-text">删除</span>
+										</el-dropdown-item>
+									</el-dropdown-menu>
+								</template>
+							</el-dropdown>
+						</div>
+					</div>
+
+					<div class="subtitle">{{ getEngineName(row.provider) }}</div>
+					<div class="badge-row">
+						<span class="badge">{{ getEngineName(row.provider) }}</span>
+						<span class="badge subtle">{{ row.is_default ? '默认配置' : '普通配置' }}</span>
+					</div>
+				</div>
+
+				<div class="desc">{{ row.description || '暂无描述' }}</div>
+
+				<div class="tags">
+					<el-tag :type="row.is_default ? 'success' : 'info'" effect="light">
+						{{ row.is_default ? '默认' : '非默认' }}
+					</el-tag>
+					<el-tag v-if="row.provider" type="info" effect="light">
+						{{ row.provider }}
+					</el-tag>
+				</div>
+
+				<div class="card-footer">
+					<el-button link type="primary" @click="checkItem(row.id)">连接测试</el-button>
+				</div>
+			</div>
 		</div>
 
 		<div class="pagination">
-			<el-pagination
-				v-model:current-page="pagination.pageIndex"
-				v-model:page-size="pagination.pageSize"
-				background
-				layout="total, sizes, prev, pager, next, jumper"
-				:page-sizes="[10, 20, 50, 100]"
-				:total="pagination.totalCount"
-				@current-change="handlePageChange"
-				@size-change="handleSizeChange"
-			/>
+			<el-pagination v-model:current-page="pagination.pageIndex" v-model:page-size="pagination.pageSize" background
+				layout="total, sizes, prev, pager, next, jumper" :page-sizes="[10, 20, 50, 100]" :total="pagination.totalCount"
+				@current-change="handlePageChange" @size-change="handleSizeChange" />
 		</div>
 
-		<el-drawer
-			v-model="drawerVisible"
-			:title="currentId ? '编辑网络搜索' : '新建网络搜索'"
-			direction="rtl"
-			size="700px"
-		>
+		<el-drawer v-model="drawerVisible" :title="currentId ? '编辑网络搜索' : '新建网络搜索'" direction="rtl" size="700px">
 			<el-form ref="formRef" :model="form" :rules="rules" label-position="top">
 				<el-form-item label="服务商" prop="provider">
 					<el-select v-model="form.provider" style="width: 100%">
-						<el-option
-							v-for="item in engineOptions"
-							:key="item.id"
-							:label="item.name"
-							:value="item.id"
-						/>
+						<el-option v-for="item in engineOptions" :key="item.id" :label="item.name" :value="item.id" />
 					</el-select>
 				</el-form-item>
 				<el-form-item label="名称" prop="name">
@@ -123,13 +110,8 @@
 				<el-form-item>
 					<div class="check-box">
 						<el-button :loading="checkLoading" @click="checkWithParameters">测试连接</el-button>
-						<el-alert
-							v-if="checkMessage"
-							:title="checkMessage"
-							:type="checkSuccess ? 'success' : 'error'"
-							:closable="false"
-							show-icon
-						/>
+						<el-alert v-if="checkMessage" :title="checkMessage" :type="checkSuccess ? 'success' : 'error'"
+							:closable="false" show-icon />
 					</div>
 				</el-form-item>
 			</el-form>
@@ -144,9 +126,9 @@
 </template>
 
 <script setup lang="ts">
-import { onMounted, reactive, ref } from 'vue'
+import { computed, onMounted, reactive, ref } from 'vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
-import { Plus, Search } from '@element-plus/icons-vue'
+import { Plus, Search, MoreFilled } from '@element-plus/icons-vue'
 import { resource } from '@repo/api-service'
 
 type EngineResponse = Awaited<ReturnType<typeof resource.postWebSearchEngines>>
@@ -202,6 +184,10 @@ function resetForm() {
 	form.parameters.engine_id = ''
 }
 
+function getEngineName(id?: string) {
+	return engineOptions.value.find((item) => item.id === id)?.name || id || '未设置服务商'
+}
+
 async function loadEngines() {
 	const res = await resource.postWebSearchEngines({})
 	if (res.isSuccess) {
@@ -357,6 +343,7 @@ onMounted(async () => {
 	flex-direction: column;
 	gap: 16px;
 }
+
 .toolbar {
 	display: flex;
 	align-items: center;
@@ -364,6 +351,7 @@ onMounted(async () => {
 	gap: 12px;
 	flex-wrap: wrap;
 }
+
 .toolbar-left,
 .toolbar-right {
 	display: flex;
@@ -371,53 +359,231 @@ onMounted(async () => {
 	gap: 10px;
 	flex-wrap: wrap;
 }
+
 .search-input {
 	width: 280px;
 }
+
+.toolbar-meta {
+	display: flex;
+	align-items: center;
+	gap: 10px;
+	flex-wrap: wrap;
+}
+
+.pill {
+	padding: 8px 12px;
+	border-radius: 999px;
+	border: 1px solid #e5e7eb;
+	background: #f8fafc;
+	font-size: 12px;
+	color: #64748b;
+}
+
 .tip-box {
 	padding: 14px 16px;
 	border-radius: 16px;
 	background: #f8fafc;
 	border: 1px solid #e5e7eb;
 }
+
 .tip-title {
 	font-weight: 700;
 	color: #111827;
 	margin-bottom: 10px;
 }
+
 .engine-tags {
 	display: flex;
 	flex-wrap: wrap;
 	gap: 8px;
 }
-.list-wrap {
+
+.grid {
+	display: grid;
+	grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+	gap: 16px;
 	min-height: 240px;
 }
+
+.actions {
+	display: flex;
+	justify-content: flex-end;
+}
+
+.actions-trigger {
+	width: 30px;
+	height: 30px;
+	border-radius: 999px;
+	display: grid;
+	place-items: center;
+	color: #475569;
+	background: rgba(255, 255, 255, 0.8);
+	transition:
+		background 0.2s ease,
+		color 0.2s ease;
+}
+
+.actions-trigger:hover {
+	background: rgba(15, 23, 42, 0.08);
+	color: #0f172a;
+}
+
+.danger-text {
+	color: var(--el-color-danger);
+}
+
+.card {
+	overflow: hidden;
+	border-radius: 20px;
+	border: 1px solid #e5e7eb;
+	background: #fff;
+	box-shadow: 0 12px 28px rgba(15, 23, 42, 0.06);
+	padding: 16px;
+	display: flex;
+	flex-direction: column;
+	gap: 14px;
+	transition:
+		transform 0.22s ease,
+		box-shadow 0.22s ease,
+		border-color 0.22s ease;
+}
+
+.card:hover {
+	transform: translateY(-3px);
+	border-color: #d8dee9;
+	box-shadow: 0 18px 36px rgba(15, 23, 42, 0.1);
+}
+
+.card-head {
+	padding: 14px 16px;
+	border-radius: 18px;
+	background:
+		radial-gradient(circle at top left, rgba(15, 23, 42, 0.06), transparent 38%),
+		linear-gradient(135deg, #f8fafc, #eef2ff 58%, #fff7ed);
+}
+
+.title {
+	font-size: 18px;
+	font-weight: 700;
+	line-height: 1.25;
+	color: #0f172a;
+	word-break: break-word;
+}
+
+.subtitle {
+	margin-top: 6px;
+	font-size: 13px;
+	color: #64748b;
+	word-break: break-word;
+}
+
+.badge-row {
+	margin-top: 10px;
+	display: flex;
+	flex-wrap: wrap;
+	gap: 8px;
+}
+
+.badge {
+	padding: 5px 10px;
+	border-radius: 999px;
+	background: rgba(255, 255, 255, 0.92);
+	font-size: 12px;
+	color: #334155;
+	border: 1px solid rgba(203, 213, 225, 0.9);
+}
+
+.badge.subtle {
+	background: rgba(15, 23, 42, 0.82);
+	color: #f8fafc;
+	border-color: transparent;
+}
+
+.desc {
+	min-height: 44px;
+	color: #64748b;
+	font-size: 14px;
+	line-height: 1.65;
+	display: -webkit-box;
+	-webkit-line-clamp: 2;
+	-webkit-box-orient: vertical;
+	overflow: hidden;
+}
+
+.tags {
+	display: flex;
+	flex-wrap: wrap;
+	gap: 8px;
+}
+
+.meta-list {
+	display: grid;
+	grid-template-columns: repeat(2, minmax(0, 1fr));
+	gap: 10px;
+}
+
+.meta-item {
+	padding: 10px 12px;
+	border-radius: 14px;
+	background: #f8fafc;
+	border: 1px solid #e2e8f0;
+	display: flex;
+	flex-direction: column;
+	gap: 4px;
+	min-width: 0;
+}
+
+.meta-label {
+	font-size: 12px;
+	color: #94a3b8;
+}
+
+.meta-value {
+	font-size: 13px;
+	color: #334155;
+	word-break: break-word;
+}
+
+.card-footer {
+	display: flex;
+	justify-content: flex-end;
+	gap: 6px;
+	margin-top: auto;
+	flex-wrap: wrap;
+}
+
 .pagination {
 	display: flex;
 	justify-content: flex-end;
 }
+
 .grid-2 {
 	display: grid;
 	grid-template-columns: repeat(2, minmax(0, 1fr));
 	gap: 12px;
 }
+
 .drawer-footer {
 	display: flex;
 	justify-content: flex-end;
 	gap: 10px;
 }
+
 .check-box {
 	width: 100%;
 	display: flex;
 	flex-direction: column;
 	gap: 10px;
 }
+
 @media (max-width: 768px) {
 	.search-input {
 		width: 100%;
 	}
-	.grid-2 {
+
+	.grid-2,
+	.meta-list {
 		grid-template-columns: 1fr;
 	}
 }

+ 309 - 0
packages/api-service/schema/knowledge.openapi.json

@@ -4391,6 +4391,315 @@
 				"security": []
 			}
 		},
+		"/api/ai/storage-provider/default-provider": {
+			"post": {
+				"summary": "获取默认存储厂商",
+				"deprecated": false,
+				"description": "",
+				"tags": ["storageProvider"],
+				"parameters": [
+					{
+						"name": "Authorization",
+						"in": "header",
+						"description": "",
+						"example": "bpm_client_1512111984605138944",
+						"schema": {
+							"type": "string",
+							"default": "bpm_client_1512111984605138944"
+						}
+					}
+				],
+				"requestBody": {
+					"content": {
+						"application/json": {
+							"schema": {
+								"type": "object",
+								"properties": {},
+								"required": []
+							},
+							"examples": {
+								"1": {
+									"value": {
+										"name": "local",
+										"is_default": false,
+										"config": {
+											"path_prefix": ""
+										}
+									},
+									"summary": "LOCAL"
+								},
+								"2": {
+									"value": {
+										"name": "minio",
+										"is_default": false,
+										"config": {
+											"endpoint": "",
+											"access_key_id": "",
+											"secret_access_key": "",
+											"mode": "docker",
+											"bucket_name": "",
+											"use_ssl": false,
+											"path_prefix": ""
+										}
+									},
+									"summary": "MINIO"
+								},
+								"3": {
+									"value": {
+										"name": "cos",
+										"is_default": false,
+										"config": {
+											"app_id": "",
+											"secret_id": "",
+											"secret_key": "",
+											"region": "docker",
+											"bucket_name": "",
+											"path_prefix": ""
+										}
+									},
+									"summary": "COS"
+								},
+								"4": {
+									"value": {
+										"name": "tos",
+										"is_default": false,
+										"config": {
+											"endpoint": "",
+											"access_key": "",
+											"secret_key": "",
+											"region": "",
+											"bucket_name": "",
+											"path_prefix": ""
+										}
+									},
+									"summary": "TOS"
+								},
+								"5": {
+									"value": {
+										"name": "s3",
+										"is_default": false,
+										"config": {
+											"endpoint": "",
+											"access_key": "",
+											"secret_key": "",
+											"region": "",
+											"bucket_name": "",
+											"path_prefix": ""
+										}
+									},
+									"summary": "S3"
+								},
+								"6": {
+									"value": {
+										"name": "oss",
+										"is_default": false,
+										"config": {
+											"endpoint": "",
+											"access_key": "",
+											"secret_key": "",
+											"region": "",
+											"bucket_name": "",
+											"path_prefix": ""
+										}
+									},
+									"summary": "OSS"
+								}
+							}
+						}
+					},
+					"required": true
+				},
+				"responses": {
+					"200": {
+						"description": "",
+						"content": {
+							"application/json": {
+								"schema": {
+									"type": "object",
+									"properties": {
+										"isSuccess": {
+											"type": "boolean"
+										},
+										"code": {
+											"type": "integer"
+										},
+										"isAuthorized": {
+											"type": "boolean"
+										},
+										"result": {
+											"type": "string"
+										}
+									},
+									"required": ["isSuccess", "code", "isAuthorized"]
+								},
+								"example": {
+									"isSuccess": true,
+									"code": 1,
+									"isAuthorized": true
+								}
+							}
+						},
+						"headers": {}
+					}
+				},
+				"security": []
+			}
+		},
+		"/api/ai/storage-provider/updateDefaultProvider": {
+			"post": {
+				"summary": "更新默认存储厂商",
+				"deprecated": false,
+				"description": "",
+				"tags": ["storageProvider"],
+				"parameters": [
+					{
+						"name": "Authorization",
+						"in": "header",
+						"description": "",
+						"example": "bpm_client_1512111984605138944",
+						"schema": {
+							"type": "string",
+							"default": "bpm_client_1512111984605138944"
+						}
+					}
+				],
+				"requestBody": {
+					"content": {
+						"application/json": {
+							"schema": {
+								"type": "object",
+								"properties": {
+									"provider": {
+										"type": "string"
+									}
+								},
+								"required": ["provider"]
+							},
+							"examples": {
+								"1": {
+									"value": {
+										"name": "local",
+										"is_default": false,
+										"config": {
+											"path_prefix": ""
+										}
+									},
+									"summary": "LOCAL"
+								},
+								"2": {
+									"value": {
+										"name": "minio",
+										"is_default": false,
+										"config": {
+											"endpoint": "",
+											"access_key_id": "",
+											"secret_access_key": "",
+											"mode": "docker",
+											"bucket_name": "",
+											"use_ssl": false,
+											"path_prefix": ""
+										}
+									},
+									"summary": "MINIO"
+								},
+								"3": {
+									"value": {
+										"name": "cos",
+										"is_default": false,
+										"config": {
+											"app_id": "",
+											"secret_id": "",
+											"secret_key": "",
+											"region": "docker",
+											"bucket_name": "",
+											"path_prefix": ""
+										}
+									},
+									"summary": "COS"
+								},
+								"4": {
+									"value": {
+										"name": "tos",
+										"is_default": false,
+										"config": {
+											"endpoint": "",
+											"access_key": "",
+											"secret_key": "",
+											"region": "",
+											"bucket_name": "",
+											"path_prefix": ""
+										}
+									},
+									"summary": "TOS"
+								},
+								"5": {
+									"value": {
+										"name": "s3",
+										"is_default": false,
+										"config": {
+											"endpoint": "",
+											"access_key": "",
+											"secret_key": "",
+											"region": "",
+											"bucket_name": "",
+											"path_prefix": ""
+										}
+									},
+									"summary": "S3"
+								},
+								"6": {
+									"value": {
+										"name": "oss",
+										"is_default": false,
+										"config": {
+											"endpoint": "",
+											"access_key": "",
+											"secret_key": "",
+											"region": "",
+											"bucket_name": "",
+											"path_prefix": ""
+										}
+									},
+									"summary": "OSS"
+								}
+							}
+						}
+					},
+					"required": true
+				},
+				"responses": {
+					"200": {
+						"description": "",
+						"content": {
+							"application/json": {
+								"schema": {
+									"type": "object",
+									"properties": {
+										"isSuccess": {
+											"type": "boolean"
+										},
+										"code": {
+											"type": "integer"
+										},
+										"isAuthorized": {
+											"type": "boolean"
+										}
+									},
+									"required": ["isSuccess", "code", "isAuthorized"]
+								},
+								"example": {
+									"isSuccess": true,
+									"code": 1,
+									"isAuthorized": true
+								}
+							}
+						},
+						"headers": {}
+					}
+				},
+				"security": []
+			}
+		},
 		"/api/ai/wiki/pageList": {
 			"post": {
 				"summary": "获取wiki页面列表",

+ 38 - 0
packages/api-service/servers/knowledge/api/storageProvider.ts

@@ -63,6 +63,24 @@ export async function postStorageProviderConfig(
   })
 }
 
+/** 获取默认存储厂商 POST /api/ai/storage-provider/default-provider */
+export async function postStorageProviderDefaultProvider(
+  body: {},
+  options?: { [key: string]: any }
+) {
+  return request<{ isSuccess: boolean; code: number; isAuthorized: boolean; result?: string }>(
+    '/api/ai/storage-provider/default-provider',
+    {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json'
+      },
+      data: body,
+      ...(options || {})
+    }
+  )
+}
+
 /** 获取支持的引擎列表 POST /api/ai/storage-provider/engines */
 export async function postStorageProviderEngines(body: {}, options?: { [key: string]: any }) {
   return request<{
@@ -167,3 +185,23 @@ export async function postStorageProviderUpdate(
     }
   )
 }
+
+/** 更新默认存储厂商 POST /api/ai/storage-provider/updateDefaultProvider */
+export async function postStorageProviderUpdateDefaultProvider(
+  body: {
+    provider: string
+  },
+  options?: { [key: string]: any }
+) {
+  return request<{ isSuccess: boolean; code: number; isAuthorized: boolean }>(
+    '/api/ai/storage-provider/updateDefaultProvider',
+    {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json'
+      },
+      data: body,
+      ...(options || {})
+    }
+  )
+}