Bladeren bron

fix: 修改问题

jiaxing.liao 5 dagen geleden
bovenliggende
commit
47c6bfcadb

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

@@ -24,6 +24,7 @@ declare module 'vue' {
     ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
     ElButton: typeof import('element-plus/es')['ElButton']
     ElCard: typeof import('element-plus/es')['ElCard']
+    ElCascader: typeof import('element-plus/es')['ElCascader']
     ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
     ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
     ElCheckTag: typeof import('element-plus/es')['ElCheckTag']
@@ -112,6 +113,7 @@ declare global {
   const ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
   const ElButton: typeof import('element-plus/es')['ElButton']
   const ElCard: typeof import('element-plus/es')['ElCard']
+  const ElCascader: typeof import('element-plus/es')['ElCascader']
   const ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
   const ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
   const ElCheckTag: typeof import('element-plus/es')['ElCheckTag']

+ 2 - 2
apps/web/src/components/Chat/ChatInput.vue

@@ -14,8 +14,8 @@
 			@cancel="emit('cancel')"
 		>
 			<!-- 头部插槽 -->
-			<template #header>
-				<slot name="header" />
+			<template v-for="(_, name) in $slots" :key="name" #[name]="slotProps">
+				<slot :name="name" v-bind="slotProps" />
 			</template>
 
 			<!-- 插槽内容 -->

+ 5 - 1
apps/web/src/features/nodeLibary/index.vue

@@ -79,6 +79,10 @@ const canAddNodeType = (nodeType: string) => {
 
 const getGroupId = (group?: string): NodeLibraryGroupId => GROUP_ALIASES[group || ''] || 'other'
 const isImageIcon = (icon?: string) => !!icon && icon.startsWith('data:image/')
+const getNodeName = (node: INodeType) =>
+	getGroupId(node.group) === 'custom'
+		? node.displayName || node.name
+		: getNodeDisplayName(node.schema.nodeType)
 
 const materials = computed<NodeGroup[]>(() => {
 	const groupMap = new Map<string, NodeGroup>()
@@ -106,7 +110,7 @@ const materials = computed<NodeGroup[]>(() => {
 			groupMap.get(groupId)!.nodes.push({
 				id: node.name,
 				type: node.schema.nodeType,
-				name: getNodeDisplayName(node.schema.nodeType),
+				name: getNodeName(node),
 				description: getNodeDescription(node.schema.nodeType),
 				icon: node.icon,
 				isImageIcon: isImageIcon(node.icon),

+ 1 - 0
apps/web/src/i18n/locales/en-us.ts

@@ -595,6 +595,7 @@ export default {
 		},
 		management: {
 			title: 'Agent Management',
+			subtitle: 'Workflow-based orchestration for automation, batch processing, and related scenarios.',
 			stats: {
 				totalAgents: 'Total Agents',
 				currentPageCount: 'Items on Page',

+ 99 - 97
apps/web/src/i18n/locales/zh-cn.ts

@@ -546,13 +546,14 @@ export default {
 		},
 		management: {
 			title: '智能体管理',
+			subtitle: '基于工作流的编排,适用于自动化、批处理等场景。',
 			stats: {
 				totalAgents: '智能体总数',
 				currentPageCount: '当前页数量',
 				conversationVariables: '会话变量数',
 				envVariables: '环境变量数'
 			},
-			searchPlaceholder: '搜索当前页的智能体名称或 ID',
+			searchPlaceholder: '搜索',
 			pageInfo: '第 {current} / {total} 页',
 			pageSize: '{size} 条/页',
 			badges: {
@@ -2583,7 +2584,8 @@ export default {
 				chunkSizeTip: '控制每个文档分块的字符数(100-4000)。',
 				chunkOverlap: '分块重叠',
 				chunkOverlapTip: '相邻文档块之间的重叠字符数(0-500)。',
-				chunkTipAuto: '文档分析器根据内容结构自动在「按标题切分」「结构感知」「按长度切分」之间选择。',
+				chunkTipAuto:
+					'文档分析器根据内容结构自动在「按标题切分」「结构感知」「按长度切分」之间选择。',
 				chunkTipHeading:
 					'在 Markdown 标题(#、##、###)边界处切分,每块自动带上所在标题路径。适合结构清晰的 Markdown 文档。',
 				chunkTipHeuristic:
@@ -2702,102 +2704,102 @@ export default {
 		}
 	},
 	nodes: {
-	groups: {
-		start: '开始',
-		data: '数据处理',
-		logic: '业务逻辑',
-		tool: '工具',
-		other: '其他',
-		custom: '自定义'
-	},
-	meta: {
-		'module-invoke': { displayName: '模块调用', description: '通过接口代码调用模块能力' },
-		'basic-dataset': {
-			displayName: '基础数据集',
-			description: '从配置好的基础数据集中读取数据'
-		},
-		'knowledge-retrieval': {
-			displayName: '知识检索',
-			description: '从知识库或指定知识文件中检索相关文本片段'
-		},
-		'ai-agent': {
-			displayName: '智能体',
-			description: '通过智能体配置执行问答与推理'
-		},
-		'view-data': { displayName: '视图数据', description: '从配置好的视图中读取数据' },
-		llm: {
-			displayName: 'LLM',
-			description: '调用大语言模型回答问题或处理自然语言'
-		},
-		start: { displayName: '用户输入', description: '用户输入节点,用于接收用户输入' },
-		end: { displayName: '输出', description: '流程结束并输出节点' },
-		'http-request': { displayName: 'HTTP 请求', description: '通过 HTTP 请求获取数据' },
-		'if-else': { displayName: '条件判断', description: '根据条件判断' },
-		database: { displayName: '数据查询', description: '通过数据库查询数据' },
-		code: { displayName: '代码', description: '通过代码处理数据' },
-		iteration: { displayName: '迭代', description: '迭代节点' },
-		loop: { displayName: '循环', description: '循环节点' },
-		'list-operator': { displayName: '列表操作', description: '列表操作节点' },
-		'question-classifier': { displayName: '问题分类', description: '将问题划分到预设分类中' },
-		'loop-end': { displayName: '退出循环', description: '用于退出迭代或者循环' },
-		'trigger-schedule': { displayName: '定时触发', description: '基于时间配置触发工作流' },
-		'trigger-webhook': {
-			displayName: 'Webhook 触发',
-			description: '通过 Webhook 接收第三方系统请求并触发工作流'
-		},
-		'loop-start': { displayName: '循环开始' },
-		'iteration-start': { displayName: '迭代开始' },
-		stickyNote: { displayName: '注释', description: 'Markdown 注释块' },
-		'mail-sender': { displayName: '邮件发送', description: '通过邮件发送信息' },
-		'sms-sender': { displayName: '短信发送', description: '通过短信发送信息' },
-		'workflow-approval': {
-			displayName: '流程审批',
-			description: '根据用户和岗位信息发起流程审批'
-		}
-	},
-	outputs: {
-		http: {
-			body: '响应内容',
-			statusCode: '响应状态码',
-			headers: '响应头列表 JSON'
-		},
-		webhook: {
-			rawRequestBody: 'Webhook 原始请求体'
-		},
-		database: {
-			rows: '查询结果行',
-			rowCount: '查询结果总行数'
-		},
-		'view-data': {
-			viewTable: '视图数据结果',
-			viewName: '视图名称',
-			totalCount: '总数量'
-		},
-		'knowledge-retrieval': {
-			result: '检索命中的文本片段列表',
-			content: '拼接后的检索内容'
-		},
-		'ai-agent': {
-			text: '生成内容',
-			think: '推理内容'
-		},
-		llm: {
-			text: '生成内容',
-			reasoningContent: '推理内容',
-			usage: '模型用量信息',
-			structuredOutput: '结构化对象'
-		},
-		list: {
-			result: '过滤结果',
-			firstRecord: '第一条记录',
-			lastRecord: '最后一条记录'
-		},
-		condition: {
-			casePrefix: '条件_'
+		groups: {
+			start: '开始',
+			data: '数据处理',
+			logic: '业务逻辑',
+			tool: '工具',
+			other: '其他',
+			custom: '自定义'
+		},
+		meta: {
+			'module-invoke': { displayName: '模块调用', description: '通过接口代码调用模块能力' },
+			'basic-dataset': {
+				displayName: '基础数据集',
+				description: '从配置好的基础数据集中读取数据'
+			},
+			'knowledge-retrieval': {
+				displayName: '知识检索',
+				description: '从知识库或指定知识文件中检索相关文本片段'
+			},
+			'ai-agent': {
+				displayName: '智能体',
+				description: '通过智能体配置执行问答与推理'
+			},
+			'view-data': { displayName: '视图数据', description: '从配置好的视图中读取数据' },
+			llm: {
+				displayName: 'LLM',
+				description: '调用大语言模型回答问题或处理自然语言'
+			},
+			start: { displayName: '用户输入', description: '用户输入节点,用于接收用户输入' },
+			end: { displayName: '输出', description: '流程结束并输出节点' },
+			'http-request': { displayName: 'HTTP 请求', description: '通过 HTTP 请求获取数据' },
+			'if-else': { displayName: '条件判断', description: '根据条件判断' },
+			database: { displayName: '数据查询', description: '通过数据库查询数据' },
+			code: { displayName: '代码', description: '通过代码处理数据' },
+			iteration: { displayName: '迭代', description: '迭代节点' },
+			loop: { displayName: '循环', description: '循环节点' },
+			'list-operator': { displayName: '列表操作', description: '列表操作节点' },
+			'question-classifier': { displayName: '问题分类', description: '将问题划分到预设分类中' },
+			'loop-end': { displayName: '退出循环', description: '用于退出迭代或者循环' },
+			'trigger-schedule': { displayName: '定时触发', description: '基于时间配置触发工作流' },
+			'trigger-webhook': {
+				displayName: 'Webhook 触发',
+				description: '通过 Webhook 接收第三方系统请求并触发工作流'
+			},
+			'loop-start': { displayName: '循环开始' },
+			'iteration-start': { displayName: '迭代开始' },
+			stickyNote: { displayName: '注释', description: 'Markdown 注释块' },
+			'mail-sender': { displayName: '邮件发送', description: '通过邮件发送信息' },
+			'sms-sender': { displayName: '短信发送', description: '通过短信发送信息' },
+			'workflow-approval': {
+				displayName: '流程审批',
+				description: '根据用户和岗位信息发起流程审批'
+			}
 		},
-		'question-classifier': {
-			classPrefix: '分类'
+		outputs: {
+			http: {
+				body: '响应内容',
+				statusCode: '响应状态码',
+				headers: '响应头列表 JSON'
+			},
+			webhook: {
+				rawRequestBody: 'Webhook 原始请求体'
+			},
+			database: {
+				rows: '查询结果行',
+				rowCount: '查询结果总行数'
+			},
+			'view-data': {
+				viewTable: '视图数据结果',
+				viewName: '视图名称',
+				totalCount: '总数量'
+			},
+			'knowledge-retrieval': {
+				result: '检索命中的文本片段列表',
+				content: '拼接后的检索内容'
+			},
+			'ai-agent': {
+				text: '生成内容',
+				think: '推理内容'
+			},
+			llm: {
+				text: '生成内容',
+				reasoningContent: '推理内容',
+				usage: '模型用量信息',
+				structuredOutput: '结构化对象'
+			},
+			list: {
+				result: '过滤结果',
+				firstRecord: '第一条记录',
+				lastRecord: '最后一条记录'
+			},
+			condition: {
+				casePrefix: '条件_'
+			},
+			'question-classifier': {
+				classPrefix: '分类'
+			}
 		}
 	}
-	}
 }

+ 2 - 5
apps/web/src/main.ts

@@ -12,7 +12,6 @@ import 'virtual:svg-icons-register'
 import { initTheme } from '@/theme'
 import { lightTheme } from '@/theme/light'
 import { darkTheme } from '@/theme/dark'
-import { initializeCustomNodes } from '@/nodes/custom/initialize-custom-nodes'
 import { installPermissionDirective } from '@/directives/permission'
 import '@/api/interceptor'
 // import 'monaco-editor/esm/vs/editor/editor.main.css';
@@ -20,9 +19,7 @@ initTheme(lightTheme, darkTheme)
 import 'normalize.css'
 import 'virtual:uno.css'
 
-const bootstrap = async () => {
-	await initializeCustomNodes()
-
+const bootstrap = () => {
 	const app = createApp(App)
 	app.use(store)
 	app.use(router)
@@ -35,4 +32,4 @@ const bootstrap = async () => {
 	app.mount('#app')
 }
 
-void bootstrap()
+bootstrap()

+ 1 - 1
apps/web/src/nodes/custom/parser.ts

@@ -93,7 +93,7 @@ export const toCustomNodeType = (raw: CustomNodeSource): INodeType | null => {
 		version: ['1'],
 		group: 'custom',
 		name: nodeType,
-		displayName: raw.identity.label,
+		displayName: raw.identity.name || raw.identity.label || nodeType,
 		description: raw.identity?.description,
 		icon: resolveCustomIcon(raw.identity?.icon40),
 		iconColor: DEFAULT_NODE_ICON_COLOR,

+ 0 - 6
apps/web/src/views/WorkflowExecution.vue

@@ -110,12 +110,6 @@
 					min-width="220"
 					show-overflow-tooltip
 				/>
-				<el-table-column
-					prop="executionId"
-					:label="t('pages.execution.table.executionId')"
-					min-width="260"
-					show-overflow-tooltip
-				/>
 				<el-table-column
 					prop="startedAt"
 					:label="t('pages.execution.table.startedAt')"

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

@@ -1,5 +1,5 @@
 // src/views/chat/api/chat.api.ts
-import { agentApplication, aiChat, aiModel, knowledge } from '@repo/api-service'
+import { agentApplication, aiChat, aiModel, knowledge, agent } from '@repo/api-service'
 import type { ChatTargetConfig, ChatTargetType } from '../types'
 
 export interface ChatRequestParams {
@@ -113,6 +113,11 @@ export async function getSessionMessages(
 	return aiChat.postSessionSessionMessages({ sessionId, pageIndex, pageSize })
 }
 
+/**
+ * 获取智能体列表
+ * @param keyword
+ * @returns
+ */
 export async function getAgentOptions(keyword = ''): Promise<ChatOptionItem[]> {
 	const res = await agentApplication.postAiAgentSelectList({
 		keyword,
@@ -125,7 +130,30 @@ export async function getAgentOptions(keyword = ''): Promise<ChatOptionItem[]> {
 		value: item.id || '',
 		description: item.description,
 		type: item.mode,
-		raw: item
+		raw: item,
+		// 选项类型
+		optionType: 'agent'
+	}))
+}
+
+/**
+ * 获取编排列表
+ * @param keyword
+ * @returns
+ */
+export async function getFlowOptions(keyword = ''): Promise<ChatOptionItem[]> {
+	const res = await agent.postAgentSelectList({
+		keyword,
+		mode: '',
+		type: ''
+	})
+
+	return (res.result || []).map((item) => ({
+		label: item.name || item.id || '',
+		value: item.id || '',
+		raw: item,
+		// 选项类型
+		optionType: 'flow'
 	}))
 }
 

+ 16 - 1
apps/web/src/views/editor/Editor.vue

@@ -58,7 +58,12 @@
 
 		<el-splitter layout="vertical" class="flex-1">
 			<el-splitter-panel>
-				<NodeView :key="workflow.id" :workflow="workflow" :reload-workflow="loadAgentWorkflow" />
+				<NodeView
+					v-if="customNodesReady"
+					:key="workflow.id"
+					:workflow="workflow"
+					:reload-workflow="loadAgentWorkflow"
+				/>
 			</el-splitter-panel>
 
 			<el-splitter-panel v-model:size.lazy="footerHeight" :min="32">
@@ -78,6 +83,7 @@ import EditorFooter from '@/features/editorFooter/index.vue'
 import NodeView from './NodeView.vue'
 import PublishBtn from './PublishBtn.vue'
 import { nodeMap } from '@/nodes'
+import { initializeCustomNodes } from '@/nodes/custom/initialize-custom-nodes'
 import { IconButton, Input } from '@repo/ui'
 import i18n from '@/i18n'
 import { useI18n } from '@/composables/useI18n'
@@ -99,8 +105,15 @@ const inputRef = ref<InstanceType<typeof Input>>()
 const saveAgentTimer = ref<number | undefined>(undefined)
 const saveVarsTimer = ref<number | undefined>(undefined)
 const isHydrating = ref(false)
+const customNodesReady = ref(false)
 const notifyTimestamps = new Map<string, number>()
 const workflowRefreshKey = 'workflow-list-refresh'
+let customNodesInitialization: Promise<void> | null = null
+
+const ensureCustomNodesInitialized = () => {
+	customNodesInitialization ||= initializeCustomNodes()
+	return customNodesInitialization
+}
 
 const id = route.params?.id as string
 const projectMap = JSON.parse(localStorage.getItem('workflow-map') || '{}') as Record<
@@ -361,6 +374,8 @@ watch(
 	() => route.params?.id,
 	async (nextId) => {
 		if (nextId) {
+			await ensureCustomNodesInitialized()
+			customNodesReady.value = true
 			await loadAgentWorkflow(nextId as string)
 		}
 	},

+ 7 - 3
apps/web/src/views/flow/index.vue

@@ -3,7 +3,7 @@
 		<div class="page-header">
 			<div class="header-title">
 				<h1>{{ t('sidebar.menu.orchestration') }}</h1>
-				<p>{{ t('pages.webSearch.subtitle') }}</p>
+				<p>{{ t('pages.management.subtitle') }}</p>
 			</div>
 		</div>
 
@@ -131,11 +131,15 @@
 								<div class="agent-name" :title="row.name || fallbackText.unnamed">
 									{{ row.name || fallbackText.unnamed }}
 								</div>
-								<div class="agent-id" :title="row.id">{{ row.id }}</div>
 							</div>
 						</div>
 
 						<div class="card-footer">
+							<div>
+								<el-tag v-if="row.status === 'published'" type="success">{{
+									t('pages.editor.status.published')
+								}}</el-tag>
+							</div>
 							<el-button type="primary" @click.stop="openAgent(row.id)">
 								{{ t('common.open') }}
 							</el-button>
@@ -708,7 +712,7 @@ onMounted(() => {
 
 .card-footer {
 	display: flex;
-	justify-content: flex-end;
+	justify-content: space-between;
 	margin-top: auto;
 	padding-top: 18px;
 }

+ 2 - 1
apps/web/src/views/model/index.vue

@@ -107,6 +107,7 @@
 														v-if="row.source === 'remote'"
 														v-permission="'edit'"
 														@click="updateModelCredentials(row.id)"
+														divided
 													>
 														{{ t('pages.model.updateCredentials') }}
 													</el-dropdown-item>
@@ -114,7 +115,6 @@
 														v-if="row.source === 'remote'"
 														v-permission="'edit'"
 														@click="deleteModelCredentials(row.id)"
-														divided
 													>
 														<span class="danger-text">{{
 															t('pages.model.deleteCredentials')
@@ -123,6 +123,7 @@
 													<el-dropdown-item
 														v-permission="'del'"
 														@click="deleteModelConfirm(row.id)"
+														divided
 													>
 														<span class="danger-text">{{ t('common.delete') }}</span>
 													</el-dropdown-item>

+ 5 - 5
apps/web/src/views/web-search/index.vue

@@ -80,17 +80,17 @@
 											<el-dropdown-item v-permission="'edit'" @click="openEditById(row.id)">{{
 												t('common.edit')
 											}}</el-dropdown-item>
-											<el-dropdown-item v-permission="'edit'" @click="updateCredentials(row.id)">
-												{{ t('pages.webSearch.updateCredential') }}
-											</el-dropdown-item>
 											<el-dropdown-item
 												v-permission="'edit'"
-												@click="deleteCredentials(row.id)"
+												@click="updateCredentials(row.id)"
 												divided
 											>
+												{{ t('pages.webSearch.updateCredential') }}
+											</el-dropdown-item>
+											<el-dropdown-item v-permission="'edit'" @click="deleteCredentials(row.id)">
 												<span class="danger-text">{{ t('pages.webSearch.deleteCredential') }}</span>
 											</el-dropdown-item>
-											<el-dropdown-item v-permission="'del'" @click="removeItem(row.id)">
+											<el-dropdown-item v-permission="'del'" @click="removeItem(row.id)" divided>
 												<span class="danger-text">{{ t('common.delete') }}</span>
 											</el-dropdown-item>
 										</el-dropdown-menu>

+ 94 - 0
packages/api-service/schema/ai.openapi.json

@@ -374,6 +374,100 @@
 				"security": []
 			}
 		},
+		"/api/ai/chat/workflow-chat": {
+			"post": {
+				"summary": "基于智能编排聊天",
+				"deprecated": false,
+				"description": "",
+				"tags": ["ai-chat"],
+				"parameters": [
+					{
+						"name": "Authorization",
+						"in": "header",
+						"description": "",
+						"example": "bpm_backend_1523697930081406976",
+						"schema": {
+							"type": "string"
+						}
+					}
+				],
+				"requestBody": {
+					"content": {
+						"application/json": {
+							"schema": {
+								"type": "object",
+								"properties": {
+									"session_id": {
+										"type": "string"
+									},
+									"query": {
+										"type": "string"
+									},
+									"agent_id": {
+										"type": "string"
+									},
+									"files": {
+										"type": "array",
+										"items": {
+											"type": "string"
+										}
+									},
+									"params": {
+										"type": "object",
+										"properties": {}
+									}
+								},
+								"required": ["session_id", "query", "agent_id", "files", "params"]
+							},
+							"examples": {
+								"1": {
+									"value": {
+										"session_id": "c79a6c8b-8eb3-4f70-b528-e07aacb695c5",
+										"query": "你好,你是谁?",
+										"agent_id": "b3a4aabb-a6b8-47f3-8a32-f45930f7d7b8",
+										"files": [],
+										"params": {}
+									},
+									"summary": "文本"
+								},
+								"2": {
+									"value": {
+										"session_id": "ecc13f07-ed31-424e-990c-d5cb53d16374",
+										"query": "帮我分析一下图片中有哪些信息?",
+										"knowledge_base_ids": [],
+										"knowledge_ids": [],
+										"agent_id": "7ccae457-69e2-4cf6-9335-a5d1fa052a2b",
+										"summary_model_id": "",
+										"disable_title": false,
+										"enable_memory": true,
+										"images": ["f2048d34-0cee-4850-a280-70707f3d14e7"],
+										"agent_enabled": true,
+										"web_search_enabled": false
+									},
+									"summary": "图片分析"
+								}
+							}
+						}
+					},
+					"required": true
+				},
+				"responses": {
+					"200": {
+						"description": "",
+						"content": {
+							"application/json": {
+								"schema": {
+									"type": "object",
+									"properties": {}
+								}
+							}
+						},
+						"headers": {}
+					}
+				},
+				"security": []
+			}
+		},
 		"/api/ai/session/pageList": {
 			"post": {
 				"summary": "获取分页列表",

+ 21 - 0
packages/api-service/servers/ai-chat/api/aiChat.ts

@@ -94,6 +94,27 @@ export async function postChatStopAnswer(
   )
 }
 
+/** 基于智能编排聊天 POST /api/ai/chat/workflow-chat */
+export async function postChatWorkflowChat(
+  body: {
+    session_id: string
+    query: string
+    agent_id: string
+    files: string[]
+    params: Record<string, any>
+  },
+  options?: { [key: string]: any }
+) {
+  return request<Record<string, any>>('/api/ai/chat/workflow-chat', {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json'
+    },
+    data: body,
+    ...(options || {})
+  })
+}
+
 /** 创建会话 POST /api/ai/session/create */
 export async function postSessionCreate(
   body: {