jiaxing.liao пре 2 недеља
родитељ
комит
e85f7869ff
28 измењених фајлова са 1298 додато и 272 уклоњено
  1. 1 0
      apps/web/auto-imports.d.ts
  2. 0 2
      apps/web/components.d.ts
  3. 34 17
      apps/web/src/components/Sidebar/index.vue
  4. 10 10
      apps/web/src/config/menu.ts
  5. 111 76
      apps/web/src/features/PromptModal.vue
  6. 33 3
      apps/web/src/features/createModal/index.vue
  7. 17 7
      apps/web/src/i18n/locales/en-us.ts
  8. 13 4
      apps/web/src/i18n/locales/zh-cn.ts
  9. 3 1
      apps/web/src/nodes/i18n.ts
  10. 184 0
      apps/web/src/nodes/src/ai-agent/index.ts
  11. 409 0
      apps/web/src/nodes/src/ai-agent/setter.vue
  12. 2 0
      apps/web/src/nodes/src/index.ts
  13. 2 1
      apps/web/src/nodes/src/llm/setter.vue
  14. 4 0
      apps/web/src/nodes/src/question-classifier/index.ts
  15. 63 22
      apps/web/src/nodes/src/question-classifier/setter.vue
  16. 5 5
      apps/web/src/router/index.ts
  17. 24 8
      apps/web/src/views/Dashboard.vue
  18. 122 20
      apps/web/src/views/FlowManagement.vue
  19. 18 7
      apps/web/src/views/agent/components/EditModal.vue
  20. 2 2
      apps/web/src/views/chat/api/chat.api.ts
  21. 3 2
      apps/web/src/views/chat/composables/useChatStream.ts
  22. 27 32
      apps/web/src/views/chat/index.vue
  23. 71 6
      apps/web/src/views/knowledge/DocumentManage.vue
  24. 97 30
      apps/web/src/views/resource/components/McpPanel.vue
  25. 34 12
      apps/web/src/views/resource/components/PromptTemplatePanel.vue
  26. 1 1
      packages/api-client/src/request.ts
  27. 4 1
      packages/api-service/schema/resource.openapi.json
  28. 4 3
      packages/api-service/servers/resource/api/resource.ts

+ 1 - 0
apps/web/auto-imports.d.ts

@@ -8,6 +8,7 @@ export {}
 declare global {
   const EffectScope: typeof import('vue').EffectScope
   const ElMessage: typeof import('element-plus/es').ElMessage
+  const ElMessageBox: typeof import('element-plus/es').ElMessageBox
   const computed: typeof import('vue').computed
   const createApp: typeof import('vue').createApp
   const customRef: typeof import('vue').customRef

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

@@ -78,7 +78,6 @@ declare module 'vue' {
     ElTree: typeof import('element-plus/es')['ElTree']
     ElUpload: typeof import('element-plus/es')['ElUpload']
     ExecutionChart: typeof import('./src/components/Chart/ExecutionChart.vue')['default']
-    FMarkdown: typeof import('./src/components/Chat/FMarkdown.vue')['default']
     MarkdownEditor: typeof import('./src/components/MarkdownEditor/index.vue')['default']
     MessageList: typeof import('./src/components/Chat/MessageList.vue')['default']
     RichEditor: typeof import('./src/components/RichEditor/index.vue')['default']
@@ -163,7 +162,6 @@ declare global {
   const ElTree: typeof import('element-plus/es')['ElTree']
   const ElUpload: typeof import('element-plus/es')['ElUpload']
   const ExecutionChart: typeof import('./src/components/Chart/ExecutionChart.vue')['default']
-  const FMarkdown: typeof import('./src/components/Chat/FMarkdown.vue')['default']
   const MarkdownEditor: typeof import('./src/components/MarkdownEditor/index.vue')['default']
   const MessageList: typeof import('./src/components/Chat/MessageList.vue')['default']
   const RichEditor: typeof import('./src/components/RichEditor/index.vue')['default']

+ 34 - 17
apps/web/src/components/Sidebar/index.vue

@@ -4,11 +4,14 @@
 			<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'
-			}">
-				<el-dropdown placement="bottom-start" trigger="click">
+			<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" />
 					</span>
@@ -22,8 +25,8 @@
 							</el-dropdown-item>
 						</el-dropdown-menu>
 					</template>
-				</el-dropdown>
-				<el-tooltip placement="bottom">
+				</el-dropdown> -->
+				<!-- <el-tooltip placement="bottom">
 					<template #content>
 						<div class="tooltip-keys">
 							<kbd>Ctrl</kbd>
@@ -35,7 +38,7 @@
 					<span style="cursor: pointer" @click="showSearchDialog = true">
 						<SvgIcon name="Search" />
 					</span>
-				</el-tooltip>
+				</el-tooltip> -->
 				<el-tooltip placement="bottom">
 					<template #content>
 						<div class="tooltip-keys">
@@ -94,19 +97,33 @@
 				</el-tooltip>
 				<SvgIcon v-else :name="item.icon" />
 				<span v-if="!collapsed" class="label">{{ item.label }}</span>
-				<SvgIcon v-if="!collapsed && item.type === 'action' && item.showChevron" name="chevron-right" />
+				<SvgIcon
+					v-if="!collapsed && item.type === 'action' && item.showChevron"
+					name="chevron-right"
+				/>
 			</div>
 		</div>
 	</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">
@@ -303,20 +320,20 @@ const toggleTheme = () => {
 	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);
 }
@@ -442,7 +459,7 @@ const toggleTheme = () => {
 	flex-direction: column;
 }
 
-.sidebar.collapsed .top-icons>span {
+.sidebar.collapsed .top-icons > span {
 	width: 100%;
 	justify-content: center;
 }

+ 10 - 10
apps/web/src/config/menu.ts

@@ -37,26 +37,20 @@ export const getSidebarMainMenu = (t: TranslateFn): SidebarMainMenuItem[] => [
 		icon: 'workflow',
 		label: t('sidebar.menu.orchestration')
 	},
-	{
-		path: '/agent',
-		icon: 'platForm',
-		label: t('sidebar.menu.management')
-	},
 	{
 		path: '/knowledge',
 		icon: 'book',
 		label: t('sidebar.menu.knowledge')
 	},
 	{
-		path: '/execution',
-		icon: 'play',
-		label: t('sidebar.menu.execution')
+		path: '/agent',
+		icon: 'platForm',
+		label: t('sidebar.menu.management')
 	},
 	{
 		path: '/chat',
 		icon: 'chatMessage',
-		label: t('sidebar.menu.chat'),
-		badge: 'beta'
+		label: t('sidebar.menu.chat')
 	}
 ]
 
@@ -116,6 +110,12 @@ export const getSidebarBottomMenu = (t: TranslateFn): SidebarBottomMenuItem[] =>
 		icon: 'line',
 		label: t('sidebar.menu.statistics')
 	},
+	{
+		type: 'route',
+		path: '/execution',
+		icon: 'play',
+		label: t('sidebar.menu.execution')
+	},
 	{
 		type: 'action',
 		action: 'settings',

+ 111 - 76
apps/web/src/features/PromptModal.vue

@@ -1,14 +1,30 @@
 <template>
-	<el-dialog v-model="visible" title="选择提示词模版" width="960px" append-to-body>
+	<el-dialog v-model="visible" title="提示词模版选择" width="960px" append-to-body>
 		<div class="prompt-template-dialog">
-			<div class="prompt-template-dialog__meta">
-				<span>{{ dialogTitle }}</span>
-				<el-button text :icon="Refresh" :loading="loading" @click="loadTemplates"> 刷新 </el-button>
+			<div class="prompt-template-toolbar">
+				<el-input
+					v-model="keyword"
+					clearable
+					placeholder="搜索提示词名称 / 描述"
+					class="prompt-template-search"
+					@keyup.enter="loadTemplates(1)"
+				/>
+				<el-select
+					v-model="typeFilter"
+					clearable
+					placeholder="类型"
+					style="width: 180px"
+					:options="typeList"
+					@change="loadTemplates(1)"
+				/>
+				<el-button @click="loadTemplates(1)">查询</el-button>
+				<el-button @click="handleReset">重置</el-button>
+				<el-button text :icon="Refresh" :loading="loading" @click="loadTemplates()">刷新</el-button>
 			</div>
 			<div v-loading="loading" class="prompt-template-grid">
-				<el-empty v-if="!options.length && !loading" description="暂无可用模版" />
+				<el-empty v-if="!list.length && !loading" description="暂无可用模版" />
 				<div
-					v-for="template in options"
+					v-for="template in list"
 					:key="template.id"
 					class="prompt-template-card"
 					:class="{ 'is-active': selectedTemplate?.id === template.id }"
@@ -32,6 +48,18 @@
 					</div>
 				</div>
 			</div>
+			<div v-if="pagination.totalCount > 0" class="prompt-template-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>
 		<template #footer>
 			<div class="drawer-footer">
@@ -48,26 +76,24 @@
 </template>
 
 <script setup lang="ts">
-import { computed, ref, watch } from 'vue'
+import { reactive, ref, watch } from 'vue'
 import { Refresh } from '@element-plus/icons-vue'
 import { resource } from '@repo/api-service'
-type PromptTemplateConfigResponse = Awaited<ReturnType<typeof resource.postPromptTemplateConfig>>
-type PromptTemplateConfig = NonNullable<PromptTemplateConfigResponse['result']>
-
-type PromptTemplateItem = {
-	content: string
-	default: boolean
-	description: string
-	has_knowledge_base: boolean
-	has_web_search: boolean
-	id: string
-	is_builtin: boolean
-	mode: string
-	name: string
-	type: string
-	user?: string
-	value: string
-}
+
+type PromptPageResponse = Awaited<ReturnType<typeof resource.postPromptTemplatePageList>>
+type PromptPageResult = NonNullable<PromptPageResponse['result']>
+type PromptTemplateItem = PromptPageResult['model'][number]
+
+const typeMap = {
+	'system-prompt': '系统提示词',
+	'agent-system-prompt': 'Agent 系统提示词',
+	rewrite: '改写提示词',
+	'fall-back': '回退提示词',
+	'context-template': '上下文模板',
+	'generate-session-title': '生成会话标题',
+	'generate-summary': '生成概要',
+	'keywords-extraction': '关键词提取'
+} as const
 
 export type PromptFieldKey =
 	| 'systemPrompt'
@@ -76,11 +102,13 @@ export type PromptFieldKey =
 	| 'rewritePromptUser'
 	| 'fallbackPrompt'
 
+export type PromptTemplateType = keyof typeof typeMap
+
 const visible = defineModel<boolean>({ required: true })
 
 const props = defineProps<{
 	field: PromptFieldKey
-	mode: string
+	type?: PromptTemplateType
 }>()
 
 const emit = defineEmits<{
@@ -88,45 +116,22 @@ const emit = defineEmits<{
 }>()
 
 const loading = ref(false)
-const config = ref<PromptTemplateConfig | null>(null)
-const options = ref<PromptTemplateItem[]>([])
+const keyword = ref('')
+const typeFilter = ref('')
+const list = ref<PromptTemplateItem[]>([])
 const selectedTemplate = ref<PromptTemplateItem | null>(null)
 
-const dialogTitle = computed(() => {
-	const titleMap: Record<PromptFieldKey, string> = {
-		systemPrompt: '系统提示词模版',
-		contextTemplate: '上下文模板模版',
-		rewritePromptSystem: '改写系统提示词模版',
-		rewritePromptUser: '改写用户提示词模版',
-		fallbackPrompt: '兜底提示词模版'
-	}
-	return titleMap[props.field]
+const pagination = reactive({
+	pageIndex: 1,
+	pageSize: 20,
+	totalCount: 0,
+	totalPages: 0
 })
 
-function getTemplateItems(field: PromptFieldKey) {
-	if (!config.value) return [] as PromptTemplateItem[]
-	switch (field) {
-		case 'systemPrompt':
-			return props.mode === 'smart-reasoning'
-				? ([
-						...(config.value.agent_system_prompts || []),
-						...(config.value.system_prompts || [])
-					] as PromptTemplateItem[])
-				: ([
-						...(config.value.system_prompts || []),
-						...(config.value.agent_system_prompts || [])
-					] as PromptTemplateItem[])
-		case 'contextTemplate':
-			return (config.value.context_templates || []) as PromptTemplateItem[]
-		case 'rewritePromptSystem':
-		case 'rewritePromptUser':
-			return (config.value.rewrites || []) as PromptTemplateItem[]
-		case 'fallbackPrompt':
-			return (config.value.fall_backs || []) as PromptTemplateItem[]
-		default:
-			return [] as PromptTemplateItem[]
-	}
-}
+const typeList = Object.keys(typeMap).map((type) => ({
+	label: typeMap[type as PromptTemplateType],
+	value: type
+}))
 
 function getTemplateContent(item: PromptTemplateItem) {
 	if (props.field === 'rewritePromptUser') return item.user || item.content || ''
@@ -148,20 +153,44 @@ function getTemplatePreview(item: PromptTemplateItem) {
 	return (item.content || item.user || '-').slice(0, 120)
 }
 
-async function loadTemplates() {
+async function loadTemplates(pageIndex = pagination.pageIndex) {
 	loading.value = true
 	try {
-		const res = await resource.postPromptTemplateConfig({})
+		const res = await resource.postPromptTemplatePageList({
+			pageIndex,
+			pageSize: pagination.pageSize,
+			keyword: keyword.value,
+			type: typeFilter.value
+		})
 		if (res.isSuccess && res.result) {
-			config.value = res.result as PromptTemplateConfig
-			options.value = getTemplateItems(props.field)
-			selectedTemplate.value = options.value[0] || null
+			list.value = (res.result.model || []) as PromptTemplateItem[]
+			pagination.pageIndex = res.result.currentPage || pageIndex
+			pagination.totalCount = res.result.totalCount || 0
+			pagination.totalPages = res.result.totalPages || 0
+			pagination.pageSize = res.result.pageSize || pagination.pageSize
+			selectedTemplate.value = list.value[0] || null
 		}
 	} finally {
 		loading.value = false
 	}
 }
 
+function handlePageChange(page: number) {
+	pagination.pageIndex = page
+	loadTemplates(page)
+}
+
+function handleSizeChange(size: number) {
+	pagination.pageSize = size
+	loadTemplates(1)
+}
+
+function handleReset() {
+	keyword.value = ''
+	typeFilter.value = props.type!
+	loadTemplates(1)
+}
+
 function confirmTemplate() {
 	if (!selectedTemplate.value) return
 	emit('confirm', getTemplateContent(selectedTemplate.value))
@@ -173,16 +202,16 @@ watch(
 	(value) => {
 		if (!value) return
 		selectedTemplate.value = null
-		loadTemplates()
+		typeFilter.value = props.type!
+		loadTemplates(1)
 	}
 )
 
 watch(
-	() => [props.field, props.mode],
+	() => [props.field, props.type],
 	() => {
-		if (!config.value) return
-		options.value = getTemplateItems(props.field)
-		selectedTemplate.value = options.value[0] || null
+		typeFilter.value = props.type!
+		if (visible.value) loadTemplates(1)
 	}
 )
 </script>
@@ -194,14 +223,15 @@ watch(
 	gap: 14px;
 }
 
-.prompt-template-dialog__meta {
+.prompt-template-toolbar {
 	display: flex;
 	align-items: center;
-	justify-content: space-between;
-	gap: 12px;
-	font-size: 14px;
-	font-weight: 600;
-	color: #111827;
+	gap: 10px;
+	flex-wrap: wrap;
+}
+
+.prompt-template-search {
+	width: 280px;
 }
 
 .prompt-template-grid {
@@ -277,6 +307,11 @@ watch(
 	overflow: auto;
 }
 
+.prompt-template-pagination {
+	display: flex;
+	justify-content: flex-end;
+}
+
 .drawer-footer {
 	display: flex;
 	justify-content: flex-end;

+ 33 - 3
apps/web/src/features/createModal/index.vue

@@ -1,7 +1,7 @@
 <template>
 	<el-dialog
 		v-model="dialogVisible"
-		:title="t('shared.createWorkflow.title')"
+		:title="dialogTitle"
 		width="600px"
 		:close-on-click-modal="false"
 		@close="handleClose"
@@ -46,7 +46,7 @@
 </template>
 
 <script setup lang="ts">
-import { ref, watch } from 'vue'
+import { computed, ref, watch } from 'vue'
 import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
 import { agent } from '@repo/api-service'
 import { useI18n } from '@/composables/useI18n'
@@ -71,6 +71,14 @@ const props = defineProps({
 	visible: {
 		type: Boolean,
 		default: false
+	},
+	mode: {
+		type: String,
+		default: 'create'
+	},
+	initialData: {
+		type: Object,
+		default: () => ({})
 	}
 })
 
@@ -97,6 +105,9 @@ const createDefaultForm = (): CreateWorkflowForm => ({
 })
 
 const form = ref<CreateWorkflowForm>(createDefaultForm())
+const dialogTitle = computed(() =>
+	props.mode === 'edit' ? t('shared.createWorkflow.editTitle') : t('shared.createWorkflow.title')
+)
 
 const rules: FormRules<CreateWorkflowForm> = {
 	name: [
@@ -115,6 +126,21 @@ watch(
 		if (val) {
 			// 打开时重置校验
 			formRef.value?.clearValidate()
+			const data = props.initialData as Partial<CreateWorkflowForm> & { profilePhoto?: string }
+			form.value = {
+				...createDefaultForm(),
+				...data
+			}
+			coverFile.value = data.profilePhoto
+				? {
+						id: data.profilePhoto,
+						name: '',
+						extensionName: '',
+						size: 0,
+						path: '',
+						contentType: 'image/*'
+					}
+				: undefined
 		}
 	},
 	{ immediate: true }
@@ -122,6 +148,7 @@ watch(
 
 const resetForm = () => {
 	form.value = createDefaultForm()
+	coverFile.value = undefined
 	formRef.value?.clearValidate()
 }
 
@@ -150,7 +177,10 @@ const handleSubmit = async () => {
 		}
 
 		const response = await agent.postAgentDoEditAgent({
-			data: payload
+			data: {
+				...payload,
+				id: props.initialData?.id
+			}
 		})
 
 		if (response?.isSuccess) {

+ 17 - 7
apps/web/src/i18n/locales/en-us.ts

@@ -174,7 +174,7 @@ export default {
 			overview: 'Overview',
 			orchestration: 'Intelligent Orchestration',
 			management: 'Agents',
-			execution: 'Execution',
+			execution: 'Execution Log',
 			chat: 'Chat',
 			ollama: 'Ollama',
 			models: 'Model Management',
@@ -256,6 +256,7 @@ export default {
 		},
 		createWorkflow: {
 			title: 'Create Workflow',
+			editTitle: 'Edit Workflow',
 			fields: {
 				name: 'Name',
 				tags: 'Tags',
@@ -274,8 +275,8 @@ export default {
 			validation: {
 				nameRequired: 'Please enter a name'
 			},
-			success: 'Workflow created successfully',
-			error: 'Failed to create workflow'
+			success: 'Workflow saved successfully',
+			error: 'Failed to save workflow'
 		},
 		searchDialog: {
 			placeholder: 'Type to search',
@@ -1514,8 +1515,7 @@ export default {
 			scoreThreshold: 'Score Threshold',
 			outputs: 'Outputs',
 			queryRequired: 'Please select a query variable',
-			knowledgeRequired:
-				'At least one of knowledge_base_ids or knowledge_ids must be specified'
+			knowledgeRequired: 'At least one of knowledge_base_ids or knowledge_ids must be specified'
 		},
 		workflowApprovalSetter: {
 			basicConfig: 'Approval Config',
@@ -1569,7 +1569,8 @@ export default {
 			temperature: 'Temperature',
 			temperatureTip: 'Controls randomness during classification. Lower values are more stable.',
 			maxTokens: 'Max Tokens',
-			maxTokensTip: 'Limits the maximum number of tokens the model can return for one classification.',
+			maxTokensTip:
+				'Limits the maximum number of tokens the model can return for one classification.',
 			thinking: 'Thinking Mode',
 			thinkingTip: 'Enables the model extended reasoning mode if the selected model supports it.',
 			advancedSettings: 'Advanced Settings',
@@ -1656,7 +1657,12 @@ export default {
 			},
 			'knowledge-retrieval': {
 				displayName: 'Knowledge Retrieval',
-				description: 'Retrieve relevant text chunks from knowledge bases or specific knowledge files'
+				description:
+					'Retrieve relevant text chunks from knowledge bases or specific knowledge files'
+			},
+			'ai-agent': {
+				displayName: 'AI Agent',
+				description: 'Execute Q&A and reasoning with agent configuration'
 			},
 			'view-data': {
 				displayName: 'View Data',
@@ -1716,6 +1722,10 @@ export default {
 				result: 'Matched text chunks',
 				content: 'Concatenated retrieval content'
 			},
+			'ai-agent': {
+				text: 'Generated content',
+				think: 'Reasoning content'
+			},
 			list: {
 				result: 'Filtered results',
 				firstRecord: 'First record',

+ 13 - 4
apps/web/src/i18n/locales/zh-cn.ts

@@ -174,8 +174,8 @@ export default {
 			overview: '概览',
 			orchestration: '智能编排',
 			management: '智能体',
-			execution: '执行',
-			chat: '聊天',
+			execution: '执行日志',
+			chat: '对话',
 			ollama: 'Ollama',
 			models: '模型管理',
 			webSearch: '网络搜索',
@@ -255,6 +255,7 @@ export default {
 		},
 		createWorkflow: {
 			title: '新建工作流',
+			editTitle: '编辑工作流',
 			fields: {
 				name: '名称',
 				tags: '标签',
@@ -273,8 +274,8 @@ export default {
 			validation: {
 				nameRequired: '请输入名称'
 			},
-			success: '创建工作流成功',
-			error: '创建工作流失败'
+			success: '保存工作流成功',
+			error: '保存工作流失败'
 		},
 		searchDialog: {
 			placeholder: '输入内容进行搜索',
@@ -1534,6 +1535,10 @@ export default {
 				displayName: '知识检索',
 				description: '从知识库或指定知识文件中检索相关文本片段'
 			},
+			'ai-agent': {
+				displayName: '智能体',
+				description: '通过智能体配置执行问答与推理'
+			},
 			'view-data': { displayName: '视图数据', description: '从配置好的视图中读取数据' },
 			start: { displayName: '用户输入', description: '用户输入节点,用于接收用户输入' },
 			end: { displayName: '输出', description: '流程结束并输出节点' },
@@ -1583,6 +1588,10 @@ export default {
 				result: '检索命中的文本片段列表',
 				content: '拼接后的检索内容'
 			},
+			'ai-agent': {
+				text: '生成内容',
+				think: '推理内容'
+			},
 			list: {
 				result: '过滤结果',
 				firstRecord: '第一条记录',

+ 3 - 1
apps/web/src/nodes/i18n.ts

@@ -22,6 +22,7 @@ const NODE_GROUP_KEYS: Record<string, 'start' | 'logic' | 'data' | 'tool' | 'oth
 	'sms-sender': 'tool',
 	'workflow-approval': 'logic',
 	'knowledge-retrieval': 'data',
+	'ai-agent': 'data',
 	'loop-start': 'logic',
 	'iteration-start': 'logic',
 	stickyNote: 'other',
@@ -29,7 +30,8 @@ const NODE_GROUP_KEYS: Record<string, 'start' | 'logic' | 'data' | 'tool' | 'oth
 }
 
 const NODE_META_KEYS: Record<string, string> = {
-	'database-query': 'database'
+	'database-query': 'database',
+	'ai-agent': 'ai-agent'
 }
 
 const getNodeMetaKey = (nodeType: string) => NODE_META_KEYS[nodeType] || nodeType

+ 184 - 0
apps/web/src/nodes/src/ai-agent/index.ts

@@ -0,0 +1,184 @@
+import { NodeConnectionTypes, type INodeDataBaseSchema, type INodeType } from '../../Interface'
+import Setter from './setter.vue'
+import { getNodeDescription, getNodeDisplayName } from '@/nodes/i18n'
+
+export type AiAgentMode = 'quick-answer' | 'smart-reasoning'
+
+export type AiAgentConfig = Record<string, any>
+
+export type AiAgentData = INodeDataBaseSchema & {
+	mode: AiAgentMode
+	agent_id?: string
+	agent_name?: string
+	agent_avatar?: string
+	agent_config: AiAgentConfig
+	query: string
+	enable_memory: boolean
+	file_variable: {
+		name: string
+		describe: string
+		type: 'array[file]'
+	}
+}
+
+export const DEFAULT_AI_AGENT_OUTPUTS: AiAgentData['outputs'] = [
+	{
+		name: 'text',
+		describe: '生成内容',
+		type: 'string'
+	},
+	{
+		name: 'think',
+		describe: '推理内容',
+		type: 'string'
+	}
+]
+
+export const createDefaultAiAgentConfig = (): AiAgentConfig => ({
+	advanced_config: {
+		enable_query_expansion: true,
+		enable_rewrite: true,
+		fallback_prompt: '',
+		fallback_response: '',
+		fallback_strategy: 'model',
+		rewrite_prompt_system: '',
+		rewrite_prompt_user: ''
+	},
+	basic_config: {
+		agent_mode: 'smart-reasoning',
+		agent_type: 'rag-qa',
+		context_template: '',
+		suggested_prompts: [],
+		system_prompt: '你是一个聪明的智能体。根据上下文,回答用户提出的问题。'
+	},
+	faq_config: {
+		faq_direct_answer_threshold: 0.9,
+		faq_priority_enabled: true,
+		faq_score_boost: 1.2
+	},
+	img_vlm_config: {
+		image_storage_provider: '',
+		image_upload_enabled: true,
+		vlm_model_id: ''
+	},
+	kb_config: {
+		kb_selection_mode: 'all',
+		knowledge_bases: [],
+		retrieve_kb_only_when_mentioned: false,
+		supported_file_types: []
+	},
+	model_config: {
+		max_completion_tokens: 2048,
+		model_id: '',
+		rerank_model_id: '',
+		temperature: 0.7,
+		thinking: false
+	},
+	multiple_config: {
+		history_turns: 5,
+		multi_turn_enabled: true
+	},
+	search_config: {
+		embedding_top_k: 10,
+		keyword_threshold: 0.3,
+		rerank_threshold: 0.3,
+		rerank_top_k: 20,
+		vector_threshold: 0.2
+	},
+	setting_config: {
+		allowed_tools: [
+			'grep_chunks',
+			'get_document_info',
+			'knowledge_search',
+			'list_knowledge_chunks',
+			'query_knowledge_graph',
+			'database_query',
+			'wiki_search',
+			'wiki_read_page',
+			'wiki_read_source_doc',
+			'wiki_flag_issue',
+			'thinking',
+			'todo_write',
+			'final_answer'
+		],
+		llm_call_timeout: 120,
+		max_iterations: 10,
+		mcp_selection_mode: 'all',
+		mcp_services: [],
+		selected_skills: [],
+		skills_selection_mode: 'none'
+	},
+	web_search_config: {
+		web_fetch_enabled: false,
+		web_fetch_top_n: 3,
+		web_search_enabled: false,
+		web_search_max_results: 5
+	}
+})
+
+export const createDefaultFileVariable = (): AiAgentData['file_variable'] => ({
+	name: 'files',
+	describe: '文件列表',
+	type: 'array[file]'
+})
+
+export const aiAgentNode: INodeType = {
+	version: ['1'],
+	displayName: getNodeDisplayName('ai-agent'),
+	name: 'ai-agent',
+	Setter,
+	description: getNodeDescription('ai-agent'),
+	group: 'data',
+	icon: 'lucide:bot',
+	iconColor: '#6172F3',
+	inputs: [NodeConnectionTypes.main],
+	outputs: [NodeConnectionTypes.main],
+	validate: (data: AiAgentData) => {
+		if (!data?.query?.trim()) return '请输入查询内容'
+		return false
+	},
+	getSubtitle: (data: AiAgentData) => {
+		if (!data?.mode) return ''
+		return data.mode === 'quick-answer' ? '快速问答' : '智能推理'
+	},
+	schema: {
+		appAgentId: '',
+		parentId: '',
+		position: {
+			x: 20,
+			y: 30
+		},
+		width: 96,
+		height: 96,
+		selected: false,
+		nodeType: 'ai-agent',
+		zIndex: 1,
+		data: {
+			type: 'ai-agent',
+			title: '智能体',
+			isInIteration: false,
+			iteration_id: '',
+			isInLoop: false,
+			loop_id: '',
+			variables: [],
+			retry_config: {
+				retry_enabled: false,
+				max_retries: 3,
+				retry_interval: 100
+			},
+			error_strategy: 'none',
+			fail_branch_node_id: '',
+			default_value: [],
+			output_can_alter: false,
+			outputs: DEFAULT_AI_AGENT_OUTPUTS,
+			mode: 'smart-reasoning',
+			agent_id: '',
+			agent_name: '',
+			agent_avatar: '',
+			agent_config: createDefaultAiAgentConfig(),
+			query: '',
+			enable_memory: false,
+			file_variable: createDefaultFileVariable()
+		}
+	}
+}

+ 409 - 0
apps/web/src/nodes/src/ai-agent/setter.vue

@@ -0,0 +1,409 @@
+<template>
+	<el-scrollbar class="w-full box-border p-12px">
+		<div class="ai-agent-setter">
+			<section class="section-block">
+				<div class="section-title-row">
+					<label class="section-title">智能体模式</label>
+				</div>
+				<div class="px-4px">
+					<el-radio-group v-model="formData.mode" class="mode-group" @change="handleModeChange">
+						<el-radio-button label="quick-answer">快速问答</el-radio-button>
+						<el-radio-button label="smart-reasoning">智能推理</el-radio-button>
+					</el-radio-group>
+				</div>
+			</section>
+
+			<section class="section-block">
+				<div class="section-title-row">
+					<label class="section-title">智能体配置</label>
+				</div>
+				<el-button class="config-button" plain @click="openAgentPicker">
+					<span class="agent-avatar">
+						{{ selectedAgentAvatar || '🤖' }}
+					</span>
+					{{ selectedAgentName || '选择智能体配置' }}
+				</el-button>
+				<div class="field-hint">选择智能体后会自动带出模式与相关配置。</div>
+			</section>
+
+			<section class="section-block">
+				<div class="section-title-row">
+					<label class="section-title">查询</label>
+				</div>
+				<VarInput
+					v-model="formData.query"
+					:rows="3"
+					class="w-full"
+					placeholder="请输入,输入/选择变量"
+				/>
+			</section>
+
+			<section class="section-block">
+				<el-switch v-model="formData.enable_memory" active-text="开启记忆" />
+			</section>
+
+			<!-- <section class="section-block">
+				<div class="section-title-row">
+					<label class="section-title">文件变量</label>
+				</div>
+				<el-input v-model="formData.file_variable.name" placeholder="变量名" />
+				<el-input v-model="formData.file_variable.describe" placeholder="描述" />
+				<el-input v-model="formData.file_variable.type" disabled />
+			</section> -->
+
+			<section class="section-block">
+				<el-collapse>
+					<el-collapse-item title="" name="2">
+						<template #title>
+							<div class="flex items-center justify-between beautify">
+								<label class="text-14px font-bold text-gray-700">输出变量</label>
+							</div>
+						</template>
+						<ul>
+							<li v-for="output in formData.outputs" :key="output.name">
+								<div>
+									<span class="text-#333">{{ output.name }}</span>
+									<span class="text-#999 ml-8px">{{ output.type }}</span>
+								</div>
+								<div class="text-#666">{{ output.describe }}</div>
+							</li>
+						</ul>
+					</el-collapse-item>
+				</el-collapse>
+			</section>
+
+			<NodeRuntimeConfig v-model="formData" />
+		</div>
+	</el-scrollbar>
+
+	<el-dialog v-model="agentPickerVisible" title="选择智能体" width="640px" append-to-body>
+		<div class="agent-picker">
+			<el-input
+				v-model="agentKeyword"
+				clearable
+				placeholder="搜索智能体"
+				@keyup.enter="fetchAgentOptions"
+			>
+				<template #append>
+					<el-button @click="fetchAgentOptions">搜索</el-button>
+				</template>
+			</el-input>
+			<el-scrollbar class="agent-picker__list">
+				<div v-loading="agentLoading" class="agent-list">
+					<el-empty v-if="!agentOptions.length && !agentLoading" description="暂无智能体" />
+					<div
+						v-for="item in agentOptions"
+						:key="item.id"
+						class="agent-option"
+						:class="{ 'agent-option--active': selectedAgentId === item.id }"
+						@click="selectAgent(item)"
+					>
+						<div class="agent-option__left">
+							<div class="agent-option__avatar">{{ getAgentAvatar(item.avatar) }}</div>
+							<div class="agent-option__main">
+								<div class="agent-option__name">{{ item.name || '未命名智能体' }}</div>
+								<div class="agent-option__desc">{{ item.description || '暂无描述' }}</div>
+							</div>
+						</div>
+						<el-tag size="small" effect="plain">{{ formatModeLabel(item.mode) }}</el-tag>
+					</div>
+				</div>
+			</el-scrollbar>
+		</div>
+		<template #footer>
+			<el-button @click="agentPickerVisible = false">关闭</el-button>
+		</template>
+	</el-dialog>
+</template>
+
+<script setup lang="ts">
+import { onMounted, ref, watch } from 'vue'
+import { agentApplication } from '@repo/api-service'
+
+import NodeRuntimeConfig from '@/nodes/_base/NodeRuntimeConfig.vue'
+import VarInput from '@/nodes/_base/VarInput.vue'
+import { useSetterModel } from '../_shared/useSetterModel'
+import {
+	createDefaultAiAgentConfig,
+	createDefaultFileVariable,
+	DEFAULT_AI_AGENT_OUTPUTS,
+	type AiAgentData,
+	type AiAgentMode
+} from './index'
+
+type AgentOption = NonNullable<
+	Awaited<ReturnType<typeof agentApplication.postAiAgentPageList>>['result']
+>['model'][number]
+
+const props = defineProps<{
+	data: AiAgentData
+}>()
+
+const emit = defineEmits<{
+	(e: 'update', data: AiAgentData): void
+}>()
+
+const formData = useSetterModel<AiAgentData>(props, emit)
+const agentPickerVisible = ref(false)
+const agentLoading = ref(false)
+const agentKeyword = ref('')
+const agentOptions = ref<AgentOption[]>([])
+const selectedAgentId = ref('')
+const selectedAgentName = ref('')
+const selectedAgentAvatar = ref('')
+
+const ensureDefaults = () => {
+	formData.value.mode = formData.value.mode || 'smart-reasoning'
+	formData.value.agent_config = {
+		...createDefaultAiAgentConfig(),
+		...(formData.value.agent_config || {})
+	}
+	formData.value.agent_id = formData.value.agent_id || ''
+	formData.value.agent_name = formData.value.agent_name || ''
+	formData.value.agent_avatar = formData.value.agent_avatar || ''
+	formData.value.file_variable = formData.value.file_variable || createDefaultFileVariable()
+	formData.value.outputs = formData.value.outputs?.length
+		? formData.value.outputs
+		: DEFAULT_AI_AGENT_OUTPUTS
+}
+
+const syncSelectedAgentDisplay = () => {
+	selectedAgentId.value = formData.value.agent_id || ''
+	selectedAgentName.value = formData.value.agent_name || ''
+	selectedAgentAvatar.value = formData.value.agent_avatar || ''
+}
+
+const formatModeLabel = (mode?: string) => {
+	if (mode === 'quick-answer') return '快速问答'
+	if (mode === 'smart-reasoning') return '智能推理'
+	return mode || '未知模式'
+}
+
+const getAgentAvatar = (avatar?: string) => avatar?.trim() || '🤖'
+
+const handleModeChange = (mode: AiAgentMode) => {
+	formData.value.agent_config = {
+		...formData.value.agent_config,
+		basic_config: {
+			...(formData.value.agent_config?.basic_config || {}),
+			agent_mode: mode
+		}
+	}
+}
+
+const fetchAgentOptions = async () => {
+	agentLoading.value = true
+	try {
+		const res = await agentApplication.postAiAgentPageList({
+			keyword: agentKeyword.value.trim(),
+			mode: '',
+			type: '',
+			pageIndex: 1,
+			pageSize: 20
+		})
+		agentOptions.value = res?.isSuccess ? res.result?.model || [] : []
+	} finally {
+		agentLoading.value = false
+	}
+}
+
+const openAgentPicker = async () => {
+	agentPickerVisible.value = true
+	if (!agentOptions.value.length) {
+		await fetchAgentOptions()
+	}
+}
+
+const selectAgent = (item: AgentOption) => {
+	selectedAgentId.value = item.id || ''
+	selectedAgentName.value = item.name || ''
+	selectedAgentAvatar.value = getAgentAvatar(item.avatar)
+	formData.value.agent_id = selectedAgentId.value
+	formData.value.agent_name = selectedAgentName.value
+	formData.value.agent_avatar = selectedAgentAvatar.value
+	formData.value.mode = (item.mode ||
+		item.config?.basic_config?.agent_mode ||
+		'smart-reasoning') as AiAgentMode
+	formData.value.agent_config = {
+		...createDefaultAiAgentConfig(),
+		...(item.config || {}),
+		basic_config: {
+			...createDefaultAiAgentConfig().basic_config,
+			...(item.config?.basic_config || {}),
+			agent_mode: formData.value.mode
+		}
+	}
+	agentPickerVisible.value = false
+}
+
+onMounted(async () => {
+	ensureDefaults()
+	syncSelectedAgentDisplay()
+	await fetchAgentOptions()
+})
+
+watch(
+	() => [formData.value.agent_id, formData.value.agent_name, formData.value.agent_avatar],
+	() => {
+		syncSelectedAgentDisplay()
+	}
+)
+</script>
+
+<style scoped lang="less">
+.ai-agent-setter {
+	display: flex;
+	flex-direction: column;
+	gap: 16px;
+}
+
+.section-block {
+	display: flex;
+	flex-direction: column;
+	gap: 10px;
+	padding-bottom: 16px;
+	border-bottom: 1px solid #eef2f7;
+}
+
+.section-title-row {
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+}
+
+.section-title {
+	font-size: 14px;
+	font-weight: 700;
+	color: #344054;
+}
+
+.field-hint {
+	font-size: 12px;
+	color: #667085;
+}
+
+.config-button {
+	width: fit-content;
+	max-width: 100%;
+}
+
+.agent-avatar {
+	display: inline-flex;
+	align-items: center;
+	justify-content: center;
+	width: 22px;
+	height: 22px;
+	margin-right: 8px;
+	font-size: 16px;
+	line-height: 1;
+}
+
+.output-list {
+	display: flex;
+	flex-direction: column;
+	gap: 8px;
+	padding: 0;
+	margin: 0;
+	list-style: none;
+}
+
+.output-item {
+	padding: 10px 12px;
+	border: 1px solid #eaecf0;
+	border-radius: 8px;
+	background: #f9fafb;
+}
+
+.output-item__top {
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	gap: 12px;
+}
+
+.output-item__name {
+	font-size: 14px;
+	font-weight: 700;
+	color: #344054;
+}
+
+.output-item__type,
+.output-item__desc {
+	font-size: 12px;
+	color: #667085;
+}
+
+.agent-picker {
+	display: flex;
+	flex-direction: column;
+	gap: 12px;
+}
+
+.agent-picker__list {
+	height: 360px;
+}
+
+.agent-list {
+	min-height: 220px;
+}
+
+.agent-option {
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	gap: 12px;
+	padding: 12px;
+	border: 1px solid #eaecf0;
+	border-radius: 8px;
+	cursor: pointer;
+}
+
+.agent-option + .agent-option {
+	margin-top: 8px;
+}
+
+.agent-option:hover,
+.agent-option--active {
+	border-color: var(--el-color-primary);
+	background: var(--el-color-primary-light-9);
+}
+
+.agent-option__left {
+	display: flex;
+	align-items: center;
+	gap: 10px;
+	min-width: 0;
+}
+
+.agent-option__avatar {
+	display: inline-flex;
+	align-items: center;
+	justify-content: center;
+	width: 32px;
+	height: 32px;
+	flex-shrink: 0;
+	font-size: 20px;
+	line-height: 1;
+	border-radius: 50%;
+	background: #f2f4f7;
+}
+
+.agent-option__main {
+	min-width: 0;
+}
+
+.agent-option__name {
+	font-size: 14px;
+	font-weight: 700;
+	color: #344054;
+}
+
+.agent-option__desc {
+	margin-top: 4px;
+	font-size: 12px;
+	color: #667085;
+	overflow: hidden;
+	text-overflow: ellipsis;
+	white-space: nowrap;
+}
+</style>

+ 2 - 0
apps/web/src/nodes/src/index.ts

@@ -18,6 +18,7 @@ import { smsSenderNode } from './sms-sender'
 import { mailSenderNode } from './mail-sender'
 import { workflowApprovalNode } from './workflow-approval'
 import { llmNode } from './llm'
+import { aiAgentNode } from './ai-agent'
 import { knowledgeRetrievalNode } from './knowledge-retrieval'
 
 import { getNodeDisplayName } from '@/nodes/i18n'
@@ -111,6 +112,7 @@ const baseNodes = [
 	startNode,
 	endNode,
 	llmNode,
+	aiAgentNode,
 	httpNode,
 	conditionNode,
 	databaseNode,

+ 2 - 1
apps/web/src/nodes/src/llm/setter.vue

@@ -4,7 +4,6 @@ import { aiModel } from '@repo/api-service'
 import { Icon, IconButton } from '@repo/ui'
 
 import NodeRuntimeConfig from '@/nodes/_base/NodeRuntimeConfig.vue'
-import VarSelect from '@/nodes/_base/VarSelect.vue'
 import VarInput from '@/nodes/_base/VarInput.vue'
 import { useSetterModel } from '../_shared/useSetterModel'
 import PromptModal from '@/features/PromptModal.vue'
@@ -208,6 +207,7 @@ onMounted(async () => {
 									<el-option label="ASSISTANT" value="assistant" />
 								</el-select>
 								<el-button
+									v-if="message.role === 'system'"
 									class="prompt-template-trigger"
 									text
 									:icon="DocumentCopy"
@@ -318,6 +318,7 @@ onMounted(async () => {
 	<PromptModal
 		v-model="promptTemplateDialogVisible"
 		:field="editPrompt?.role === 'system' ? 'systemPrompt' : 'contextTemplate'"
+		:type="editPrompt?.role === 'system' ? 'system-prompt' : 'context-template'"
 		mode=""
 		@confirm="handlePromptTemplateConfirm"
 	/>

+ 4 - 0
apps/web/src/nodes/src/question-classifier/index.ts

@@ -37,6 +37,10 @@ export const questionClassifierNode: INodeType = {
 
 		return ports
 	},
+	validate: (data: QuestionClassifierData) => {
+		if (!data.model_id) return '请选择模型'
+		return false
+	},
 	schema: {
 		appAgentId: '',
 		parentId: '',

+ 63 - 22
apps/web/src/nodes/src/question-classifier/setter.vue

@@ -1,6 +1,36 @@
 <template>
 	<el-scrollbar class="w-full box-border p-12px">
 		<div class="qc-setter">
+			<!-- 模型设置 -->
+			<section class="section-block">
+				<div class="section-header">
+					<label class="section-title"
+						>{{ texts.modelSettings }}<span class="text-#f04438">*</span></label
+					>
+				</div>
+				<div class="advanced-item">
+					<el-select
+						v-model="formData.model_id"
+						filterable
+						remote
+						reserve-keyword
+						clearable
+						class="w-full"
+						:remote-method="searchChatModels"
+						:loading="modelsLoading"
+						:placeholder="texts.modelPlaceholder"
+					>
+						<el-option
+							v-for="model in chatModels"
+							:key="model.id"
+							:label="modelLabel(model)"
+							:value="model.id"
+						/>
+					</el-select>
+					<div class="advanced-tip">{{ texts.modelTip }}</div>
+				</div>
+			</section>
+
 			<section class="section-block">
 				<div class="w-full flex items-center justify-between beautify">
 					<label class="text-14px font-bold text-gray-700">{{ texts.input }}</label>
@@ -21,8 +51,14 @@
 					<div class="empty-desc">{{ texts.empty }}</div>
 				</div>
 
-				<VueDraggable v-else v-model="formData.classes" :animation="150" handle=".handle" @end="handleSortEnd"
-					class="class-list">
+				<VueDraggable
+					v-else
+					v-model="formData.classes"
+					:animation="150"
+					handle=".handle"
+					@end="handleSortEnd"
+					class="class-list"
+				>
 					<div v-for="(item, index) in formData.classes" :key="item.id" class="class-card">
 						<div class="class-card__header">
 							<div class="class-card__title">
@@ -30,33 +66,33 @@
 								<span class="class-index">{{ item.name }}</span>
 							</div>
 							<div class="class-card__actions">
-								<IconButton link icon="lucide:copy" class="text-#667085" @click="handleDuplicateClass(index)" />
-								<IconButton link icon="lucide:trash-2" class="text-#f04438 ml-0!" @click="handleRemoveClass(index)" />
+								<IconButton
+									link
+									icon="lucide:copy"
+									class="text-#667085"
+									@click="handleDuplicateClass(index)"
+								/>
+								<IconButton
+									link
+									icon="lucide:trash-2"
+									class="text-#f04438 ml-0!"
+									@click="handleRemoveClass(index)"
+								/>
 							</div>
 						</div>
 
 						<div class="class-card__body">
-							<VarInput v-model="item.instruction" class="w-full" :rows="3"
-								:placeholder="texts.classInstructionPlaceholder" />
+							<VarInput
+								v-model="item.instruction"
+								class="w-full"
+								:rows="3"
+								:placeholder="texts.classInstructionPlaceholder"
+							/>
 						</div>
 					</div>
 				</VueDraggable>
 			</section>
 
-			<!-- 模型设置 -->
-			<section class="section-block">
-				<div class="section-header">
-					<label class="section-title">{{ texts.modelSettings }}</label>
-				</div>
-				<div class="advanced-item">
-					<el-select v-model="formData.model_id" filterable remote reserve-keyword clearable class="w-full"
-						:remote-method="searchChatModels" :loading="modelsLoading" :placeholder="texts.modelPlaceholder">
-						<el-option v-for="model in chatModels" :key="model.id" :label="modelLabel(model)" :value="model.id" />
-					</el-select>
-					<div class="advanced-tip">{{ texts.modelTip }}</div>
-				</div>
-			</section>
-
 			<section class="section-block">
 				<el-collapse>
 					<el-collapse-item name="1">
@@ -69,7 +105,12 @@
 						</template>
 						<div class="advanced-item">
 							<div class="advanced-label">{{ texts.instruction }}</div>
-							<VarInput v-model="formData.instruction" class="w-full" :rows="3" placeholder="输入/选择变量" />
+							<VarInput
+								v-model="formData.instruction"
+								class="w-full"
+								:rows="3"
+								placeholder="输入/选择变量"
+							/>
 						</div>
 					</el-collapse-item>
 
@@ -105,7 +146,7 @@ import NodeRuntimeConfig from '@/nodes/_base/NodeRuntimeConfig.vue'
 import VarInput from '@/nodes/_base/VarInput.vue'
 import { useI18n } from '@/composables/useI18n'
 import { useSetterModel } from '../_shared/useSetterModel'
-import { aiModel } from "@repo/api-service"
+import { aiModel } from '@repo/api-service'
 
 import type { QuestionClassifierData, ClassItem } from './index'
 

+ 5 - 5
apps/web/src/router/index.ts

@@ -150,14 +150,14 @@ const routes = [
 				path: 'workspace',
 				name: 'Workspace',
 				component: Workspace
+			},
+			{
+				path: '/workflow',
+				name: 'Workflow',
+				component: FlowManagement
 			}
 		]
 	},
-	{
-		path: '/workflow',
-		name: 'Workflow',
-		component: FlowManagement
-	},
 	{
 		path: '/workflow/:id',
 		name: 'Editor',

+ 24 - 8
apps/web/src/views/Dashboard.vue

@@ -8,10 +8,10 @@
 					{{ t('pages.dashboard.welcomeSubtitle', { date: currentDate }) }}
 				</p>
 			</div>
-			<el-button type="primary" size="large" @click="createWorkflow">
+			<!-- <el-button type="primary" size="large" @click="createWorkflow">
 				<SvgIcon name="workflow" size="16" />
 				{{ t('pages.dashboard.createWorkflow') }}
-			</el-button>
+			</el-button> -->
 		</div>
 
 		<!-- 统计卡片 -->
@@ -104,7 +104,12 @@
 					</el-button>
 				</div>
 				<div class="workflow-list">
-					<div class="workflow-item" v-for="item in recentWorkflows" :key="item.id" @click="toEditor(item.id)">
+					<div
+						class="workflow-item"
+						v-for="item in recentWorkflows"
+						:key="item.id"
+						@click="toEditor(item.id)"
+					>
 						<div class="workflow-icon">
 							<SvgIcon name="workflow" size="20" />
 						</div>
@@ -150,7 +155,12 @@
 				</el-button>
 			</div>
 			<div class="templates-grid">
-				<div class="template-card" v-for="item in templates" :key="item.id" @click="goToTemplate(item.id)">
+				<div
+					class="template-card"
+					v-for="item in templates"
+					:key="item.id"
+					@click="goToTemplate(item.id)"
+				>
 					<div class="template-icon">
 						<SvgIcon :name="item.icon" size="32" />
 					</div>
@@ -162,12 +172,18 @@
 		</div>
 
 		<!-- 模板弹窗 -->
-		<TemplateModal :visible="templateModalVisible" @close="templateModalVisible = false"
-			@select="handleTemplateSelect" />
+		<TemplateModal
+			:visible="templateModalVisible"
+			@close="templateModalVisible = false"
+			@select="handleTemplateSelect"
+		/>
 
 		<!-- 新建工作流弹窗 -->
-		<CreateWorkflowModal :visible="createModalVisible" @close="createModalVisible = false"
-			@success="handleCreateSuccess" />
+		<CreateWorkflowModal
+			:visible="createModalVisible"
+			@close="createModalVisible = false"
+			@success="handleCreateSuccess"
+		/>
 	</div>
 </template>
 

+ 122 - 20
apps/web/src/views/FlowManagement.vue

@@ -4,6 +4,12 @@
 			<div class="header-title">
 				<h1>智能编排</h1>
 			</div>
+			<el-button type="primary" @click="openCreateWorkflow">
+				<el-icon>
+					<Plus />
+				</el-icon>
+				新建编排
+			</el-button>
 		</div>
 
 		<div class="stats-grid">
@@ -71,25 +77,36 @@
 					class="agent-card"
 					@click="handleRowClick(row)"
 				>
+					<div class="card-actions" @click.stop>
+						<el-dropdown trigger="click">
+							<span class="card-actions__trigger">
+								<el-icon>
+									<MoreFilled />
+								</el-icon>
+							</span>
+							<template #dropdown>
+								<el-dropdown-menu>
+									<el-dropdown-item @click="openEditWorkflow(row)"> 编辑 </el-dropdown-item>
+									<!-- <el-dropdown-item @click="confirmDeleteWorkflow(row.id)" divided>
+										<span class="danger-text">删除</span>
+									</el-dropdown-item> -->
+								</el-dropdown-menu>
+							</template>
+						</el-dropdown>
+					</div>
+
 					<div class="cover">
-						<img
-							v-if="row.profilePhoto"
+						<el-image
 							:src="row.profilePhoto ? `/File/GetImage?fileId=${row.profilePhoto}` : undefined"
 							:alt="row.name"
 							class="cover-image"
-						/>
-						<div v-else class="cover-fallback">
-							<div class="fallback-monogram">{{ row.name?.slice(0, 1) || 'A' }}</div>
-						</div>
+						>
+							<template #error>
+								<div class="image-viewer-slot image-slot"></div>
+							</template>
+						</el-image>
 
 						<div class="cover-gradient"></div>
-
-						<div class="cover-top">
-							<span class="cover-badge">{{ t('pages.management.badges.agent') }}</span>
-							<span class="cover-badge subtle">
-								{{ t('pages.management.badges.envVariables', { count: row.env_variables.length }) }}
-							</span>
-						</div>
 					</div>
 
 					<div class="cover-bottom">
@@ -134,18 +151,26 @@
 				/>
 			</div>
 		</div>
+
+		<CreateWorkflowModal
+			:visible="createWorkflowVisible"
+			:mode="workflowModalMode"
+			:initial-data="workflowInitialData"
+			@close="createWorkflowVisible = false"
+			@success="handleWorkflowSaved"
+		/>
 	</div>
 </template>
 
 <script setup lang="ts">
 import { computed, onMounted, ref } from 'vue'
-import { useRouter } from 'vue-router'
-import { ElMessage } from 'element-plus'
-import { RefreshRight, Search } from '@element-plus/icons-vue'
-import { agent } from '@repo/api-service'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { MoreFilled, Plus, RefreshRight, Search } from '@element-plus/icons-vue'
+import { agent, agentApplication } from '@repo/api-service'
 
 import SvgIcon from '@/components/SvgIcon/index.vue'
 import { useI18n } from '@/composables/useI18n'
+import CreateWorkflowModal from '@/features/createModal/index.vue'
 
 type AgentListResponse = Awaited<ReturnType<typeof agent.postAgentGetAgentList>>
 type AgentListResult = NonNullable<AgentListResponse['result']>
@@ -180,13 +205,13 @@ const emptyText = computed(
 		}) as const
 )
 
-const separator = '、'
-
-const router = useRouter()
 const loading = ref(false)
 const keyword = ref('')
 const pageIndex = ref(1)
 const pageData = ref<AgentListResult>(createEmptyPageData())
+const createWorkflowVisible = ref(false)
+const workflowModalMode = ref<'create' | 'edit'>('create')
+const workflowInitialData = ref<Record<string, any>>({})
 
 const agents = computed(() => pageData.value.model)
 
@@ -217,10 +242,62 @@ const openAgent = (id: string) => {
 	window.open(`#/workflow/${id}`, '_blank')
 }
 
+const openCreateWorkflow = () => {
+	workflowModalMode.value = 'create'
+	workflowInitialData.value = {}
+	createWorkflowVisible.value = true
+}
+
+const openEditWorkflow = async (row: AgentListItem) => {
+	try {
+		const res = await agent.postAgentGetAgentInfo({ id: row.id })
+		if (!res.isSuccess || !res.result) {
+			throw new Error('load workflow detail failed')
+		}
+
+		const detail = res.result as Record<string, any>
+		workflowInitialData.value = {
+			id: detail.id,
+			name: detail.name || row.name,
+			remark: detail.remark || '',
+			profilePhoto: detail.profilePhoto || row.profilePhoto || '',
+			viewPort: detail.viewPort || row.viewPort || { x: 0, y: 0, zoom: 1 }
+		}
+		workflowModalMode.value = 'edit'
+		createWorkflowVisible.value = true
+	} catch (error) {
+		console.error('openEditWorkflow error', error)
+		ElMessage.error('加载编排详情失败')
+	}
+}
+
+const confirmDeleteWorkflow = async (id: string) => {
+	try {
+		await ElMessageBox.confirm('确定要删除该编排吗?删除后不可恢复。', '删除确认', {
+			type: 'warning',
+			confirmButtonText: '删除',
+			cancelButtonText: '取消'
+		})
+		const res = await agentApplication.postAiAgentOpenApiDelete({ id })
+		if (!res.isSuccess) throw new Error('delete failed')
+		ElMessage.success('删除成功')
+		await loadAgents(pageIndex.value)
+	} catch (error) {
+		if (error !== 'cancel' && error !== 'close') {
+			console.error('confirmDeleteWorkflow error', error)
+		}
+	}
+}
+
 const handleRowClick = (row: AgentListItem) => {
 	openAgent(row.id)
 }
 
+const handleWorkflowSaved = async () => {
+	createWorkflowVisible.value = false
+	await loadAgents(pageIndex.value)
+}
+
 const loadAgents = async (targetPage = 1) => {
 	loading.value = true
 
@@ -630,6 +707,31 @@ onMounted(() => {
 	margin-top: 20px;
 }
 
+.card-actions {
+	position: absolute;
+	right: 12px;
+	top: 12px;
+	z-index: 1;
+}
+
+.card-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;
+}
+
+.card-actions__trigger:hover {
+	background: rgba(15, 23, 42, 0.08);
+	color: #0f172a;
+}
+
 @media (max-width: 960px) {
 	.page-header,
 	.toolbar {

+ 18 - 7
apps/web/src/views/agent/components/EditModal.vue

@@ -80,7 +80,12 @@
 											class="prompt-template-trigger"
 											text
 											:icon="DocumentCopy"
-											@click="openPromptTemplatePicker('systemPrompt')"
+											@click="
+												openPromptTemplatePicker(
+													'systemPrompt',
+													form.mode === 'quick-answer' ? 'agent-system-prompt' : 'system-prompt'
+												)
+											"
 										>
 											模板选择
 										</el-button>
@@ -120,7 +125,7 @@
 											class="prompt-template-trigger"
 											text
 											:icon="DocumentCopy"
-											@click="openPromptTemplatePicker('contextTemplate')"
+											@click="openPromptTemplatePicker('contextTemplate', 'context-template')"
 										>
 											模板选择
 										</el-button>
@@ -509,7 +514,7 @@
 												class="prompt-template-trigger"
 												text
 												:icon="DocumentCopy"
-												@click="openPromptTemplatePicker('fallbackPrompt')"
+												@click="openPromptTemplatePicker('fallbackPrompt', 'fall-back')"
 											>
 												模板选择
 											</el-button>
@@ -744,7 +749,7 @@
 												class="prompt-template-trigger"
 												text
 												:icon="DocumentCopy"
-												@click="openPromptTemplatePicker('rewritePromptSystem')"
+												@click="openPromptTemplatePicker('rewritePromptSystem', 'rewrite')"
 											>
 												模板选择
 											</el-button>
@@ -783,7 +788,7 @@
 												class="prompt-template-trigger"
 												text
 												:icon="DocumentCopy"
-												@click="openPromptTemplatePicker('rewritePromptUser')"
+												@click="openPromptTemplatePicker('rewritePromptUser', 'rewrite')"
 											>
 												模板选择
 											</el-button>
@@ -822,6 +827,7 @@
 			v-model="promptTemplateDialogVisible"
 			:field="currentPromptField"
 			:mode="form.mode"
+			:type="currentPromptTemplateType"
 			@confirm="handlePromptTemplateConfirm"
 		/>
 
@@ -841,7 +847,10 @@ import { DocumentCopy } from '@element-plus/icons-vue'
 import { agentApplication, aiModel, knowledge, resource } from '@repo/api-service'
 import EmojiPicker from 'vue3-emoji-picker'
 import 'vue3-emoji-picker/css'
-import PromptModal, { type PromptFieldKey } from '@/features/PromptModal.vue'
+import PromptModal, {
+	type PromptFieldKey,
+	type PromptTemplateType
+} from '@/features/PromptModal.vue'
 import type {
 	AgentFormData,
 	AgentItem,
@@ -898,6 +907,7 @@ const rewritePromptSystemInputRef = ref()
 const rewritePromptUserInputRef = ref()
 const promptTemplateDialogVisible = ref(false)
 const currentPromptField = ref<PromptFieldKey>('systemPrompt')
+const currentPromptTemplateType = ref<PromptTemplateType>('system-prompt')
 const supportedFileTypes = ['pdf', 'docx', 'txt', 'md', 'csv', 'xlsx', 'jpg']
 const selectionModeOptions = [
 	{ label: '全部知识库', value: 'all' },
@@ -1117,8 +1127,9 @@ function clearEmoji() {
 	form.avatar = ''
 }
 
-async function openPromptTemplatePicker(field: PromptFieldKey) {
+async function openPromptTemplatePicker(field: PromptFieldKey, type: PromptTemplateType) {
 	currentPromptField.value = field
+	currentPromptTemplateType.value = type
 	promptTemplateDialogVisible.value = true
 }
 

+ 2 - 2
apps/web/src/views/chat/api/chat.api.ts

@@ -152,10 +152,10 @@ export async function getKnowledgeBaseOptions(keyword = ''): Promise<ChatOptionI
 	}))
 }
 
-export async function getModelOptions(keyword = ''): Promise<ChatOptionItem[]> {
+export async function getModelOptions(keyword = '', type: string): Promise<ChatOptionItem[]> {
 	const res = await aiModel.postModelPageList({
 		keyword,
-		type: '',
+		type,
 		source: '',
 		pageIndex: 1,
 		pageSize: 100

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

@@ -73,7 +73,7 @@ export function useChatStream() {
 		try {
 			const token =
 				localStorage.getItem('oauth2token') ||
-				document.cookie.match(new RegExp('(^| )' + 'x-sessionId_b' + '=([^;]*)(;|$)'))?.[2]
+				document.cookie.match(new RegExp('(^| )' + 'x-sessionId' + '=([^;]*)(;|$)'))?.[2]
 
 			// dev读取环境变量 prod使用当前
 			const baseUrl = import.meta.env.DEV ? `http://${import.meta.env.VITE_BASE_URL}` : ''
@@ -82,7 +82,8 @@ export function useChatStream() {
 				signal: abortController.signal,
 				headers: {
 					Authorization: token ? JSON.parse(token) : ''
-				}
+				},
+				credentials: 'include'
 			})
 
 			for await (const chunk of request.stream<string>()) {

+ 27 - 32
apps/web/src/views/chat/index.vue

@@ -26,13 +26,13 @@
 			>
 				<div
 					v-if="settingsDraft.type === 'agent' && agentPromptsItems?.length"
-					style="margin-top: 12px; display: flex; flex-direction: column; gap: 12px"
+					style="margin-top: 12px; width: 80%"
 				>
 					<Prompts
 						title="快速开始:"
 						:items="agentPromptsItems"
 						@item-click="handlePromptItemClick"
-						vertical
+						wrap
 					/>
 				</div>
 			</MessageList>
@@ -46,18 +46,8 @@
 				@cancel="handleCancel"
 			>
 				<template #header>
-					<div class="px-8px py-4px flex items-center gap-4px">
-						<div v-if="settingsDraft.type === 'agent'" class="flex items-center gap-4px">
-							<div class="title">智能体:</div>
-							<el-select
-								class="w-180px"
-								placeholder="选择智能体"
-								v-model="settingsDraft.agentId"
-								:options="agentOptions"
-								@change="handleAgentSelectChange"
-							/>
-						</div>
-						<div v-if="settingsDraft.type === 'knowledge'" class="flex items-center gap-4px">
+					<div class="px-8px py-4px flex items-center gap-12px">
+						<div class="flex items-center gap-4px">
 							<div class="title">知识库:</div>
 							<el-select
 								class="w-180px"
@@ -67,11 +57,11 @@
 								multiple
 							/>
 						</div>
-						<div v-if="settingsDraft.type === 'knowledge'" class="flex items-center gap-4px">
+						<div class="flex items-center gap-4px">
 							<div class="title">知识:</div>
 							<el-select
 								class="w-180px"
-								placeholder="选择智能体"
+								placeholder="选择知识"
 								v-model="settingsDraft.knowledgeIds"
 								:options="knowledgeBaseOptions"
 								multiple
@@ -80,15 +70,14 @@
 					</div>
 				</template>
 				<template #prefix-extra>
+					<!-- 智能体 -->
 					<el-select
-						v-model="activeTargetType"
-						class="chat-target-select"
-						@change="handleTargetTypeChange"
-					>
-						<el-option :label="t('pages.chat.targetKnowledge')" value="knowledge" />
-						<el-option :label="t('pages.chat.targetAgent')" value="agent" />
-						<el-option :label="t('pages.chat.targetModel')" value="model" />
-					</el-select>
+						class="w-180px"
+						placeholder="选择智能体"
+						v-model="settingsDraft.agentId"
+						:options="agentOptions"
+						@change="handleAgentSelectChange"
+					/>
 					<!-- 附件 -->
 					<el-badge :value="currentAttachments.length" :hidden="!currentAttachments.length">
 						<el-button
@@ -107,6 +96,7 @@
 				<template #action>
 					<el-select
 						placeholder="选择模型"
+						placement="top"
 						v-model="settingsDraft.summaryModelId"
 						:options="modelOptions"
 						class="w-120px"
@@ -323,7 +313,7 @@ async function loadChatOptions() {
 		const [agents, knowledgeBases, models] = await Promise.all([
 			getAgentOptions(),
 			getKnowledgeBaseOptions(),
-			getModelOptions()
+			getModelOptions('', 'KnowledgeQA')
 		])
 		agentOptions.value = agents
 		knowledgeBaseOptions.value = knowledgeBases
@@ -405,7 +395,9 @@ async function getSuggestQuestion() {
 				key: index,
 				label: item.question,
 				itemStyle: {
-					height: 'auto'
+					height: 'auto',
+					width: 'calc(50% - 6px)',
+					boxSizing: 'border-box'
 				}
 			}
 		})
@@ -446,11 +438,14 @@ function parseAnswerThinkState(text?: string) {
 	let answer = raw
 
 	if (hasThinkStart) {
-		answer = answer.replace(/^\s*<think\b[^>]*>([\s\S]*?)(?:<\/think>|$)/i, (_match, inner: string) => {
-			const piece = normalizeText(inner)
-			if (piece) thinking.push(piece)
-			return ''
-		})
+		answer = answer.replace(
+			/^\s*<think\b[^>]*>([\s\S]*?)(?:<\/think>|$)/i,
+			(_match, inner: string) => {
+				const piece = normalizeText(inner)
+				if (piece) thinking.push(piece)
+				return ''
+			}
+		)
 	}
 
 	return {
@@ -1031,7 +1026,7 @@ function handleCancel() {
 
 .title {
 	font-size: 12px;
-	color: var(--text-primary);
+	color: var(--text-secondary);
 	flex-shrink: 0;
 }
 

+ 71 - 6
apps/web/src/views/knowledge/DocumentManage.vue

@@ -100,7 +100,7 @@
 			</el-table>
 			<el-empty v-else description="暂无知识内容" class="page-empty" />
 		</el-card>
-
+		<!-- 新建 -->
 		<el-drawer v-model="manualDrawerVisible" title="新建知识" direction="rtl" size="640px">
 			<el-form ref="manualFormRef" :model="manualForm" :rules="manualRules" label-position="top">
 				<el-form-item label="标题" prop="title">
@@ -122,7 +122,7 @@
 				</div>
 			</template>
 		</el-drawer>
-
+		<!-- 导入模版 -->
 		<el-drawer v-model="fileDrawerVisible" title="导入文件知识" direction="rtl" size="640px">
 			<el-form :model="fileForm" label-position="top">
 				<el-form-item label="上传文件">
@@ -179,24 +179,31 @@
 				</div>
 			</template>
 		</el-drawer>
-
-		<el-drawer v-model="editDrawerVisible" title="编辑知识" direction="rtl" size="560px">
+		<!-- 编辑 -->
+		<el-drawer
+			v-model="editDrawerVisible"
+			title="编辑知识"
+			direction="rtl"
+			size="560px"
+			class="edit-knowledge-drawer"
+		>
 			<el-form
 				ref="editFormRef"
 				v-loading="editDrawerLoading"
 				:model="editForm"
 				:rules="editRules"
 				label-position="top"
+				class="edit-knowledge-form"
 			>
 				<el-form-item label="标题" prop="title">
 					<el-input v-model="editForm.title" />
 				</el-form-item>
-				<el-form-item label="描述(点击编辑,支持Markdown语法)">
+				<el-form-item label="描述(点击编辑,支持Markdown语法)" class="edit-description-item">
 					<!-- <markdown v-if="!isEdit" :content="editForm.description" @click="isEdit = true" /> -->
 					<el-input
 						v-model="editForm.description"
 						type="textarea"
-						:rows="5"
+						class="edit-description-input"
 						@blur="isEdit = false"
 					/>
 				</el-form-item>
@@ -552,6 +559,64 @@ watch(
 	gap: 8px;
 }
 
+:global(.edit-knowledge-drawer) {
+	display: flex;
+	flex-direction: column;
+}
+
+:global(.edit-knowledge-drawer .el-drawer__header),
+:global(.edit-knowledge-drawer .el-drawer__footer) {
+	flex: 0 0 auto;
+}
+
+:global(.edit-knowledge-drawer .el-drawer__body) {
+	flex: 1;
+	display: flex;
+	flex-direction: column;
+	min-height: 0;
+	padding-bottom: 12px;
+	overflow: hidden;
+}
+
+.edit-knowledge-form {
+	flex: 1;
+	display: flex;
+	flex-direction: column;
+	min-height: 0;
+}
+
+.edit-knowledge-form :deep(.el-form-item:last-child) {
+	flex: 1;
+	min-height: 0;
+	margin-bottom: 0;
+}
+
+.edit-description-item {
+	flex: 1;
+	display: flex;
+	flex-direction: column;
+	min-height: 0;
+	margin-bottom: 0;
+}
+
+.edit-description-item :deep(.el-form-item__content) {
+	flex: 1;
+	min-height: 0;
+	display: flex;
+}
+
+.edit-description-input {
+	flex: 1;
+	height: 100%;
+	min-height: 0;
+}
+
+.edit-description-input :deep(.el-textarea__inner) {
+	height: 100%;
+	min-height: 100%;
+	resize: none;
+}
+
 .metadata-config {
 	width: 100%;
 }

+ 97 - 30
apps/web/src/views/resource/components/McpPanel.vue

@@ -2,14 +2,26 @@
 	<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>
 					</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" />
@@ -66,10 +78,10 @@
 								</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)">
+										<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>
@@ -84,17 +96,28 @@
 				</div>
 
 				<div class="desc">{{ row.description || '暂无描述' }}</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" />
+			<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" />
@@ -121,10 +144,18 @@
 				</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">
@@ -132,16 +163,23 @@
 						<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>
@@ -153,16 +191,19 @@
 						<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>
@@ -174,16 +215,19 @@
 						<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>
@@ -195,8 +239,13 @@
 						<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>
@@ -245,8 +294,24 @@
 				<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" :title="item.name">
-							<div class="tool-desc">{{ item.description }}</div>
+						<el-collapse-item
+							v-for="item in filteredToolDetailList"
+							:key="item.name"
+							:title="item.name"
+						>
+							<el-descriptions direction="vertical" :column="1">
+								<el-descriptions-item label="描述">
+									<div class="tool-desc">{{ item.description }}</div>
+								</el-descriptions-item>
+								<el-descriptions-item label="参数结构">
+									<CodeEditor
+										:model-value="JSON.stringify(item.inputSchema, null, 2)"
+										language="json"
+										:height="260"
+										readonly
+									/>
+								</el-descriptions-item>
+							</el-descriptions>
 						</el-collapse-item>
 					</el-collapse>
 				</el-scrollbar>
@@ -260,6 +325,8 @@ import { computed, onMounted, reactive, ref } from 'vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import { Plus, Search, MoreFilled } from '@element-plus/icons-vue'
 import { resource } from '@repo/api-service'
+import { XMarkdown } from 'vue-element-plus-x'
+import CodeEditor from '@/components/CodeEditor/CodeEditor.vue'
 
 type McpPageResponse = Awaited<ReturnType<typeof resource.postMcpPageList>>
 type McpItem = NonNullable<McpPageResponse['result']>['model'][number]

+ 34 - 12
apps/web/src/views/resource/components/PromptTemplatePanel.vue

@@ -2,16 +2,27 @@
 	<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>
 					</template>
 				</el-input>
-				<el-select v-model="type" clearable placeholder="类型" style="width: 160px" :options="typeList"
-					@change="loadList(1)"></el-select>
+				<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>
@@ -31,7 +42,8 @@
 				<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 }}
+							<el-tag v-if="item.is_builtin" type="success" effect="light">内置</el-tag
+							>{{ item.name }}
 						</div>
 
 						<div class="resource-card__desc">{{ item.description || '暂无描述' }}</div>
@@ -65,12 +77,24 @@
 		</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" />
+			<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-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" />
@@ -185,7 +209,7 @@ const typeMap: Record<string, string> = {
 	'system-prompt': '系统提示词',
 	'agent-system-prompt': 'Agent 系统提示词',
 	rewrite: '改写提示词',
-	'fall-back': '回退提示词',
+	'fall-back': '兜底提示词',
 	'context-template': '上下文模板',
 	'generate-session-title': '生成会话标题',
 	'generate-summary': '生成概要',
@@ -264,7 +288,6 @@ function openEdit(item: PromptItem) {
 	form.content = item.content || ''
 	form.user = item.user || ''
 	form.type = item.type || 'system-prompt'
-	form.is_default = !!item.is_default
 	form.has_knowledge_base = !!item.has_knowledge_base
 	form.has_web_search = !!item.has_web_search
 	drawerVisible.value = true
@@ -434,7 +457,6 @@ onMounted(() => {
 }
 
 @media (max-width: 768px) {
-
 	.toolbar,
 	.grid-2 {
 		grid-template-columns: 1fr;

+ 1 - 1
packages/api-client/src/request.ts

@@ -101,7 +101,7 @@ class HttpClient {
 			// const enterpriseCode = search?.['enterpriseCode'] || 'a'
 			const token =
 				localStorage.getItem('oauth2token') ||
-				document.cookie.match(new RegExp('(^| )' + 'x-sessionId_b' + '=([^;]*)(;|$)'))?.[2]
+				document.cookie.match(new RegExp('(^| )' + 'x-sessionId' + '=([^;]*)(;|$)'))?.[2]
 
 			// 添加token
 			if (token) {

+ 4 - 1
packages/api-service/schema/resource.openapi.json

@@ -50,9 +50,12 @@
 									},
 									"pageSize": {
 										"type": "integer"
+									},
+									"type": {
+										"type": "string"
 									}
 								},
-								"required": ["keyword", "pageIndex", "pageSize"]
+								"required": []
 							},
 							"example": {
 								"keyword": "",

+ 4 - 3
packages/api-service/servers/resource/api/resource.ts

@@ -513,9 +513,10 @@ export async function postPromptTemplateInitPromptContext(
 /** 获取分页列表 POST /api/ai/prompt-template/pageList */
 export async function postPromptTemplatePageList(
   body: {
-    keyword: string
-    pageIndex: number
-    pageSize: number
+    keyword?: string
+    pageIndex?: number
+    pageSize?: number
+    type?: string
   },
   options?: { [key: string]: any }
 ) {