Browse Source

fix: 调整项目内容

jiaxing.liao 1 week ago
parent
commit
446d81949c

File diff suppressed because it is too large
+ 3 - 3
apps/web/src/assets/icons/ollama.svg


+ 8 - 14
apps/web/src/router/index.ts

@@ -16,18 +16,17 @@ const UserCenter = () => import('@/views/UserCenter.vue')
 const LogStream = () => import('@/views/LogStream.vue')
 const ModelLog = () => import('@/views/ModelLog.vue')
 const WorkflowExecution = () => import('@/views/WorkflowExecution.vue')
-const FlowManagement = () => import('@/views/FlowManagement.vue')
+const FlowManagement = () => import('@/views/flow/index.vue')
 const ModelManager = () => import('@/views/model/index.vue')
-const ModelPage = () => import('@/views/model/ModelPage.vue')
-const OllamaPage = () => import('@/views/model/OllamaPage.vue')
+const ModelPage = () => import('@/views/model/index.vue')
+const OllamaPage = () => import('@/views/ollama/index.vue')
 const KnowledgeManager = () => import('@/views/knowledge/index.vue')
 const AgentManager = () => import('@/views/agent/index.vue')
-const ResourceManager = () => import('@/views/resource/index.vue')
-const PromptPage = () => import('@/views/resource/PromptPage.vue')
-const WebSearchPage = () => import('@/views/resource/WebSearchPage.vue')
-const McpPage = () => import('@/views/resource/McpPage.vue')
-const StoragePage = () => import('@/views/resource/StoragePage.vue')
-const SkillsPage = () => import('@/views/resource/SkillsPage.vue')
+const PromptPage = () => import('@/views/prompt/index.vue')
+const WebSearchPage = () => import('@/views/web-search/index.vue')
+const McpPage = () => import('@/views/mcp/index.vue')
+const StoragePage = () => import('@/views/storage/index.vue')
+const SkillsPage = () => import('@/views/skills/index.vue')
 const Workspace = () => import('@/views/workspace/index.vue')
 
 const routes = [
@@ -116,11 +115,6 @@ const routes = [
 				name: 'KnowledgeManager',
 				component: KnowledgeManager
 			},
-			{
-				path: 'resource',
-				name: 'ResourceManager',
-				component: ResourceManager
-			},
 			{
 				path: 'prompts',
 				name: 'PromptPage',

+ 2 - 2
apps/web/src/views/chat/composables/useChatStream.ts

@@ -133,9 +133,9 @@ export function useChatStream() {
 				if (!result.trim()) continue
 
 				const results = parseSseBlock(result)
-				// 当内容done为true 回话内容结束
+				// 当内容为complete 需要主动结束
 				results.forEach(onChunk)
-				if (results.some((item) => item?.complete)) {
+				if (results.some((item) => item?.response_type === 'complete')) {
 					doneReceived = true
 					controller.abort()
 					break

+ 78 - 28
apps/web/src/views/chat/index.vue

@@ -149,9 +149,9 @@
 </template>
 
 <script setup lang="ts">
-import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
+import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
-import { PictureFilled, Setting } from '@element-plus/icons-vue'
+import { PictureFilled } from '@element-plus/icons-vue'
 import { useI18n } from '@/composables/useI18n'
 import FileUploadInput from '@/features/fileUpload/FileUploadInput.vue'
 import { useChatStream } from './composables/useChatStream'
@@ -218,6 +218,31 @@ const activeStreamToken = ref('')
 const chatInputRef = ref<InstanceType<typeof ChatInput>>()
 const agentPromptsItems = ref<PromptsItemsProps[]>()
 const addkbRef = ref<InstanceType<typeof AddKbModal>>()
+let scrollToBottomRafId: number | null = null
+
+const clearPendingScrollToBottom = () => {
+	if (scrollToBottomRafId !== null) {
+		window.cancelAnimationFrame(scrollToBottomRafId)
+		scrollToBottomRafId = null
+	}
+}
+
+const scheduleScrollToBottom = (repeat = 3) => {
+	clearPendingScrollToBottom()
+
+	let remaining = Math.max(1, repeat)
+	const run = () => {
+		messageListRef.value?.scrollToBottom?.()
+		remaining -= 1
+		if (remaining > 0) {
+			scrollToBottomRafId = window.requestAnimationFrame(run)
+			return
+		}
+		scrollToBottomRafId = null
+	}
+
+	scrollToBottomRafId = window.requestAnimationFrame(run)
+}
 
 /**
  * 创建知识
@@ -242,6 +267,30 @@ const showImageUploadButton = computed(
 	() => activeTargetType.value === 'agent' && allowImageUpload.value
 )
 
+const lastMessageScrollKey = computed(() => {
+	const lastMessage = messages.value[messages.value.length - 1]
+	return [
+		messages.value.length,
+		lastMessage?.id || lastMessage?.key || '',
+		lastMessage?.content?.length || 0,
+		lastMessage?.answerText?.length || 0,
+		lastMessage?.thinking?.length || 0,
+		lastMessage?.toolCalls?.length || 0,
+		lastMessage?.toolResults?.length || 0,
+		lastMessage?.references?.length || 0,
+		lastMessage?.streamCompleted ? 1 : 0
+	].join(':')
+})
+
+watch(
+	lastMessageScrollKey,
+	() => {
+		if (!messages.value.length) return
+		scheduleScrollToBottom()
+	},
+	{ flush: 'post' }
+)
+
 /**
  * 计算当前聊天配置对象,用于发送消息时使用
  */
@@ -274,6 +323,10 @@ onMounted(async () => {
 	}
 })
 
+onBeforeUnmount(() => {
+	clearPendingScrollToBottom()
+})
+
 /**
  * 创建默认的聊天目标配置
  * @param type - 聊天目标类型,默认为 'agent'
@@ -913,17 +966,16 @@ const loadConversationMessages = async (conversationId: string) => {
 		return
 	}
 
-	try {
-		const res = await getSessionMessages(conversationId)
-		if (res.isSuccess && res.result?.model) {
-			messages.value = res.result.model.flatMap((item) =>
-				parseHistoryRecord(item as Record<string, any>)
-			)
-			await nextTick()
-			messageListRef.value?.scrollToBottom?.()
-		} else {
-			messages.value = []
-		}
+		try {
+			const res = await getSessionMessages(conversationId)
+			if (res.isSuccess && res.result?.model) {
+				messages.value = res.result.model.flatMap((item) =>
+					parseHistoryRecord(item as Record<string, any>)
+				)
+				scheduleScrollToBottom(4)
+			} else {
+				messages.value = []
+			}
 	} catch (error) {
 		console.error('Failed to load messages', error)
 		messages.value = []
@@ -1025,12 +1077,12 @@ const fetchKnowledgeOptions = async (baseIds: string[]) => {
 		return
 	}
 
-	const results = await Promise.all(
-		baseIds.map((knowledgeBaseId) =>
-			knowledge.postAiKnowledgePageList({
-				knowledge_base_id: knowledgeBaseId,
-				title: '',
-				file_type: '',
+		const results = await Promise.all(
+			baseIds.map((knowledgeBaseId) =>
+				knowledge.postAiKnowledgePageList({
+					knowledge_base_id: knowledgeBaseId,
+					title: '',
+					file_type: '',
 				pageIndex: 1,
 				pageSize: 20
 			})
@@ -1038,11 +1090,11 @@ const fetchKnowledgeOptions = async (baseIds: string[]) => {
 	)
 
 	const optionMap: Record<string, { label: string; value: string }> = {}
-	results.forEach((res) => {
-		if (!res?.isSuccess) return
-		;(res.result?.model || []).forEach((item) => {
-			if (!item.id) return
-			optionMap[item.id] = {
+		results.forEach((res: any) => {
+			if (!res?.isSuccess) return
+			;(res.result?.model || []).forEach((item: any) => {
+				if (!item.id) return
+				optionMap[item.id] = {
 				label: item.file_name!,
 				value: item.id
 			}
@@ -1091,13 +1143,11 @@ const handleSend = async (content?: string) => {
 
 	const userMsg = createUserMessageWithAttachments(content)
 	messages.value.push(userMsg)
-	await nextTick()
-	messageListRef.value?.scrollToBottom?.()
+	scheduleScrollToBottom()
 
 	const aiMsg = createAiMessage()
 	messages.value.push(aiMsg)
-	await nextTick()
-	messageListRef.value?.scrollToBottom?.()
+	scheduleScrollToBottom()
 	const aiMessageKey = aiMsg.key as string
 	const streamToken = `${aiMessageKey}-${Date.now()}`
 	activeStreamToken.value = streamToken

apps/web/src/views/FlowManagement.vue → apps/web/src/views/flow/index.vue


+ 1 - 1
apps/web/src/views/knowledge/index.vue

@@ -51,7 +51,7 @@ function handleSelectBase(base?: KnowledgeBaseItem) {
 .knowledge-management-page {
 	min-height: 100%;
 	padding: 24px;
-	background: #f5f7fa;
+	// background: #f5f7fa;
 	box-sizing: border-box;
 }
 

+ 314 - 279
apps/web/src/views/resource/components/McpPanel.vue

@@ -1,51 +1,59 @@
 <template>
-	<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)"
-				>
-					<template #prefix>
+	<div class="management-page">
+		<div class="page-head">
+			<div>
+				<h1>MCP 服务</h1>
+				<p>维护 MCP 服务列表,并查看资源与工具详情。</p>
+			</div>
+		</div>
+
+		<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)"
+					>
+						<template #prefix>
+							<el-icon>
+								<Search />
+							</el-icon>
+						</template>
+					</el-input>
+					<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" />
+						<el-option label="http" value="http" />
+					</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>
-							<Search />
+							<Plus />
 						</el-icon>
-					</template>
-				</el-input>
-				<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" />
-					<el-option label="http" value="http" />
-				</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>
-					新建 MCP
-				</el-button>
+						新建 MCP
+					</el-button>
+				</div>
 			</div>
-		</div>
 
-		<div class="toolbar-meta">
-			<span class="pill">共 {{ pagination.totalCount }} 个服务</span>
-			<span class="pill">已启用 {{ enabledCount }} 个</span>
-		</div>
+			<div class="toolbar-meta">
+				<span class="pill">共 {{ pagination.totalCount }} 个服务</span>
+				<span class="pill">已启用 {{ enabledCount }} 个</span>
+			</div>
 
-		<!-- <div class="summary-grid">
+			<!-- <div class="summary-grid">
 			<div class="summary-card">
 				<div class="summary-value">{{ pagination.totalCount }}</div>
 				<div class="summary-label">MCP 总数</div>
@@ -60,265 +68,272 @@
 			</div>
 		</div> -->
 
-		<div v-loading="loading" class="grid">
-			<el-empty class="empty" v-if="!list.length && !loading" description="暂无 MCP 服务" />
-			<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 v-loading="loading" class="grid">
+				<el-empty class="empty" v-if="!list.length && !loading" description="暂无 MCP 服务" />
+				<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 class="actions">
+								<el-dropdown>
+									<span class="actions-trigger">
+										<el-icon>
+											<MoreFilled />
+										</el-icon>
+									</span>
+									<template #dropdown>
+										<el-dropdown-menu>
+											<el-dropdown-item @click="checkItem(row.id!)">测试</el-dropdown-item>
+											<el-dropdown-item @click="openTools(row.id!)">工具</el-dropdown-item>
+											<el-dropdown-item @click="openEditById(row.id!)">编辑</el-dropdown-item>
+											<el-dropdown-item divided @click="removeItem(row.id!)">
+												<span class="danger-text">删除</span>
+											</el-dropdown-item>
+										</el-dropdown-menu>
+									</template>
+								</el-dropdown>
+							</div>
 						</div>
-						<div class="actions">
-							<el-dropdown>
-								<span class="actions-trigger">
-									<el-icon>
-										<MoreFilled />
-									</el-icon>
-								</span>
-								<template #dropdown>
-									<el-dropdown-menu>
-										<el-dropdown-item @click="checkItem(row.id!)">测试</el-dropdown-item>
-										<el-dropdown-item @click="openTools(row.id!)">工具</el-dropdown-item>
-										<el-dropdown-item @click="openEditById(row.id!)">编辑</el-dropdown-item>
-										<el-dropdown-item divided @click="removeItem(row.id!)">
-											<span class="danger-text">删除</span>
-										</el-dropdown-item>
-									</el-dropdown-menu>
-								</template>
-							</el-dropdown>
+						<div class="badge-row">
+							<span class="badge">{{ row.transport_type || '未设置传输类型' }}</span>
+							<span class="badge subtle">{{ row.enabled ? '启用中' : '已禁用' }}</span>
 						</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="desc">{{ row.description || '暂无描述' }}</div>
+				</div>
 			</div>
-		</div>
 
-		<div v-if="pagination.totalCount > 0" 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"
-			/>
-		</div>
+			<div v-if="pagination.totalCount > 0" 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"
+				/>
+			</div>
 
-		<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" />
-				</el-form-item>
-				<el-form-item label="描述" prop="description">
-					<el-input v-model="form.description" type="textarea" :rows="2" />
-				</el-form-item>
-				<div class="grid-2">
-					<el-form-item label="传输类型" prop="transport_type">
-						<el-select v-model="form.transport_type" style="width: 100%">
-							<el-option label="SSE(Server-Sent Events)" value="sse" />
-							<el-option label="HTTP Streamable" value="http-streamable" />
-						</el-select>
+			<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" />
 					</el-form-item>
-					<el-form-item label="启用" prop="enabled">
-						<el-switch v-model="form.enabled" />
+					<el-form-item label="描述" prop="description">
+						<el-input v-model="form.description" type="textarea" :rows="2" />
 					</el-form-item>
-				</div>
-				<el-form-item label="地址" prop="url">
-					<el-input v-model="form.url" />
-				</el-form-item>
-				<el-form-item label="超时(秒)" prop="advanced_config.timeout">
-					<el-input-number v-model="form.advanced_config.timeout" :min="0" style="width: 100%" />
-				</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%"
-						/>
+					<div class="grid-2">
+						<el-form-item label="传输类型" prop="transport_type">
+							<el-select v-model="form.transport_type" style="width: 100%">
+								<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">
+							<el-switch v-model="form.enabled" />
+						</el-form-item>
+					</div>
+					<el-form-item label="地址" prop="url">
+						<el-input v-model="form.url" />
 					</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-form-item label="超时(秒)" prop="advanced_config.timeout">
+						<el-input-number v-model="form.advanced_config.timeout" :min="0" style="width: 100%" />
 					</el-form-item>
-				</div>
-				<el-form-item label="Headers">
-					<div class="kv-config">
-						<div class="kv-config__top">
-							<span>请求头</span>
-							<el-button type="primary" link @click="addHeaderRow">
-								<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"
-							>
-								<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)"
-								>
-									删除
+					<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-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-form-item>
+					</div>
+					<el-form-item label="Headers">
+						<div class="kv-config">
+							<div class="kv-config__top">
+								<span>请求头</span>
+								<el-button type="primary" link @click="addHeaderRow">
+									<el-icon> <Plus /> </el-icon>添加
 								</el-button>
 							</div>
-						</div>
-					</div>
-				</el-form-item>
-				<el-form-item label="Auth Config">
-					<div class="kv-config">
-						<div class="kv-config__top">
-							<span>鉴权配置</span>
-							<el-button type="primary" link @click="addAuthRow">
-								<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)"
+							<div class="kv-config__rows">
+								<div
+									v-for="(item, index) in headerList"
+									:key="`header-${index}`"
+									class="kv-config__row"
 								>
-									删除
-								</el-button>
+									<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>
+								</div>
 							</div>
 						</div>
-					</div>
-				</el-form-item>
-				<el-form-item label="Env Vars">
-					<div class="kv-config">
-						<div class="kv-config__top">
-							<span>环境变量</span>
-							<el-button type="primary" link @click="addEnvRow">
-								<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-form-item>
+					<el-form-item label="Auth Config">
+						<div class="kv-config">
+							<div class="kv-config__top">
+								<span>鉴权配置</span>
+								<el-button type="primary" link @click="addAuthRow">
+									<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>
+								</div>
+							</div>
+						</div>
+					</el-form-item>
+					<el-form-item label="Env Vars">
+						<div class="kv-config">
+							<div class="kv-config__top">
+								<span>环境变量</span>
+								<el-button type="primary" link @click="addEnvRow">
+									<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>
+								</div>
+							</div>
 						</div>
+					</el-form-item>
+					<el-form-item>
+						<div class="check-box">
+							<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
+							/>
+						</div>
+					</el-form-item>
+				</el-form>
+				<template #footer>
+					<div class="drawer-footer">
+						<el-button @click="drawerVisible = false">取消</el-button>
+						<el-button type="primary" :loading="submitLoading" @click="handleSubmit"
+							>保存</el-button
+						>
 					</div>
-				</el-form-item>
-				<el-form-item>
-					<div class="check-box">
-						<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
-						/>
-					</div>
-				</el-form-item>
-			</el-form>
-			<template #footer>
-				<div class="drawer-footer">
-					<el-button @click="drawerVisible = false">取消</el-button>
-					<el-button type="primary" :loading="submitLoading" @click="handleSubmit">保存</el-button>
+				</template>
+			</el-drawer>
+
+			<el-dialog v-model="detailVisible" title="MCP 详情" width="720px">
+				<el-descriptions v-if="detailItem" :column="1" border>
+					<el-descriptions-item label="名称">{{ detailItem.name }}</el-descriptions-item>
+					<el-descriptions-item label="传输类型">{{
+						detailItem.transport_type
+					}}</el-descriptions-item>
+					<el-descriptions-item label="地址">{{ detailItem.url || '-' }}</el-descriptions-item>
+					<el-descriptions-item label="描述">{{
+						detailItem.description || '-'
+					}}</el-descriptions-item>
+				</el-descriptions>
+			</el-dialog>
+
+			<el-dialog v-model="resourcesVisible" title="MCP 资源" width="720px">
+				<el-empty v-if="!resourceList.length" description="暂无资源" />
+				<el-space v-else wrap>
+					<el-tag v-for="item in resourceList" :key="item">{{ item }}</el-tag>
+				</el-space>
+			</el-dialog>
+
+			<el-dialog v-model="toolsVisible" title="MCP 工具" width="720px" class="tool-modal">
+				<div class="tool-dialog">
+					<el-input
+						v-model="toolKeyword"
+						clearable
+						placeholder="搜索工具名称或描述"
+						class="tool-search-input"
+					>
+						<template #prefix>
+							<el-icon>
+								<Search />
+							</el-icon>
+						</template>
+					</el-input>
+					<el-empty v-if="!filteredToolDetailList.length" description="暂无工具" />
+					<el-scrollbar v-else max-height="420px">
+						<el-collapse>
+							<el-collapse-item v-for="item in filteredToolDetailList" :key="item.name">
+								<template #title>
+									<div>
+										<div class="tool-desc text-16px text-primary mb-4px">{{ item.name }}</div>
+										<div class="tool-desc text-12px">{{ item.description }}</div>
+									</div>
+								</template>
+								<el-card>
+									<el-descriptions direction="vertical" :column="1">
+										<el-descriptions-item label="参数结构">
+											<CodeEditor
+												:model-value="JSON.stringify(item.inputSchema, null, 2)"
+												language="json"
+												:height="260"
+												:tools="false"
+												readonly
+											/>
+										</el-descriptions-item>
+									</el-descriptions>
+								</el-card>
+							</el-collapse-item>
+						</el-collapse>
+					</el-scrollbar>
 				</div>
-			</template>
-		</el-drawer>
-
-		<el-dialog v-model="detailVisible" title="MCP 详情" width="720px">
-			<el-descriptions v-if="detailItem" :column="1" border>
-				<el-descriptions-item label="名称">{{ detailItem.name }}</el-descriptions-item>
-				<el-descriptions-item label="传输类型">{{
-					detailItem.transport_type
-				}}</el-descriptions-item>
-				<el-descriptions-item label="地址">{{ detailItem.url || '-' }}</el-descriptions-item>
-				<el-descriptions-item label="描述">{{
-					detailItem.description || '-'
-				}}</el-descriptions-item>
-			</el-descriptions>
-		</el-dialog>
-
-		<el-dialog v-model="resourcesVisible" title="MCP 资源" width="720px">
-			<el-empty v-if="!resourceList.length" description="暂无资源" />
-			<el-space v-else wrap>
-				<el-tag v-for="item in resourceList" :key="item">{{ item }}</el-tag>
-			</el-space>
-		</el-dialog>
-
-		<el-dialog v-model="toolsVisible" title="MCP 工具" width="720px" class="tool-modal">
-			<div class="tool-dialog">
-				<el-input
-					v-model="toolKeyword"
-					clearable
-					placeholder="搜索工具名称或描述"
-					class="tool-search-input"
-				>
-					<template #prefix>
-						<el-icon>
-							<Search />
-						</el-icon>
-					</template>
-				</el-input>
-				<el-empty v-if="!filteredToolDetailList.length" description="暂无工具" />
-				<el-scrollbar v-else max-height="420px">
-					<el-collapse>
-						<el-collapse-item v-for="item in filteredToolDetailList" :key="item.name">
-							<template #title>
-								<div>
-									<div class="tool-desc text-16px text-primary mb-4px">{{ item.name }}</div>
-									<div class="tool-desc text-12px">{{ item.description }}</div>
-								</div>
-							</template>
-							<el-card>
-								<el-descriptions direction="vertical" :column="1">
-									<el-descriptions-item label="参数结构">
-										<CodeEditor
-											:model-value="JSON.stringify(item.inputSchema, null, 2)"
-											language="json"
-											:height="260"
-											:tools="false"
-											readonly
-										/>
-									</el-descriptions-item>
-								</el-descriptions>
-							</el-card>
-						</el-collapse-item>
-					</el-collapse>
-				</el-scrollbar>
-			</div>
-		</el-dialog>
+			</el-dialog>
+		</div>
 	</div>
 </template>
 
@@ -640,6 +655,26 @@ onMounted(() => {
 </script>
 
 <style scoped lang="less">
+.management-page {
+	padding: 24px;
+	min-height: 100%;
+	box-sizing: border-box;
+}
+
+.page-head {
+	margin-bottom: 18px;
+
+	h1 {
+		margin: 0;
+		font-size: 28px;
+	}
+
+	p {
+		margin: 6px 0 0;
+		color: #6b7280;
+	}
+}
+
 :deep(.el-collapse-item__header) {
 	height: 80px;
 }

File diff suppressed because it is too large
+ 0 - 1298
apps/web/src/views/model/ModelManage.vue


+ 0 - 40
apps/web/src/views/model/ModelPage.vue

@@ -1,40 +0,0 @@
-<template>
-	<div class="management-page">
-		<div class="page-head">
-			<div>
-				<h1>模型管理</h1>
-				<p>统一管理远程模型与本地导入到系统的模型配置。</p>
-			</div>
-		</div>
-		<ModelManage />
-	</div>
-</template>
-
-<script setup lang="ts">
-import ModelManage from './ModelManage.vue'
-</script>
-
-<style scoped lang="less">
-.management-page {
-	padding: 16px;
-	background: #f5f7fa;
-	min-height: 100%;
-	box-sizing: border-box;
-}
-
-.page-head {
-	margin-bottom: 20px;
-
-	h1 {
-		margin: 0;
-		font-size: 28px;
-		color: var(--text-primary);
-	}
-
-	p {
-		margin: 6px 0 0;
-		font-size: 14px;
-		color: var(--text-secondary);
-	}
-}
-</style>

+ 0 - 44
apps/web/src/views/model/OllamaPage.vue

@@ -1,44 +0,0 @@
-<template>
-	<div class="management-page">
-		<div class="page-head">
-			<div>
-				<h1>Ollama</h1>
-				<p>查看 Ollama 连接状态、本地模型列表,以及下载任务进度。</p>
-			</div>
-		</div>
-		<OllamaManage @open-import-model="handleOpenImport" />
-	</div>
-</template>
-
-<script setup lang="ts">
-import OllamaManage from './OllamaManage.vue'
-
-function handleOpenImport(name: string) {
-	window.dispatchEvent(new CustomEvent('open-import-ollama-model', { detail: name }))
-}
-</script>
-
-<style scoped lang="less">
-.management-page {
-	padding: 16px;
-	background: #f5f7fa;
-	min-height: 100%;
-	box-sizing: border-box;
-}
-
-.page-head {
-	margin-bottom: 20px;
-
-	h1 {
-		margin: 0;
-		font-size: 28px;
-		color: var(--text-primary);
-	}
-
-	p {
-		margin: 6px 0 0;
-		font-size: 14px;
-		color: var(--text-secondary);
-	}
-}
-</style>

File diff suppressed because it is too large
+ 1325 - 2
apps/web/src/views/model/index.vue


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

@@ -7,15 +7,6 @@ export interface OllamaModel {
 	size?: number
 }
 
-export interface DownloadTask {
-	id: string
-	modelName: string
-	progress: number
-	status: string
-	startTime: string
-	message: string
-}
-
 export interface ModelProvider {
 	value: string
 	label: string

+ 170 - 113
apps/web/src/views/model/OllamaManage.vue

@@ -1,118 +1,144 @@
-<template>
-	<div class="ollama-manage">
-		<el-row :gutter="16" class="stats-row">
-			<el-col :span="8">
-				<el-card class="stat-card" shadow="hover">
-					<div class="stat-content">
-						<div :class="['stat-icon', ollamaStatus.isSuccess ? 'online' : 'offline']">
-							<el-icon :size="24">
-								<component :is="ollamaStatus.isSuccess ? CircleCheck : CircleClose" />
-							</el-icon>
-						</div>
-						<div class="stat-info">
-							<div class="stat-label">Ollama连接状态</div>
-							<div class="stat-value">{{ ollamaStatus.isSuccess ? '已连接' : '未连接' }}</div>
-						</div>
-						<el-button type="primary" plain @click="checkOllamaStatus">刷新</el-button>
-					</div>
-				</el-card>
-			</el-col>
-			<el-col :span="8">
-				<el-card class="stat-card" shadow="hover">
-					<div class="stat-content">
-						<div class="stat-icon default">
-							<el-icon :size="24">
-								<Document />
-							</el-icon>
+<template>
+	<div class="management-page">
+		<div class="page-head">
+			<div>
+				<h1>Ollama</h1>
+				<p>查看 Ollama 连接状态、本地模型列表,以及下载任务进度。</p>
+			</div>
+		</div>
+
+		<div class="ollama-manage">
+			<el-row :gutter="16" class="stats-row">
+				<el-col :span="8">
+					<el-card class="stat-card" shadow="hover">
+						<div class="stat-content">
+							<div :class="['stat-icon', ollamaStatus.isSuccess ? 'online' : 'offline']">
+								<el-icon :size="24">
+									<component :is="ollamaStatus.isSuccess ? CircleCheck : CircleClose" />
+								</el-icon>
+							</div>
+							<div class="stat-info">
+								<div class="stat-label">Ollama连接状态</div>
+								<div class="stat-value">{{ ollamaStatus.isSuccess ? '已连接' : '未连接' }}</div>
+							</div>
+							<el-button type="primary" plain @click="checkOllamaStatus">刷新</el-button>
 						</div>
-						<div class="stat-info">
-							<div class="stat-label">已下载模型</div>
-							<div class="stat-value">{{ localModels.length }}</div>
+					</el-card>
+				</el-col>
+				<el-col :span="8">
+					<el-card class="stat-card" shadow="hover">
+						<div class="stat-content">
+							<div class="stat-icon default">
+								<el-icon :size="24">
+									<Document />
+								</el-icon>
+							</div>
+							<div class="stat-info">
+								<div class="stat-label">已下载模型</div>
+								<div class="stat-value">{{ localModels.length }}</div>
+							</div>
 						</div>
-					</div>
-				</el-card>
-			</el-col>
-			<el-col :span="8">
-				<el-card class="stat-card" shadow="hover">
-					<div class="stat-content">
-						<div class="stat-icon default">
-							<el-icon :size="24">
-								<List />
-							</el-icon>
+					</el-card>
+				</el-col>
+				<el-col :span="8">
+					<el-card class="stat-card" shadow="hover">
+						<div class="stat-content">
+							<div class="stat-icon default">
+								<el-icon :size="24">
+									<List />
+								</el-icon>
+							</div>
+							<div class="stat-info">
+								<div class="stat-label">进行中任务</div>
+								<div class="stat-value">{{ downloadingTasks }}</div>
+							</div>
 						</div>
-						<div class="stat-info">
-							<div class="stat-label">进行中任务</div>
-							<div class="stat-value">{{ downloadingTasks }}</div>
+					</el-card>
+				</el-col>
+			</el-row>
+
+			<div class="action-bar">
+				<el-button type="primary" @click="showDownloadDialog = true">
+					<el-icon>
+						<Plus />
+					</el-icon>
+					下载新模型
+				</el-button>
+				<el-button @click="handleRefresh()">
+					<el-icon>
+						<Refresh />
+					</el-icon>
+					刷新
+				</el-button>
+			</div>
+
+			<el-card class="list-card">
+				<el-table
+					v-if="localModels.length || localLoading"
+					:data="localModels"
+					v-loading="localLoading"
+					border
+				>
+					<el-table-column prop="name" label="模型名称" />
+					<el-table-column prop="size" label="大小" :formatter="formatSize" />
+					<el-table-column prop="digest" label="Digest" />
+					<el-table-column prop="modified_at" label="更新时间" />
+					<template #empty>暂无已下载的本地模型</template>
+				</el-table>
+				<el-empty v-else description="暂无已下载的本地模型" class="page-empty" />
+			</el-card>
+
+			<div class="task-title">下载任务</div>
+			<el-card class="list-card mt-4">
+				<el-table
+					v-if="sortedDownloadTasks.length"
+					:data="sortedDownloadTasks"
+					border
+					no-data-text="暂无下载任务"
+				>
+					<el-table-column prop="modelName" label="模型名称" width="200" />
+					<el-table-column prop="status" label="状态" width="120">
+						<template #default="{ row }">
+							<el-tag :type="getStatusType(row.status)">{{ formatStatus(row.status) }}</el-tag>
+						</template>
+					</el-table-column>
+					<el-table-column prop="progress" label="进度" width="200">
+						<template #default="{ row }">
+							<el-progress
+								:percentage="getPercentage(row.progress)"
+								:stroke-width="8"
+								:show-text="row.status === 'downloading'"
+							/>
+						</template>
+					</el-table-column>
+					<el-table-column prop="startTime" label="开始时间" />
+					<el-table-column prop="message" label="消息" />
+					<template #empty>暂无下载任务</template>
+				</el-table>
+				<el-empty v-else description="暂无下载任务" class="page-empty page-empty--compact" />
+			</el-card>
+
+			<el-dialog v-model="showDownloadDialog" title="下载新模型" width="500px">
+				<el-form :model="downloadForm" label-width="80px">
+					<el-form-item label="模型名称" required>
+						<el-input v-model="downloadForm.modelName" placeholder="例如: llama3.1" />
+					</el-form-item>
+					<el-form-item>
+						<div>
+							你可访问
+							<a href="https://ollama.ai/library" target="_blank">https://ollama.ai/library</a>
+							查询可用模型
 						</div>
-					</div>
-				</el-card>
-			</el-col>
-		</el-row>
-
-		<div class="action-bar">
-			<el-button type="primary" @click="showDownloadDialog = true">
-				<el-icon>
-					<Plus />
-				</el-icon> 下载新模型
-			</el-button>
-			<el-button @click="handleRefresh()">
-				<el-icon>
-					<Refresh />
-				</el-icon> 刷新
-			</el-button>
+					</el-form-item>
+				</el-form>
+				<template #footer>
+					<el-button @click="showDownloadDialog = false">取消</el-button>
+					<el-button type="primary" @click="startDownloadModel" :loading="downloadLoading"
+						>开始下载</el-button
+					>
+				</template>
+			</el-dialog>
 		</div>
-
-		<el-card class="list-card">
-			<el-table v-if="localModels.length || localLoading" :data="localModels" v-loading="localLoading" border>
-				<el-table-column prop="name" label="模型名称" />
-				<el-table-column prop="size" label="大小" :formatter="formatSize" />
-				<el-table-column prop="digest" label="Digest" />
-				<el-table-column prop="modified_at" label="更新时间" />
-				<template #empty>暂无已下载的本地模型</template>
-			</el-table>
-			<el-empty v-else description="暂无已下载的本地模型" class="page-empty" />
-		</el-card>
-
-		<div class="task-title">下载任务</div>
-		<el-card class="list-card mt-4">
-			<el-table v-if="sortedDownloadTasks.length" :data="sortedDownloadTasks" border no-data-text="暂无下载任务">
-				<el-table-column prop="modelName" label="模型名称" width="200" />
-				<el-table-column prop="status" label="状态" width="120">
-					<template #default="{ row }">
-						<el-tag :type="getStatusType(row.status)">{{ formatStatus(row.status) }}</el-tag>
-					</template>
-				</el-table-column>
-				<el-table-column prop="progress" label="进度" width="200">
-					<template #default="{ row }">
-						<el-progress :percentage="getPercentage(row.progress)" :stroke-width="8"
-							:show-text="row.status === 'downloading'" />
-					</template>
-				</el-table-column>
-				<el-table-column prop="startTime" label="开始时间" />
-				<el-table-column prop="message" label="消息" />
-				<template #empty>暂无下载任务</template>
-			</el-table>
-			<el-empty v-else description="暂无下载任务" class="page-empty page-empty--compact" />
-		</el-card>
-
-		<el-dialog v-model="showDownloadDialog" title="下载新模型" width="500px">
-			<el-form :model="downloadForm" label-width="80px">
-				<el-form-item label="模型名称" required>
-					<el-input v-model="downloadForm.modelName" placeholder="例如: llama3.1" />
-				</el-form-item>
-				<el-form-item>
-					<div>
-						你可访问
-						<a href="https://ollama.ai/library" target="_blank">https://ollama.ai/library</a>
-						查询可用模型
-					</div>
-				</el-form-item>
-			</el-form>
-			<template #footer>
-				<el-button @click="showDownloadDialog = false">取消</el-button>
-				<el-button type="primary" @click="startDownloadModel" :loading="downloadLoading">开始下载</el-button>
-			</template>
-		</el-dialog>
 	</div>
 </template>
 
@@ -138,11 +164,15 @@ const downloadForm = reactive({ modelName: '' })
 let progressTimer: number | null = null
 let pollingActive = false
 
-const downloadingTasks = computed(() => downloadTasks.value.filter((t) => t.status === 'downloading').length)
+const downloadingTasks = computed(
+	() => downloadTasks.value.filter((t) => t.status === 'downloading').length
+)
 const sortedDownloadTasks = computed(() => {
 	return [...downloadTasks.value].sort((a, b) => {
 		const order = { downloading: 0, failed: 1, success: 2 }
-		return (order[a.status as keyof typeof order] ?? 3) - (order[b.status as keyof typeof order] ?? 3)
+		return (
+			(order[a.status as keyof typeof order] ?? 3) - (order[b.status as keyof typeof order] ?? 3)
+		)
 	})
 })
 
@@ -275,9 +305,36 @@ onMounted(async () => {
 onUnmounted(() => {
 	stopProgressPoll()
 })
+
+function handleOpenImport(name: string) {
+	window.dispatchEvent(new CustomEvent('open-import-ollama-model', { detail: name }))
+}
 </script>
 
-<style lang="less" scoped>
+<style scoped lang="less">
+.management-page {
+	padding: 16px;
+	background: #f5f7fa;
+	min-height: 100%;
+	box-sizing: border-box;
+}
+
+.page-head {
+	margin-bottom: 20px;
+
+	h1 {
+		margin: 0;
+		font-size: 28px;
+		color: var(--text-primary);
+	}
+
+	p {
+		margin: 6px 0 0;
+		font-size: 14px;
+		color: var(--text-secondary);
+	}
+}
+
 .ollama-manage {
 	width: 100%;
 }

+ 15 - 0
apps/web/src/views/ollama/types.d.ts

@@ -0,0 +1,15 @@
+export interface OllamaModel {
+	name?: string
+	digest?: string
+	modified_at?: string
+	size?: number
+}
+
+export interface DownloadTask {
+	id: string
+	modelName: string
+	progress: number
+	status: string
+	startTime: string
+	message: string
+}

+ 171 - 138
apps/web/src/views/resource/components/PromptTemplatePanel.vue

@@ -1,158 +1,171 @@
 <template>
-	<div class="panel">
-		<div class="toolbar">
-			<div class="toolbar-left">
-				<el-input
-					v-model="keyword"
-					clearable
-					placeholder="搜索提示词名称 / 描述"
-					class="search-input"
-					@keyup.enter="loadList(1)"
-				>
-					<template #prefix>
-						<el-icon>
-							<Search />
-						</el-icon>
-					</template>
-				</el-input>
-				<el-select
-					v-model="type"
-					clearable
-					placeholder="类型"
-					style="width: 160px"
-					:options="typeList"
-					@change="loadList(1)"
-				></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-button>
+	<div class="management-page">
+		<div class="page-head">
+			<div>
+				<h1>提示词</h1>
+				<p>管理系统提示词、上下文模板、改写与兜底等资源配置。</p>
 			</div>
 		</div>
 
-		<div v-loading="loading" class="card-grid">
-			<el-empty v-if="!visibleList.length && !loading" description="暂无提示词" class="empty" />
-			<div v-for="item in visibleList" :key="item.id" class="resource-card">
-				<div class="resource-card__top">
-					<div>
-						<div class="resource-card__title flex items-center gap-4px">
-							<el-tag v-if="item.is_builtin" type="success" effect="light">内置</el-tag
-							>{{ item.name }}
-						</div>
-
-						<div class="resource-card__desc">{{ item.description || '暂无描述' }}</div>
-					</div>
-					<el-dropdown :hide-on-click="false">
-						<span class="cursor-pointer">
+		<div class="panel">
+			<div class="toolbar">
+				<div class="toolbar-left">
+					<el-input
+						v-model="keyword"
+						clearable
+						placeholder="搜索提示词名称 / 描述"
+						class="search-input"
+						@keyup.enter="loadList(1)"
+					>
+						<template #prefix>
 							<el-icon>
-								<MoreFilled />
+								<Search />
 							</el-icon>
-						</span>
-						<template #dropdown>
-							<el-dropdown-menu>
-								<el-dropdown-item>
-									<el-button link type="primary" @click="openDetail(item)">详情</el-button>
-								</el-dropdown-item>
-								<el-dropdown-item>
-									<el-button link type="primary" @click="openEdit(item)">编辑</el-button>
-								</el-dropdown-item>
-								<el-dropdown-item>
-									<el-button link type="danger" @click="removeItem(item.id)">删除</el-button>
-								</el-dropdown-item>
-							</el-dropdown-menu>
 						</template>
-					</el-dropdown>
+					</el-input>
+					<el-select
+						v-model="type"
+						clearable
+						placeholder="类型"
+						style="width: 160px"
+						:options="typeList"
+						@change="loadList(1)"
+					></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-button>
 				</div>
-				<div class="resource-card__meta">
-					<span>类型:{{ formatType(item.type) }}</span>
+			</div>
+
+			<div v-loading="loading" class="card-grid">
+				<el-empty v-if="!visibleList.length && !loading" description="暂无提示词" class="empty" />
+				<div v-for="item in visibleList" :key="item.id" class="resource-card">
+					<div class="resource-card__top">
+						<div>
+							<div class="resource-card__title flex items-center gap-4px">
+								<el-tag v-if="item.is_builtin" type="success" effect="light">内置</el-tag
+								>{{ item.name }}
+							</div>
+
+							<div class="resource-card__desc">{{ item.description || '暂无描述' }}</div>
+						</div>
+						<el-dropdown :hide-on-click="false">
+							<span class="cursor-pointer">
+								<el-icon>
+									<MoreFilled />
+								</el-icon>
+							</span>
+							<template #dropdown>
+								<el-dropdown-menu>
+									<el-dropdown-item>
+										<el-button link type="primary" @click="openDetail(item)">详情</el-button>
+									</el-dropdown-item>
+									<el-dropdown-item>
+										<el-button link type="primary" @click="openEdit(item)">编辑</el-button>
+									</el-dropdown-item>
+									<el-dropdown-item>
+										<el-button link type="danger" @click="removeItem(item.id)">删除</el-button>
+									</el-dropdown-item>
+								</el-dropdown-menu>
+							</template>
+						</el-dropdown>
+					</div>
+					<div class="resource-card__meta">
+						<span>类型:{{ formatType(item.type) }}</span>
+					</div>
+					<div class="resource-card__content">{{ shortText(item.content) }}</div>
 				</div>
-				<div class="resource-card__content">{{ shortText(item.content) }}</div>
 			</div>
-		</div>
 
-		<div v-if="pagination.totalCount > 0" 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"
-			/>
-		</div>
+			<div v-if="pagination.totalCount > 0" 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"
+				/>
+			</div>
 
-		<el-drawer
-			v-model="drawerVisible"
-			:title="currentId ? '编辑提示词' : '新建提示词'"
-			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" />
-				</el-form-item>
-				<el-form-item label="描述" prop="description">
-					<el-input v-model="form.description" type="textarea" :rows="2" />
-				</el-form-item>
-				<el-form-item label="内容" prop="content">
-					<el-input v-model="form.content" type="textarea" :rows="8" />
-				</el-form-item>
-				<el-form-item label="用户提示词" prop="user">
-					<el-input v-model="form.user" type="textarea" :rows="4" />
-				</el-form-item>
-				<div class="grid-2">
-					<el-form-item label="类型" prop="type">
-						<el-select v-model="form.type" style="width: 100%">
-							<el-option label="系统提示词" value="system-prompt" />
-							<el-option label="agent 系统提示词" value="agent-system-prompt" />
-							<el-option label="改写提示词" value="rewrite" />
-							<el-option label="回退提示词" value="fallback" />
-							<el-option label="上下文模板" value="context-template" />
-						</el-select>
+			<el-drawer
+				v-model="drawerVisible"
+				:title="currentId ? '编辑提示词' : '新建提示词'"
+				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" />
 					</el-form-item>
-					<el-form-item label="是否默认" prop="is_default">
-						<el-switch v-model="form.is_default" />
+					<el-form-item label="描述" prop="description">
+						<el-input v-model="form.description" type="textarea" :rows="2" />
 					</el-form-item>
-				</div>
-				<div class="grid-2">
-					<el-form-item label="包含知识库" prop="has_knowledge_base">
-						<el-switch v-model="form.has_knowledge_base" />
+					<el-form-item label="内容" prop="content">
+						<el-input v-model="form.content" type="textarea" :rows="8" />
 					</el-form-item>
-					<el-form-item label="包含网络搜索" prop="has_web_search">
-						<el-switch v-model="form.has_web_search" />
+					<el-form-item label="用户提示词" prop="user">
+						<el-input v-model="form.user" type="textarea" :rows="4" />
 					</el-form-item>
-				</div>
-			</el-form>
-			<template #footer>
-				<div class="drawer-footer">
-					<el-button @click="drawerVisible = false">取消</el-button>
-					<el-button type="primary" :loading="submitLoading" @click="handleSubmit">保存</el-button>
-				</div>
-			</template>
-		</el-drawer>
-
-		<el-drawer v-model="detailVisible" title="提示词详情" width="760px">
-			<el-descriptions v-if="detailItem" :column="1" direction="vertical">
-				<el-descriptions-item label="名称">{{ detailItem.name }}</el-descriptions-item>
-				<el-descriptions-item label="类型">{{ formatType(detailItem.type) }}</el-descriptions-item>
-				<el-descriptions-item label="描述">{{
-					detailItem.description || '-'
-				}}</el-descriptions-item>
-				<el-descriptions-item label="内容">
-					<div class="w-full p-12rpx">
-						<Markdown :content="detailItem.content" />
+					<div class="grid-2">
+						<el-form-item label="类型" prop="type">
+							<el-select v-model="form.type" style="width: 100%">
+								<el-option label="系统提示词" value="system-prompt" />
+								<el-option label="agent 系统提示词" value="agent-system-prompt" />
+								<el-option label="改写提示词" value="rewrite" />
+								<el-option label="回退提示词" value="fallback" />
+								<el-option label="上下文模板" value="context-template" />
+							</el-select>
+						</el-form-item>
+						<el-form-item label="是否默认" prop="is_default">
+							<el-switch v-model="form.is_default" />
+						</el-form-item>
 					</div>
-				</el-descriptions-item>
-			</el-descriptions>
-		</el-drawer>
+					<div class="grid-2">
+						<el-form-item label="包含知识库" prop="has_knowledge_base">
+							<el-switch v-model="form.has_knowledge_base" />
+						</el-form-item>
+						<el-form-item label="包含网络搜索" prop="has_web_search">
+							<el-switch v-model="form.has_web_search" />
+						</el-form-item>
+					</div>
+				</el-form>
+				<template #footer>
+					<div class="drawer-footer">
+						<el-button @click="drawerVisible = false">取消</el-button>
+						<el-button type="primary" :loading="submitLoading" @click="handleSubmit"
+							>保存</el-button
+						>
+					</div>
+				</template>
+			</el-drawer>
+
+			<el-drawer v-model="detailVisible" title="提示词详情" width="760px">
+				<el-descriptions v-if="detailItem" :column="1" direction="vertical">
+					<el-descriptions-item label="名称">{{ detailItem.name }}</el-descriptions-item>
+					<el-descriptions-item label="类型">{{
+						formatType(detailItem.type)
+					}}</el-descriptions-item>
+					<el-descriptions-item label="描述">{{
+						detailItem.description || '-'
+					}}</el-descriptions-item>
+					<el-descriptions-item label="内容">
+						<div class="w-full p-12rpx">
+							<Markdown :content="detailItem.content" />
+						</div>
+					</el-descriptions-item>
+				</el-descriptions>
+			</el-drawer>
+		</div>
 	</div>
 </template>
 
@@ -347,6 +360,26 @@ onMounted(() => {
 </script>
 
 <style scoped lang="less">
+.management-page {
+	padding: 24px;
+	min-height: 100%;
+	box-sizing: border-box;
+}
+
+.page-head {
+	margin-bottom: 18px;
+
+	h1 {
+		margin: 0;
+		font-size: 28px;
+	}
+
+	p {
+		margin: 6px 0 0;
+		color: #6b7280;
+	}
+}
+
 .panel {
 	display: flex;
 	flex-direction: column;

+ 0 - 37
apps/web/src/views/resource/McpPage.vue

@@ -1,37 +0,0 @@
-<template>
-	<div class="management-page">
-		<div class="page-head">
-			<div>
-				<h1>MCP 服务</h1>
-				<p>维护 MCP 服务列表,并查看资源与工具详情。</p>
-			</div>
-		</div>
-		<McpPanel />
-	</div>
-</template>
-
-<script setup lang="ts">
-import McpPanel from './components/McpPanel.vue'
-</script>
-
-<style scoped lang="less">
-.management-page {
-	padding: 24px;
-	min-height: 100%;
-	box-sizing: border-box;
-}
-
-.page-head {
-	margin-bottom: 18px;
-
-	h1 {
-		margin: 0;
-		font-size: 28px;
-	}
-
-	p {
-		margin: 6px 0 0;
-		color: #6b7280;
-	}
-}
-</style>

+ 0 - 37
apps/web/src/views/resource/PromptPage.vue

@@ -1,37 +0,0 @@
-<template>
-	<div class="management-page">
-		<div class="page-head">
-			<div>
-				<h1>提示词</h1>
-				<p>管理系统提示词、上下文模板、改写与兜底等资源配置。</p>
-			</div>
-		</div>
-		<PromptTemplatePanel />
-	</div>
-</template>
-
-<script setup lang="ts">
-import PromptTemplatePanel from './components/PromptTemplatePanel.vue'
-</script>
-
-<style scoped lang="less">
-.management-page {
-	padding: 24px;
-	min-height: 100%;
-	box-sizing: border-box;
-}
-
-.page-head {
-	margin-bottom: 18px;
-
-	h1 {
-		margin: 0;
-		font-size: 28px;
-	}
-
-	p {
-		margin: 6px 0 0;
-		color: #6b7280;
-	}
-}
-</style>

+ 0 - 37
apps/web/src/views/resource/SkillsPage.vue

@@ -1,37 +0,0 @@
-<template>
-	<div class="management-page">
-		<div class="page-head">
-			<div>
-				<h1>Skills 技能</h1>
-				<p>查看当前系统可用的 Skills 列表,供智能体配置时选择。</p>
-			</div>
-		</div>
-		<SkillsPanel />
-	</div>
-</template>
-
-<script setup lang="ts">
-import SkillsPanel from './components/SkillsPanel.vue'
-</script>
-
-<style scoped lang="less">
-.management-page {
-	padding: 24px;
-	min-height: 100%;
-	box-sizing: border-box;
-}
-
-.page-head {
-	margin-bottom: 18px;
-
-	h1 {
-		margin: 0;
-		font-size: 28px;
-	}
-
-	p {
-		margin: 6px 0 0;
-		color: #6b7280;
-	}
-}
-</style>

+ 0 - 37
apps/web/src/views/resource/StoragePage.vue

@@ -1,37 +0,0 @@
-<template>
-	<div class="management-page">
-		<div class="page-head">
-			<div>
-				<h1>存储引擎</h1>
-				<p>管理对象存储引擎配置,支持保存与连通测试。</p>
-			</div>
-		</div>
-		<StorageManager />
-	</div>
-</template>
-
-<script setup lang="ts">
-import StorageManager from './components/StorageManager.vue'
-</script>
-
-<style scoped lang="less">
-.management-page {
-	padding: 24px;
-	min-height: 100%;
-	box-sizing: border-box;
-}
-
-.page-head {
-	margin-bottom: 18px;
-
-	h1 {
-		margin: 0;
-		font-size: 28px;
-	}
-
-	p {
-		margin: 6px 0 0;
-		color: #6b7280;
-	}
-}
-</style>

+ 0 - 37
apps/web/src/views/resource/WebSearchPage.vue

@@ -1,37 +0,0 @@
-<template>
-	<div class="management-page">
-		<div class="page-head">
-			<div>
-				<h1>网络搜索</h1>
-				<p>管理搜索引擎接入、参数配置与连通测试。</p>
-			</div>
-		</div>
-		<WebSearchPanel />
-	</div>
-</template>
-
-<script setup lang="ts">
-import WebSearchPanel from './components/WebSearchPanel.vue'
-</script>
-
-<style scoped lang="less">
-.management-page {
-	padding: 24px;
-	min-height: 100%;
-	box-sizing: border-box;
-}
-
-.page-head {
-	margin-bottom: 18px;
-
-	h1 {
-		margin: 0;
-		font-size: 28px;
-	}
-
-	p {
-		margin: 6px 0 0;
-		color: #6b7280;
-	}
-}
-</style>

+ 0 - 7
apps/web/src/views/resource/index.vue

@@ -1,7 +0,0 @@
-<template>
-	<PromptPage />
-</template>
-
-<script setup lang="ts">
-import PromptPage from './PromptPage.vue'
-</script>

+ 43 - 20
apps/web/src/views/resource/components/SkillsPanel.vue

@@ -1,27 +1,31 @@
 <template>
-	<div class="skills-panel">
-		<div class="toolbar">
-			<el-input
-				v-model="keyword"
-				clearable
-				placeholder="搜索技能名称或描述"
-				class="search-input"
-			>
-				<template #prefix>
-					<el-icon><Search /></el-icon>
-				</template>
-			</el-input>
-			<el-button @click="loadSkills" :loading="loading">刷新</el-button>
+	<div class="management-page">
+		<div class="page-head">
+			<div>
+				<h1>Skills 技能</h1>
+				<p>查看当前系统可用的 Skills 列表,供智能体配置时选择。</p>
+			</div>
 		</div>
 
-		<div v-loading="loading" class="card-grid">
-			<el-empty v-if="!visibleList.length && !loading" description="暂无 Skills" class="empty" />
-			<div v-for="item in visibleList" :key="item.name" class="skill-card">
-				<div class="skill-card__top">
-					<div class="skill-card__title">{{ item.name }}</div>
-					<el-tag effect="plain" type="info">Skill</el-tag>
+		<div class="skills-panel">
+			<div class="toolbar">
+				<el-input v-model="keyword" clearable placeholder="搜索技能名称或描述" class="search-input">
+					<template #prefix>
+						<el-icon><Search /></el-icon>
+					</template>
+				</el-input>
+				<el-button @click="loadSkills" :loading="loading">刷新</el-button>
+			</div>
+
+			<div v-loading="loading" class="card-grid">
+				<el-empty v-if="!visibleList.length && !loading" description="暂无 Skills" class="empty" />
+				<div v-for="item in visibleList" :key="item.name" class="skill-card">
+					<div class="skill-card__top">
+						<div class="skill-card__title">{{ item.name }}</div>
+						<el-tag effect="plain" type="info">Skill</el-tag>
+					</div>
+					<div class="skill-card__desc">{{ item.description || '暂无描述' }}</div>
 				</div>
-				<div class="skill-card__desc">{{ item.description || '暂无描述' }}</div>
 			</div>
 		</div>
 	</div>
@@ -65,6 +69,25 @@ onMounted(() => {
 </script>
 
 <style scoped lang="less">
+.management-page {
+	padding: 24px;
+	min-height: 100%;
+	box-sizing: border-box;
+}
+
+.page-head {
+	margin-bottom: 18px;
+
+	h1 {
+		margin: 0;
+		font-size: 28px;
+	}
+
+	p {
+		margin: 6px 0 0;
+		color: #6b7280;
+	}
+}
 .skills-panel {
 	display: flex;
 	flex-direction: column;

+ 273 - 244
apps/web/src/views/resource/components/StorageManager.vue

@@ -1,259 +1,268 @@
 <template>
-	<div class="storage-manager" v-loading="pageLoading">
-		<div class="storage-actions">
-			<el-button type="primary" :loading="initLoading" @click="handleInit">初始化存储</el-button>
-			<el-button :loading="engineLoading" @click="loadEngines">刷新列表</el-button>
+	<div class="management-page">
+		<div class="page-head">
+			<div>
+				<h1>存储引擎</h1>
+				<p>管理对象存储引擎配置,支持保存与连通测试。</p>
+			</div>
 		</div>
 
-		<div 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 class="actions">
-							<el-dropdown>
-								<span class="actions-trigger">
-									<el-icon>
-										<MoreFilled />
-									</el-icon>
-								</span>
-								<template #dropdown>
-									<el-dropdown-menu>
-										<el-dropdown-item
-											:disabled="currentDefaultProvider === item.name"
-											@click="updateDefaultProvider(item.name)"
-										>
-											{{ currentDefaultProvider === item.name ? '当前默认' : '设为默认' }}
-										</el-dropdown-item>
-										<el-dropdown-item @click="openEditProvider(item.name)">编辑</el-dropdown-item>
-									</el-dropdown-menu>
-								</template>
-							</el-dropdown>
+		<div class="storage-manager" v-loading="pageLoading">
+			<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="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 class="actions">
+								<el-dropdown>
+									<span class="actions-trigger">
+										<el-icon>
+											<MoreFilled />
+										</el-icon>
+									</span>
+									<template #dropdown>
+										<el-dropdown-menu>
+											<el-dropdown-item
+												:disabled="currentDefaultProvider === item.name"
+												@click="updateDefaultProvider(item.name)"
+											>
+												{{ currentDefaultProvider === item.name ? '当前默认' : '设为默认' }}
+											</el-dropdown-item>
+											<el-dropdown-item @click="openEditProvider(item.name)">编辑</el-dropdown-item>
+										</el-dropdown-menu>
+									</template>
+								</el-dropdown>
+							</div>
 						</div>
 					</div>
-				</div>
-
-				<div class="tags">
-					<el-tag
-						v-if="currentDefaultProvider === item.name"
-						class="default-provider-tag"
-						effect="light"
-					>
-						默认
-					</el-tag>
-					<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>
-			<el-empty v-if="!engines.length" class="empty" description="暂无引擎" />
-		</div>
 
-		<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 class="tags">
+						<el-tag
+							v-if="currentDefaultProvider === item.name"
+							class="default-provider-tag"
+							effect="light"
+						>
+							默认
+						</el-tag>
+						<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>
+				<el-empty v-if="!engines.length" class="empty" description="暂无引擎" />
+			</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 === 'ks3'">
-						<div class="form-grid">
-							<el-form-item label="Endpoint">
-								<el-input v-model="form.ks3.endpoint" />
-							</el-form-item>
-							<el-form-item label="Region">
-								<el-input v-model="form.ks3.region" />
-							</el-form-item>
-							<el-form-item label="Access Key">
-								<el-input v-model="form.ks3.access_key" />
-							</el-form-item>
-							<el-form-item label="Secret Key">
-								<el-input v-model="form.ks3.secret_key" show-password />
-							</el-form-item>
-							<el-form-item label="Bucket">
-								<el-input v-model="form.ks3.bucket_name" />
-							</el-form-item>
-							<el-form-item label="Path Prefix">
-								<el-input v-model="form.ks3.path_prefix" />
-							</el-form-item>
+			<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>
-					</template>
+					</div>
 
-					<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 :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 === 'ks3'">
+							<div class="form-grid">
+								<el-form-item label="Endpoint">
+									<el-input v-model="form.ks3.endpoint" />
+								</el-form-item>
+								<el-form-item label="Region">
+									<el-input v-model="form.ks3.region" />
+								</el-form-item>
+								<el-form-item label="Access Key">
+									<el-input v-model="form.ks3.access_key" />
+								</el-form-item>
+								<el-form-item label="Secret Key">
+									<el-input v-model="form.ks3.secret_key" show-password />
+								</el-form-item>
+								<el-form-item label="Bucket">
+									<el-input v-model="form.ks3.bucket_name" />
+								</el-form-item>
+								<el-form-item label="Path Prefix">
+									<el-input v-model="form.ks3.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-item label="设为默认">
+							<el-switch v-model="form.is_default" />
 						</el-form-item>
-					</template>
-
-					<el-form-item label="设为默认">
-						<el-switch v-model="form.is_default" />
-					</el-form-item>
-				</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>
+					</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>
 	</div>
 </template>
 
@@ -703,6 +712,26 @@ onMounted(async () => {
 </script>
 
 <style scoped lang="less">
+.management-page {
+	padding: 24px;
+	min-height: 100%;
+	box-sizing: border-box;
+}
+
+.page-head {
+	margin-bottom: 18px;
+
+	h1 {
+		margin: 0;
+		font-size: 28px;
+	}
+
+	p {
+		margin: 6px 0 0;
+		color: #6b7280;
+	}
+}
+
 .storage-manager {
 	display: flex;
 	flex-direction: column;

+ 167 - 97
apps/web/src/views/resource/components/WebSearchPanel.vue

@@ -1,116 +1,166 @@
 <template>
-	<div class="panel">
-		<div class="toolbar">
-			<div class="toolbar-left">
-				<el-input v-model="keyword" clearable placeholder="搜索提供者名称" class="search-input" @keyup.enter="loadList(1)">
-					<template #prefix>
+	<div class="management-page">
+		<div class="page-head">
+			<div>
+				<h1>网络搜索</h1>
+				<p>管理搜索引擎接入、参数配置与连通测试。</p>
+			</div>
+		</div>
+
+		<div class="panel">
+			<div class="toolbar">
+				<div class="toolbar-left">
+					<el-input
+						v-model="keyword"
+						clearable
+						placeholder="搜索提供者名称"
+						class="search-input"
+						@keyup.enter="loadList(1)"
+					>
+						<template #prefix>
+							<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>
+					<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>
-							<Search />
+							<Plus />
 						</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>
-				<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-button>
+						新建网络搜索
+					</el-button>
+				</div>
 			</div>
-		</div>
 
-		<div v-loading="loading" class="grid">
-			<el-empty v-if="!list.length && !loading" class="empty" description="暂无网络搜索配置" />
-			<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 v-loading="loading" class="grid">
+				<el-empty v-if="!list.length && !loading" class="empty" description="暂无网络搜索配置" />
+				<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>
 
-					<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 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>
 
-				<div class="card-footer">
-					<el-button link type="primary" @click="checkItem(row.id)">连接测试</el-button>
+					<div class="card-footer">
+						<el-button link type="primary" @click="checkItem(row.id)">连接测试</el-button>
+					</div>
 				</div>
 			</div>
-		</div>
 
-		<div v-if="pagination.totalCount > 0" 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" />
-		</div>
+			<div v-if="pagination.totalCount > 0" 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"
+				/>
+			</div>
 
-		<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-select>
-				</el-form-item>
-				<el-form-item label="名称" prop="name">
-					<el-input v-model="form.name" />
-				</el-form-item>
-				<el-form-item label="描述" prop="description">
-					<el-input v-model="form.description" type="textarea" :rows="2" />
-				</el-form-item>
-				<el-form-item label="API Key" prop="parameters.api_key">
-					<el-input v-model="form.parameters.api_key" type="password" show-password />
-				</el-form-item>
-				<div class="grid-2">
-					<el-form-item label="代理地址" prop="parameters.proxy_url">
-						<el-input v-model="form.parameters.proxy_url" />
+			<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-select>
 					</el-form-item>
-					<el-form-item label="引擎 ID" prop="parameters.engine_id">
-						<el-input v-model="form.parameters.engine_id" />
+					<el-form-item label="名称" prop="name">
+						<el-input v-model="form.name" />
 					</el-form-item>
-				</div>
-				<el-form-item label="设为默认" prop="is_default">
-					<el-switch v-model="form.is_default" />
-				</el-form-item>
-				<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-form-item label="描述" prop="description">
+						<el-input v-model="form.description" type="textarea" :rows="2" />
+					</el-form-item>
+					<el-form-item label="API Key" prop="parameters.api_key">
+						<el-input v-model="form.parameters.api_key" type="password" show-password />
+					</el-form-item>
+					<div class="grid-2">
+						<el-form-item label="代理地址" prop="parameters.proxy_url">
+							<el-input v-model="form.parameters.proxy_url" />
+						</el-form-item>
+						<el-form-item label="引擎 ID" prop="parameters.engine_id">
+							<el-input v-model="form.parameters.engine_id" />
+						</el-form-item>
 					</div>
-				</el-form-item>
-			</el-form>
-			<template #footer>
-				<div class="drawer-footer">
-					<el-button @click="drawerVisible = false">取消</el-button>
-					<el-button type="primary" :loading="submitLoading" @click="handleSubmit">保存</el-button>
-				</div>
-			</template>
-		</el-drawer>
+					<el-form-item label="设为默认" prop="is_default">
+						<el-switch v-model="form.is_default" />
+					</el-form-item>
+					<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
+							/>
+						</div>
+					</el-form-item>
+				</el-form>
+				<template #footer>
+					<div class="drawer-footer">
+						<el-button @click="drawerVisible = false">取消</el-button>
+						<el-button type="primary" :loading="submitLoading" @click="handleSubmit"
+							>保存</el-button
+						>
+					</div>
+				</template>
+			</el-drawer>
+		</div>
 	</div>
 </template>
 
@@ -327,6 +377,26 @@ onMounted(async () => {
 </script>
 
 <style scoped lang="less">
+.management-page {
+	padding: 24px;
+	min-height: 100%;
+	box-sizing: border-box;
+}
+
+.page-head {
+	margin-bottom: 18px;
+
+	h1 {
+		margin: 0;
+		font-size: 28px;
+	}
+
+	p {
+		margin: 6px 0 0;
+		color: #6b7280;
+	}
+}
+
 .panel {
 	display: flex;
 	flex-direction: column;