Kaynağa Gözat

feat: 联调智能编排对话

jiaxing.liao 4 gün önce
ebeveyn
işleme
fa8e36f4e6

+ 1 - 1
apps/web/src/features/RunWorkflow/utils.ts

@@ -5,7 +5,7 @@ export const RUN_WORKFLOW_QUERY_VARIABLE = 'query'
 export function createEmptyValue(formType: string) {
 	switch (formType) {
 		case 'number':
-			return 0
+			return undefined
 		case 'checkbox':
 			return false
 		case 'file':

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

@@ -1132,6 +1132,7 @@ export default {
 			settingsSessionParams: 'Session Settings',
 			settingsAgentSwitches: 'Agent Options',
 			selectPlaceholder: 'Select',
+			workflowChatFormTitle: 'Chat Form',
 			selectAgentPlaceholder: 'Select an agent',
 			selectWorkflowPlaceholder: 'Select a workflow',
 			selectKnowledgeBasePlaceholder: 'Select knowledge base',
@@ -1174,6 +1175,7 @@ export default {
 			createSuccess: 'Created successfully',
 			inputRequired: 'Please enter a message',
 			selectAgentFirst: 'Please select an agent first',
+			selectWorkflowFirst: 'Please select a workflow first',
 			selectKnowledgeBaseFirst: 'Please select a knowledge base first',
 			selectSummaryModelFirst: 'Please select a summary model first',
 			requestInProgress: 'A request is already in progress',

+ 3 - 2
apps/web/src/i18n/locales/zh-cn.ts

@@ -1038,6 +1038,7 @@ export default {
 			settingsSessionParams: '会话参数',
 			settingsAgentSwitches: 'Agent 开关',
 			selectPlaceholder: '请选择',
+			workflowChatFormTitle: '编排任务表单',
 			selectAgentPlaceholder: '请选择智能体',
 			selectWorkflowPlaceholder: '请选择编排',
 			selectKnowledgeBasePlaceholder: '选择知识库',
@@ -1046,8 +1047,7 @@ export default {
 			quickStartTitle: '快速开始:',
 			generateLink: '生成链接',
 			shareDialogTitle: '生成访问链接',
-			shareDialogTip:
-				'通过该链接进入对话时会自动使用当前智能体、模型、知识库等配置,无需再次选择。',
+			shareDialogTip: '通过该链接进入对话时会自动使用当前配置,无需再次选择。',
 			copyShareLink: '复制链接',
 			invalidShareLink: '访问链接参数无效',
 			shareLinkCopied: '链接已复制',
@@ -1079,6 +1079,7 @@ export default {
 			createSuccess: '创建成功',
 			inputRequired: '请输入对话内容',
 			selectAgentFirst: '请先选择智能体',
+			selectWorkflowFirst: '请先选择智能编排',
 			selectKnowledgeBaseFirst: '请先选择知识库',
 			selectSummaryModelFirst: '请先选择摘要模型',
 			requestInProgress: '当前有请求正在进行',

+ 5 - 4
apps/web/src/nodes/src/start/setter.vue

@@ -188,6 +188,7 @@
 						<el-input-number
 							v-model="numberDefaultValue"
 							controls-position="right"
+							value-on-clear=""
 							class="w-full"
 						/>
 					</div>
@@ -374,7 +375,7 @@ const dialogFormRef = ref<FormInstance>()
 const createDefaultValue = (formType: FormType): StartVariable['default_value'] => {
 	switch (formType) {
 		case 'number':
-			return 0
+			return undefined
 		case 'checkbox':
 			return false
 		case 'file':
@@ -665,9 +666,9 @@ const stringDefaultValue = computed({
 })
 
 const numberDefaultValue = computed({
-	get: () => (typeof dialogForm.default_value === 'number' ? dialogForm.default_value : 0),
+	get: () => (typeof dialogForm.default_value === 'number' ? dialogForm.default_value : undefined),
 	set: (value: number | null | undefined) => {
-		dialogForm.default_value = Number(value ?? 0)
+		dialogForm.default_value = value == null ? undefined : value
 	}
 })
 
@@ -764,7 +765,7 @@ const normalizedDialogVariable = computed<StartVariable>(() => {
 
 	switch (formType) {
 		case 'number':
-			defaultValue = Number(dialogForm.default_value ?? 0)
+			defaultValue = typeof dialogForm.default_value === 'number' ? dialogForm.default_value : undefined
 			break
 		case 'checkbox':
 			defaultValue = Boolean(dialogForm.default_value)

+ 13 - 1
apps/web/src/views/chat/api/chat.api.ts

@@ -28,6 +28,7 @@ export interface ChatOptionItem {
 export function getChatUrl(type: ChatTargetType) {
 	if (type === 'knowledge') return '/api/ai/chat/knowledge-chat'
 	if (type === 'model') return '/api/ai/chat/model-chat'
+	if (type === 'flow') return '/api/ai/chat/workflow-chat'
 	return '/api/ai/chat/agent-chat'
 }
 
@@ -35,7 +36,8 @@ export function buildChatRequestBody(
 	type: ChatTargetType,
 	config: ChatTargetConfig,
 	sessionId: string,
-	query: string
+	query: string,
+	workflowParams: Record<string, any> = {}
 ) {
 	const base = {
 		session_id: sessionId,
@@ -58,6 +60,16 @@ export function buildChatRequestBody(
 		return base
 	}
 
+	if (type === 'flow') {
+		return {
+			session_id: sessionId,
+			query,
+			agent_id: config.agentId,
+			files: config.images,
+			params: workflowParams
+		}
+	}
+
 	return {
 		...base,
 		knowledge_base_ids: config.knowledgeBaseIds,

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

@@ -3,6 +3,11 @@ import hookFetch from 'hook-fetch'
 import { sseTextDecoderPlugin } from 'hook-fetch/plugins/sse'
 import type { ChatSseMessage } from '../types'
 
+/**
+ * 全局 HTTP 客户端实例
+ * - timeout: 0 表示不设置超时,SSE 流式请求可能持续较长时间
+ * - 挂载 sseTextDecoderPlugin 插件,将服务端返回的字节流解码为 SSE 文本事件
+ */
 const api = hookFetch.create({
 	baseURL: '',
 	timeout: 0,
@@ -12,12 +17,36 @@ const api = hookFetch.create({
 })
 api.use(sseTextDecoderPlugin({ json: false }))
 
+/**
+ * 聊天流式请求 Composable
+ *
+ * 封装 SSE(Server-Sent Events)流式对话的完整生命周期:
+ * - 发起 POST 请求并持续读取响应流
+ * - 解析 SSE 事件并逐块回调给调用方
+ * - 处理正常完成、用户主动取消、网络异常三种终止场景
+ *
+ * @returns isLoading  - 是否正在加载中(响应式,可绑定 UI loading 状态)
+ * @returns cancelRequest - 主动取消当前正在进行的流式请求
+ * @returns streamChat   - 发起一次流式聊天请求
+ */
 export function useChatStream() {
+	/** 当前是否有请求正在进行中,驱动 UI loading 状态 */
 	const isLoading = ref(false)
+	/** 当前活跃的 AbortController,用于中断正在进行的 HTTP 请求 */
 	let activeController: AbortController | null = null
+	/** 自增的流 ID,每次发起新请求时 +1,用于标识和区分不同的流 */
 	let activeStreamId = 0
+	/**
+	 * 已主动取消的流 ID 集合
+	 * 当用户取消请求或发起新请求导致旧流被取消时,将对应 streamId 存入此集合
+	 * 用于在流结束后判断是"主动取消"还是"异常断连",避免误报错误
+	 */
 	const cancelledStreamIds = new Set<number>()
 
+	/**
+	 * 取消当前正在进行的流式请求
+	 * 将当前 streamId 记入取消集合,中断 HTTP 连接,重置 loading 状态
+	 */
 	const cancelRequest = () => {
 		if (activeStreamId) {
 			cancelledStreamIds.add(activeStreamId)
@@ -27,12 +56,24 @@ export function useChatStream() {
 		isLoading.value = false
 	}
 
+	/**
+	 * 解析单个 SSE 事件块为结构化消息数组
+	 *
+	 * SSE 协议中每条消息以 "data:" 前缀开头,多条消息之间用空行分隔。
+	 * 本函数提取所有 data: 行的内容,尝试 JSON 反序列化;
+	 * 若解析失败则降级为纯文本 answer 类型消息。
+	 *
+	 * @param block - 单个 SSE 事件文本块(可能包含多行 data:)
+	 * @returns 解析后的消息数组
+	 */
 	const parseSseEvent = (block: string): ChatSseMessage[] => {
+		// 提取所有 "data:" 前缀的行,去掉前缀后拼接
 		const dataLines = block
 			.split(/\r?\n/)
 			.filter((line) => line.startsWith('data:'))
 			.map((line) => line.replace(/^data:\s?/, ''))
 
+		// 若无 data: 行,将整个块作为纯文本消息处理
 		if (!dataLines.length) {
 			const text = block.trim()
 			return text ? [{ response_type: 'answer', content: text, done: false }] : []
@@ -42,13 +83,25 @@ export function useChatStream() {
 
 		console.log('msg:', data)
 		try {
+			// 尝试 JSON 解析为结构化消息
 			return [JSON.parse(data)]
 		} catch {
+			// JSON 解析失败,降级为纯文本 answer 消息
 			return [{ response_type: 'answer', content: data, done: false }]
 		}
 	}
 
+	/**
+	 * 解析一段完整的 SSE 数据块(可能包含多个事件)
+	 *
+	 * SSE 事件之间以双换行符(\n\n)分隔。
+	 * 若块内包含多个事件,则拆分后逐个调用 parseSseEvent 解析。
+	 *
+	 * @param block - 从流中读取到的原始文本块
+	 * @returns 所有解析后的消息数组
+	 */
 	const parseSseBlock = (block: string): ChatSseMessage[] => {
+		// 以双换行符拆分,识别多个独立 SSE 事件
 		const eventBlocks = block
 			.split(/\r?\n\r?\n/)
 			.map((item) => item.trim())
@@ -61,12 +114,33 @@ export function useChatStream() {
 		return parseSseEvent(block)
 	}
 
+	/**
+	 * 从错误响应对象中提取可读的错误信息
+	 * 按优先级依次尝试 error / errors.message / message / msg 字段
+	 *
+	 * @param payload - 服务端返回的错误响应对象
+	 * @returns 错误信息字符串,无匹配时返回空字符串
+	 */
 	const getErrorMessage = (payload: any) => {
 		if (!payload || typeof payload !== 'object') return ''
 		return `${payload.error || payload.errors?.message || payload.message || payload.msg || ''}`.trim()
 	}
 
+	/**
+	 * 检测流式响应中的直接返回错误(非 SSE 格式的错误)
+	 *
+	 * 某些场景下服务端不返回 SSE 流,而是直接返回一个 JSON 错误对象
+	 * (如鉴权失败、参数校验失败等)。本函数负责识别这类错误。
+	 *
+	 * 判断规则:
+	 * - 响应不是标准 SSE 消息(无 response_type / data 字段)
+	 * - 响应包含 isSuccess: false 标记
+	 *
+	 * @param result - 流中读取到的原始结果
+	 * @returns 若为直接错误则返回 Error 对象,否则返回 null
+	 */
 	const normalizeDirectResponseError = (result: unknown) => {
+		// 统一将字符串或对象类型的结果转为 payload 对象
 		const payload =
 			typeof result === 'string'
 				? (() => {
@@ -83,7 +157,9 @@ export function useChatStream() {
 					: null
 
 		if (!payload || Array.isArray(payload)) return null
+		// 包含 SSE 特征字段,属于正常消息,不是直接错误
 		if (payload.response_type || payload.data) return null
+		// 业务层返回失败状态,构造错误对象
 		if (payload.isSuccess === false) {
 			return new Error(getErrorMessage(payload) || 'Request failed')
 		}
@@ -91,6 +167,12 @@ export function useChatStream() {
 		return null
 	}
 
+	/**
+	 * 将各类请求错误统一标准化为 Error 对象
+	 *
+	 * @param error - 原始错误(可能是 Error 实例、HTTP 响应对象或其他类型)
+	 * @returns 标准化后的 Error 对象
+	 */
 	const normalizeRequestError = async (error: any) => {
 		if (!error) return new Error('Unknown error')
 		if (error instanceof Error) return error
@@ -102,6 +184,35 @@ export function useChatStream() {
 		return new Error(message)
 	}
 
+	const isTerminalEvent = (item: ChatSseMessage) => {
+		const cmd = (item as ChatSseMessage & { cmd?: string }).cmd
+		return (
+			item?.response_type === 'complete' ||
+			item?.response_type === 'error' ||
+			cmd === 'CMD_AGENT_FINISH_MSG' ||
+			cmd === 'CMD_AGENT_ERROR_MSG' ||
+			cmd === 'CMD_CONNECT_ERROR_MSG' ||
+			cmd === 'CMD_AGENT_SUSPEND_MSG'
+		)
+	}
+
+	/**
+	 * 发起一次 SSE 流式聊天请求
+	 *
+	 * 完整流程:
+	 * 1. 取消上一次未完成的请求(确保同一时刻只有一个活跃流)
+	 * 2. 构建带鉴权信息的 POST 请求
+	 * 3. 通过 for-await 逐块读取响应流
+	 * 4. 解析每个 SSE 事件并通过 onChunk 回调通知调用方
+	 * 5. 检测到 complete/error 终止事件时结束流
+	 * 6. 区分正常完成、用户取消、异常断连三种终止场景
+	 *
+	 * @param url       - 请求的 API 路径(如 /api/ai/chat/agent-chat)
+	 * @param body      - 请求体对象,将被序列化为 JSON
+	 * @param onChunk   - 每收到一个 SSE 事件时的回调,用于更新 UI 消息
+	 * @param onComplete - 流正常结束时的回调
+	 * @param onError   - 流异常终止时的回调
+	 */
 	const streamChat = async (
 		url: string,
 		body: any,
@@ -109,17 +220,25 @@ export function useChatStream() {
 		onComplete: () => void,
 		onError: (err: Error) => void
 	) => {
+		// 发起新请求前,取消上一次未完成的流(避免并发冲突)
 		cancelRequest()
 
 		isLoading.value = true
+		// 分配新的流 ID,用于本次请求的唯一标识
 		const streamId = activeStreamId + 1
 		activeStreamId = streamId
 		const controller = new AbortController()
 		activeController = controller
+		/** 是否已收到终止事件(complete 或 error) */
 		let completeReceived = false
+		/** 防止 onComplete 被重复调用的标记 */
 		let completed = false
+		/** 终端中止定时器(预留扩展,当前未使用) */
 		let terminalAbortTimer: ReturnType<typeof setTimeout> | null = null
 
+		/**
+		 * 安全地触发完成回调,保证幂等(只调用一次)
+		 */
 		const finishStream = () => {
 			if (!completed) {
 				completed = true
@@ -128,66 +247,86 @@ export function useChatStream() {
 		}
 
 		try {
+			// 从 Cookie 中读取会话鉴权 token(x-sessionId)
 			const token = // localStorage.getItem('oauth2token') ||
 				document.cookie.match(new RegExp('(^| )' + 'x-sessionId' + '=([^;]*)(;|$)'))?.[2]
 
-			// dev读取环境变量 prod使用当前
+			// 开发环境:请求代理到 VITE_BASE_URL 指定的后端地址
+			// 生产环境:使用相对路径,由 Nginx 等反向代理转发
 			const baseUrl = import.meta.env.DEV ? `http://${import.meta.env.VITE_BASE_URL}` : ''
 
+			// 发起 POST 请求,开启流式响应
 			const request = api.post(baseUrl + url, body, {
 				signal: controller.signal,
 				headers: {
 					Authorization: token || ''
 				},
+				mode: 'cors',
 				credentials: 'include'
 			})
 
+			// 逐块读取 SSE 响应流
 			for await (const chunk of request.stream<string>()) {
+				// 处理流式读取过程中出现的错误
 				if (chunk.error) {
 					throw await normalizeRequestError(chunk.error)
 				}
 
+				// 检测服务端直接返回的非 SSE 格式错误(如鉴权失败)
 				const directResponseError = normalizeDirectResponseError(chunk.result)
 				if (directResponseError) {
 					throw directResponseError
 				}
 
+				// 将 chunk 结果转为字符串,跳过空内容
 				const result = typeof chunk.result === 'string' ? chunk.result : `${chunk.result ?? ''}`
 				if (!result.trim()) continue
 
+				// 解析当前数据块中的所有 SSE 事件
 				const results = parseSseBlock(result)
+				// 逐条通知调用方处理
 				results.forEach(onChunk)
-				const hasCompleteEvent = results.some((item) => item?.response_type === 'complete')
-				const hasErrorEvent = results.some((item) => item?.response_type === 'error')
-				const hasTerminalEvent = hasCompleteEvent || hasErrorEvent
+
+				// 检测是否包含终止事件,兼容 response_type 与 runner cmd 两种协议。
+				const hasTerminalEvent = results.some(isTerminalEvent)
 				if (hasTerminalEvent) {
 					completeReceived = true
 					isLoading.value = false
 					finishStream()
+					// 终止 HTTP 连接,释放资源
 					controller.abort()
 					break
 				}
 			}
 
+			// 流结束后:判断是否正常完成
 			if (completeReceived) {
+				// 已收到终止事件,确保 finishStream 被调用(幂等)
 				finishStream()
 			} else if (!cancelledStreamIds.has(streamId)) {
+				// 未收到终止事件,且不是用户主动取消 → 异常断连,抛出错误
 				throw new Error('Response stream ended before complete')
 			}
+			// 若 streamId 在 cancelledStreamIds 中,说明是主动取消,静默结束,不报错
 		} catch (error: any) {
 			if (error?.name === 'AbortError' && (completeReceived || cancelledStreamIds.has(streamId))) {
+				// AbortError 来自正常的完成或主动取消,调用 finishStream 收尾
 				finishStream()
 			} else if (error?.name !== 'AbortError') {
+				// 非 AbortError 的真实网络错误,通知调用方
 				onError(await normalizeRequestError(error))
 			}
 		} finally {
+			// 清理:无论成功或失败,都必须执行的收尾逻辑
 			if (terminalAbortTimer) {
 				clearTimeout(terminalAbortTimer)
 			}
+			// 仅当当前流仍是最新活跃流时才重置全局状态,避免覆盖新请求的状态
 			if (activeStreamId === streamId) {
 				activeController = null
 				isLoading.value = false
 			}
+			// 从取消集合中移除当前流 ID,释放内存
 			cancelledStreamIds.delete(streamId)
 		}
 	}

+ 608 - 79
apps/web/src/views/chat/index.vue

@@ -16,9 +16,47 @@
 		<div class="chat-main">
 			<ChatHeader :title="activeConversationTitle">
 				<template #actions>
+					<!-- 展示/隐藏表单 -->
+					<el-popover
+						:visible="showForm"
+						placement="bottom"
+						trigger="manual"
+						:width="520"
+						popper-class="workflow-form-popper"
+					>
+						<div v-loading="workflowFormLoading" class="workflow-form">
+							<div class="workflow-form__title">{{ t('pages.chat.workflowChatFormTitle') }}</div>
+							<InputTab
+								:start-node="workflowFormStartNode"
+								:visible-variables="workflowVisibleVariables"
+								:input-values="workflowInputValues"
+								:json-drafts="workflowJsonDrafts"
+								:validation-errors="workflowValidationErrors"
+								:is-running="isLoading"
+								:show-action-bar="false"
+								:pane-style="workflowFormPaneStyle"
+							/>
+						</div>
+						<template #reference>
+							<IconButton
+								v-if="hasVisibleForm"
+								icon="lucide:sliders-horizontal"
+								:type="showForm ? 'primary' : 'default'"
+								:icon-color="showForm ? '#fff' : undefined"
+								height="18"
+								width="18"
+								@click="showForm = !showForm"
+								size="small"
+								square
+							/>
+						</template>
+					</el-popover>
+					<!-- 创建连接 -->
 					<el-button v-if="!isPresetMode" @click="shareConversation">
-						<Icon icon="lucide:share-2" />
-						<span>{{ t('pages.chat.generateLink') }}</span>
+						<span class="flex items-center gap-4px">
+							<Icon icon="lucide:share-2" />
+							<span>{{ t('pages.chat.generateLink') }}</span>
+						</span>
 					</el-button>
 				</template>
 			</ChatHeader>
@@ -31,6 +69,12 @@
 				@add-to-kb="handleAddKb"
 				@card-submit="handleSend"
 			>
+				<template #message-extra="{ item }">
+					<div class="min-w-400px">
+						<WorkflowTraceBubble v-if="item.workflowTraceVisible" :message="item" />
+					</div>
+				</template>
+
 				<div
 					v-if="settingsDraft.type === 'agent' && agentPromptsItems?.length"
 					style="margin-top: 12px; width: 80%"
@@ -53,12 +97,12 @@
 				@submit="handleSend"
 				@cancel="handleCancel"
 			>
-				<template #header>
-					<div v-if="!isPresetMode" class="px-8px py-12px flex items-center gap-12px">
+				<template #header v-if="!isPresetMode && settingsDraft.type !== 'flow'">
+					<div class="px-8px py-12px flex items-center gap-12px">
 						<div class="flex items-center gap-4px">
 							<!-- <div class="title">知识库:</div> -->
 							<el-select
-								class="w-180px"
+								class="w-180px!"
 								:placeholder="t('pages.chat.selectKnowledgeBasePlaceholder')"
 								v-model="settingsDraft.knowledgeBaseIds"
 								:options="knowledgeBaseOptions"
@@ -69,7 +113,7 @@
 						<div class="flex items-center gap-4px">
 							<!-- <div class="title">知识:</div> -->
 							<el-select
-								class="w-180px"
+								class="w-180px!"
 								:placeholder="t('pages.chat.selectKnowledgePlaceholder')"
 								v-model="settingsDraft.knowledgeIds"
 								:options="knowledgeOptions"
@@ -79,23 +123,17 @@
 					</div>
 				</template>
 				<template #prefix-extra>
-					<!-- 智能体 -->
-					<el-select
+					<!-- 智能体与智能体编排 -->
+					<el-cascader
 						v-if="!isPresetMode"
-						class="w-180px"
-						:placeholder="t('pages.chat.selectAgentPlaceholder')"
-						v-model="settingsDraft.agentId"
-						:options="agentOptions"
-						@change="handleAgentSelectChange"
+						v-model="targetSelection"
+						:options="getTargetOptions"
+						:placeholder="t('pages.chat.selectPlaceholder')"
+						popper-class="target-popper"
+						class="chat-target-cascader"
+						@change="handleChangeTarget"
+						filterable
 					/>
-					<!-- 编排 -->
-					<!-- <el-select
-						v-if="!isPresetMode"
-						class="w-180px"
-						:placeholder="t('pages.chat.selectWorkflowPlaceholder')"
-						v-model="settingsDraft"
-						:options="agentOptions"
-					/> -->
 					<!-- 附件 -->
 					<el-badge :value="currentAttachments.length" :hidden="!currentAttachments.length">
 						<el-button
@@ -111,14 +149,15 @@
 						</el-button>
 					</el-badge>
 				</template>
+
 				<template #action>
 					<el-select
-						v-if="!isPresetMode"
+						v-if="!isPresetMode && settingsDraft.type !== 'flow'"
 						:placeholder="t('pages.chat.selectModelPlaceholder')"
 						placement="top"
 						v-model="settingsDraft.summaryModelId"
 						:options="modelOptions"
-						class="w-120px"
+						class="w-120px!"
 					/>
 				</template>
 			</ChatInput>
@@ -186,6 +225,8 @@ import { ElMessage, ElMessageBox } from 'element-plus'
 import { PictureFilled } from '@element-plus/icons-vue'
 import { useI18n } from '@/composables/useI18n'
 import FileUploadInput from '@/features/fileUpload/FileUploadInput.vue'
+import InputTab from '@/features/RunWorkflow/components/InputTab.vue'
+import { buildExecuteParams, createEmptyValue } from '@/features/RunWorkflow/utils'
 import { useChatStream } from './composables/useChatStream'
 import { Prompts } from 'vue-element-plus-x'
 import {
@@ -194,6 +235,7 @@ import {
 	deleteSession,
 	getAgentInfo,
 	getAgentOptions,
+	getFlowOptions,
 	getChatUrl,
 	getKnowledgeBaseOptions,
 	getModelOptions,
@@ -207,7 +249,8 @@ import ChatInput from '@/components/Chat/ChatInput.vue'
 import ChatSidebar from './components/ChatSidebar.vue'
 import MessageList from '@/components/Chat/MessageList.vue'
 import AddKbModal from './components/AddKbModal.vue'
-import { agentApplication, knowledge, aiChat } from '@repo/api-service'
+import WorkflowTraceBubble from '@/features/ChatDrawer/WorkflowTraceBubble.vue'
+import { agent, agentApplication, knowledge, aiChat } from '@repo/api-service'
 
 import type {
 	BubbleMessage,
@@ -221,7 +264,11 @@ import type {
 } from './types'
 import type { PromptsItemsProps } from 'vue-element-plus-x/types/Prompts'
 import type { WorkflowUploadFile } from '@/features/fileUpload/shared'
-import { Icon } from '@repo/ui'
+import type { CSSProperties } from 'vue'
+import type { IWorkflowNode } from '@repo/workflow'
+import type { FileType, FormType, StartVariable } from '@/nodes/src/start'
+import type { RunnerNodeState } from '@/store/modules/runner.store'
+import { Icon, IconButton } from '@repo/ui'
 
 const { t } = useI18n()
 const { isLoading, cancelRequest, streamChat } = useChatStream()
@@ -235,6 +282,8 @@ const imageUploadDialogVisible = ref(false)
 const currentAttachments = ref<WorkflowUploadFile[]>([])
 const allowImageUpload = ref(false)
 const agentOptions = ref<ChatOptionItem[]>([])
+const flowOptions = ref<ChatOptionItem[]>([])
+const targetSelection = ref<[ChatTargetType, string]>(['agent', ''])
 const knowledgeBaseOptions = ref<ChatOptionItem[]>([])
 const knowledgeOptions = ref<{ label: string; value: string }[]>([])
 const modelOptions = ref<ChatOptionItem[]>([])
@@ -256,8 +305,52 @@ const shareDialogVisible = ref(false)
 const shareUrl = ref('')
 const presetConfig = ref<ChatTargetConfig | null>(null)
 const isPresetMode = computed(() => !!presetConfig.value)
+const showForm = ref(false)
+const workflowFormLoading = ref(false)
+const workflowStartVariables = ref<StartVariable[]>([])
+const workflowInputValues = reactive<Record<string, any>>({})
+const workflowJsonDrafts = reactive<Record<string, string>>({})
+const workflowValidationErrors = reactive<Record<string, string>>({})
+const workflowFormPaneStyle: CSSProperties = {
+	marginTop: '0',
+	padding: '0'
+}
 let scrollToBottomPending = false
 
+// 联动下拉列表
+const getTargetOptions = computed(() => {
+	return [
+		{
+			label: t('pages.chat.settingsAgent'),
+			value: 'agent',
+			children: agentOptions.value
+		},
+		{
+			label: t('sidebar.menu.orchestration'),
+			value: 'flow',
+			children: flowOptions.value
+		}
+	]
+})
+
+/**
+ * 解析当前级联选择器的显示信息(类型标签、名称、Tag 颜色)
+ */
+const selectionDisplayInfo = computed(() => {
+	const [type, id] = targetSelection.value
+	if (!type || !id) return null
+
+	const isAgent = type === 'agent'
+	const typeLabel = isAgent ? t('pages.chat.settingsAgent') : t('sidebar.menu.orchestration')
+	const options = isAgent ? agentOptions.value : flowOptions.value
+	const matched = options.find((o) => o.value === id)
+
+	return {
+		typeLabel,
+		name: matched?.label || id,
+		tagType: isAgent ? 'success' : 'warning'
+	}
+})
 const scrollToBottom = async () => {
 	if (scrollToBottomPending) return
 	scrollToBottomPending = true
@@ -288,10 +381,13 @@ const activeConversationTitle = computed(() => {
 
 /**
  * 判断是否显示图片上传按钮
- * 仅在类型为 agent 且允许上传图片时显示
+ * agent 模式:根据智能体配置决定是否显示
+ * flow 模式:始终显示
  */
 const showImageUploadButton = computed(
-	() => activeTargetType.value === 'agent' && allowImageUpload.value
+	() =>
+		(activeTargetType.value === 'agent' && allowImageUpload.value) ||
+		activeTargetType.value === 'flow'
 )
 
 /**
@@ -299,9 +395,9 @@ const showImageUploadButton = computed(
  */
 const currentChatConfig = computed<ChatTargetConfig>(() => ({
 	type: activeTargetType.value,
-	knowledgeBaseIds: [...settingsDraft.knowledgeBaseIds],
-	knowledgeIds: [...settingsDraft.knowledgeIds],
-	summaryModelId: settingsDraft.summaryModelId,
+	knowledgeBaseIds: activeTargetType.value === 'flow' ? [] : [...settingsDraft.knowledgeBaseIds],
+	knowledgeIds: activeTargetType.value === 'flow' ? [] : [...settingsDraft.knowledgeIds],
+	summaryModelId: activeTargetType.value === 'flow' ? '' : settingsDraft.summaryModelId,
 	disableTitle: settingsDraft.disableTitle,
 	enableMemory: settingsDraft.enableMemory,
 	agentId: settingsDraft.agentId,
@@ -318,6 +414,10 @@ const getDefaultAgentId = () => {
 	return agentOptions.value[0]?.value || ''
 }
 
+const getDefaultFlowId = () => {
+	return flowOptions.value[0]?.value || ''
+}
+
 onMounted(async () => {
 	presetConfig.value = parsePresetConfigFromRoute()
 	await loadChatOptions()
@@ -326,11 +426,12 @@ onMounted(async () => {
 		return
 	}
 	await loadConversations()
-	if (conversations.value.length > 0 && !activeConversationId.value) {
-		activeConversationId.value = conversations.value?.[0]?.id!
-		await loadConversationMessages(activeConversationId.value)
-		await scrollToBottom()
-	}
+	// 默认选中第一个会话
+	// if (conversations.value.length > 0 && !activeConversationId.value) {
+	// 	activeConversationId.value = conversations.value?.[0]?.id!
+	// 	await loadConversationMessages(activeConversationId.value)
+	// 	await scrollToBottom()
+	// }
 })
 
 /**
@@ -346,7 +447,7 @@ const createDefaultConfig = (type: ChatTargetType = 'agent'): ChatTargetConfig =
 		summaryModelId: '',
 		disableTitle: false,
 		enableMemory: true,
-		agentId: type === 'agent' ? getDefaultAgentId() : '',
+		agentId: type === 'flow' ? getDefaultFlowId() : type === 'agent' ? getDefaultAgentId() : '',
 		agentEnabled: true,
 		webSearchEnabled: false,
 		images: []
@@ -354,6 +455,166 @@ const createDefaultConfig = (type: ChatTargetType = 'agent'): ChatTargetConfig =
 }
 
 const settingsDraft = reactive<ChatTargetConfig>(createDefaultConfig('agent'))
+const workflowVisibleVariables = computed(() =>
+	workflowStartVariables.value.filter((item) => !item.is_hide)
+)
+const workflowFormStartNode = computed<IWorkflowNode | null>(() =>
+	settingsDraft.type === 'flow'
+		? ({
+				id: 'chat-workflow-start',
+				type: 'canvas-node',
+				position: { x: 0, y: 0 },
+				data: {
+					nodeType: 'start',
+					variables: workflowStartVariables.value
+				}
+			} as unknown as IWorkflowNode)
+		: null
+)
+const hasVisibleForm = computed(
+	() => settingsDraft.type === 'flow' && workflowVisibleVariables.value.length > 0
+)
+
+const resetWorkflowFormValidation = () => {
+	Object.keys(workflowValidationErrors).forEach((key) => {
+		delete workflowValidationErrors[key]
+	})
+}
+
+const resetWorkflowFormState = () => {
+	workflowStartVariables.value = []
+	Object.keys(workflowInputValues).forEach((key) => {
+		delete workflowInputValues[key]
+	})
+	Object.keys(workflowJsonDrafts).forEach((key) => {
+		delete workflowJsonDrafts[key]
+	})
+	resetWorkflowFormValidation()
+}
+
+const formatJsonDraft = (value: unknown, fallback = '{}') => {
+	if (value === undefined || value === null || value === '') return fallback
+	try {
+		return JSON.stringify(value, null, 2)
+	} catch {
+		return fallback
+	}
+}
+
+const normalizeFormType = (formType?: string): FormType => {
+	const allowed = new Set<FormType>([
+		'text-input',
+		'text-area',
+		'select',
+		'number',
+		'checkbox',
+		'file',
+		'file-list',
+		'json_object'
+	])
+	return allowed.has(formType as FormType) ? (formType as FormType) : 'text-input'
+}
+
+const normalizeWorkflowVariable = (item: Record<string, any>): StartVariable | null => {
+	const name = `${item?.name || ''}`.trim()
+	if (!name) return null
+
+	const formType = normalizeFormType(item.formType)
+	return {
+		name,
+		label: item.label || name,
+		max_length: item.max_length,
+		default_value: item.default_value,
+		json: item.json,
+		is_require: !!item.is_require,
+		is_hide: !!item.is_hide,
+		formType,
+		options: Array.isArray(item.options) ? item.options : [],
+		file_types: Array.isArray(item.file_types) ? (item.file_types as FileType[]) : [],
+		file_extensions: Array.isArray(item.file_extensions) ? item.file_extensions : [],
+		allow_link_input: item.allow_link_input
+	}
+}
+
+const initializeWorkflowFormValues = () => {
+	Object.keys(workflowInputValues).forEach((key) => {
+		delete workflowInputValues[key]
+	})
+	Object.keys(workflowJsonDrafts).forEach((key) => {
+		delete workflowJsonDrafts[key]
+	})
+
+	workflowStartVariables.value.forEach((variable) => {
+		const initialValue =
+			variable.default_value !== undefined
+				? structuredClone(variable.default_value)
+				: createEmptyValue(variable.formType)
+
+		if (variable.formType === 'json_object') {
+			workflowInputValues[variable.name] =
+				initialValue && typeof initialValue === 'object' && !Array.isArray(initialValue)
+					? initialValue
+					: {}
+			workflowJsonDrafts[variable.name] = formatJsonDraft(workflowInputValues[variable.name], '{}')
+			return
+		}
+
+		workflowInputValues[variable.name] = initialValue
+	})
+}
+
+const loadWorkflowExecuteInfo = async (id: string) => {
+	resetWorkflowFormState()
+	if (!id) return
+
+	workflowFormLoading.value = true
+	try {
+		const res = await agent.postAgentGetExecuteInfo({ id })
+		if (!res?.isSuccess || !res.result) {
+			throw new Error('load workflow execute info failed')
+		}
+
+		workflowStartVariables.value = (res.result.form_variables || [])
+			.map((item) => normalizeWorkflowVariable(item as Record<string, any>))
+			.filter(Boolean) as StartVariable[]
+		initializeWorkflowFormValues()
+		showForm.value = workflowVisibleVariables.value.length > 0
+	} catch (error) {
+		console.error('loadWorkflowExecuteInfo error', error)
+		ElMessage.error(t('pages.editor.loadAgentInfoFailed'))
+	} finally {
+		workflowFormLoading.value = false
+	}
+}
+
+const buildWorkflowChatParams = () => {
+	if (settingsDraft.type !== 'flow') return {}
+
+	const params = buildExecuteParams({
+		startVariables: workflowStartVariables.value,
+		inputValues: workflowInputValues,
+		jsonDrafts: workflowJsonDrafts,
+		validationErrors: workflowValidationErrors,
+		translateFieldRequired: (name) =>
+			t('pages.runWorkflow.fieldRequired', {
+				name
+			}),
+		translateInvalidJson: () => t('pages.runWorkflow.invalidJson'),
+		translateFieldTooLong: (name, max) =>
+			t('pages.runWorkflow.fieldTooLong', {
+				name,
+				max
+			})
+	})
+
+	if (!params) {
+		showForm.value = true
+		ElMessage.warning(t('pages.runWorkflow.inputPanel.completeRequired'))
+		return false
+	}
+
+	return params
+}
 
 /**
  * 深拷贝聊天配置对象
@@ -361,11 +622,12 @@ const settingsDraft = reactive<ChatTargetConfig>(createDefaultConfig('agent'))
  * @returns 拷贝后的新配置对象
  */
 const cloneConfig = (config: ChatTargetConfig): ChatTargetConfig => {
+	const isFlow = config.type === 'flow'
 	return {
 		type: config.type,
-		knowledgeBaseIds: [...config.knowledgeBaseIds],
-		knowledgeIds: [...config.knowledgeIds],
-		summaryModelId: config.summaryModelId,
+		knowledgeBaseIds: isFlow ? [] : [...config.knowledgeBaseIds],
+		knowledgeIds: isFlow ? [] : [...config.knowledgeIds],
+		summaryModelId: isFlow ? '' : config.summaryModelId,
 		disableTitle: config.disableTitle,
 		enableMemory: config.enableMemory,
 		agentId: config.agentId,
@@ -375,18 +637,52 @@ const cloneConfig = (config: ChatTargetConfig): ChatTargetConfig => {
 	}
 }
 
+/**
+ * 选择智能体活智能编排
+ */
+const handleChangeTarget = async (value: [ChatTargetType, string] | string[]) => {
+	const [type, id] = value as [ChatTargetType, string]
+	if (!type || !id) return
+
+	activeTargetType.value = type
+	settingsDraft.type = type
+	settingsDraft.agentId = id
+
+	if (type === 'flow') {
+		settingsDraft.knowledgeBaseIds = []
+		settingsDraft.knowledgeIds = []
+		settingsDraft.summaryModelId = ''
+		settingsDraft.agentEnabled = true
+		settingsDraft.webSearchEnabled = false
+		knowledgeOptions.value = []
+		currentAttachments.value = []
+		agentPromptsItems.value = undefined
+		allowImageUpload.value = true
+		await loadWorkflowExecuteInfo(id)
+		return
+	}
+
+	showForm.value = false
+	resetWorkflowFormState()
+	await refreshAgentUploadCapability(id)
+}
+
 const normalizeConfig = (config: Partial<ChatTargetConfig>): ChatTargetConfig => {
 	const base = createDefaultConfig(config.type || 'agent')
+	const type = config.type || base.type
+	const isFlow = type === 'flow'
 	return {
 		...base,
 		...config,
-		type: config.type || base.type,
-		knowledgeBaseIds: Array.isArray(config.knowledgeBaseIds) ? config.knowledgeBaseIds : [],
-		knowledgeIds: Array.isArray(config.knowledgeIds) ? config.knowledgeIds : [],
-		summaryModelId: config.summaryModelId || '',
+		type,
+		knowledgeBaseIds:
+			!isFlow && Array.isArray(config.knowledgeBaseIds) ? config.knowledgeBaseIds : [],
+		knowledgeIds: !isFlow && Array.isArray(config.knowledgeIds) ? config.knowledgeIds : [],
+		summaryModelId: isFlow ? '' : config.summaryModelId || '',
 		disableTitle: config.disableTitle ?? base.disableTitle,
 		enableMemory: config.enableMemory ?? base.enableMemory,
-		agentId: config.agentId || '',
+		agentId:
+			config.agentId || (isFlow ? getDefaultFlowId() : type === 'agent' ? getDefaultAgentId() : ''),
 		agentEnabled: config.agentEnabled ?? base.agentEnabled,
 		webSearchEnabled: config.webSearchEnabled ?? base.webSearchEnabled,
 		images: []
@@ -433,8 +729,12 @@ const copyShareUrl = async () => {
 
 const initializePresetConversation = async (config: ChatTargetConfig) => {
 	syncSettingsDraft(config)
-	await fetchKnowledgeOptions(config.knowledgeBaseIds)
-	await refreshAgentUploadCapability(config.agentId)
+	if (config.type === 'flow') {
+		await loadWorkflowExecuteInfo(config.agentId)
+	} else {
+		await fetchKnowledgeOptions(config.knowledgeBaseIds)
+		await refreshAgentUploadCapability(config.agentId)
+	}
 	await loadConversations(true)
 
 	const res = await createSession()
@@ -448,31 +748,6 @@ const initializePresetConversation = async (config: ChatTargetConfig) => {
 	}
 }
 
-/**
- * 获取指定会话的配置
- * 优先从缓存中获取,其次从会话历史中恢复,最后使用默认配置
- * @param conversationId - 会话 ID
- * @returns 聊天目标配置对象
- */
-const getConversationConfig = (conversationId: string) => {
-	if (presetConfig.value) return cloneConfig(presetConfig.value)
-
-	const cachedConfig = sessionConfigMap[conversationId]
-	if (cachedConfig) return cachedConfig
-
-	const conversation = conversations.value.find((item) => item.id === conversationId)
-	if (conversation?.targetConfig) {
-		const config = {
-			...createDefaultConfig(conversation.targetType || activeTargetType.value),
-			...cloneConfig(conversation.targetConfig as ChatTargetConfig)
-		}
-
-		return config
-	}
-
-	return createDefaultConfig(activeTargetType.value)
-}
-
 /**
  * 同步配置到当前的设置草稿中
  * @param config - 要同步的配置对象
@@ -482,6 +757,7 @@ const syncSettingsDraft = (config: ChatTargetConfig) => {
 	Object.assign(next, config)
 	activeTargetType.value = next.type
 	Object.assign(settingsDraft, next)
+	targetSelection.value = [next.type, next.agentId]
 }
 
 /**
@@ -490,18 +766,27 @@ const syncSettingsDraft = (config: ChatTargetConfig) => {
  */
 const loadChatOptions = async () => {
 	try {
-		const [agents, knowledgeBases, models] = await Promise.all([
+		const [agents, knowledgeBases, models, flows] = await Promise.all([
 			getAgentOptions(),
 			getKnowledgeBaseOptions(),
-			getModelOptions('', 'KnowledgeQA')
+			getModelOptions('', 'KnowledgeQA'),
+			getFlowOptions()
 		])
 		agentOptions.value = agents
 		knowledgeBaseOptions.value = knowledgeBases
 		modelOptions.value = models
+		flowOptions.value = flows
 
 		if (activeTargetType.value === 'agent' && !settingsDraft.agentId) {
 			settingsDraft.agentId = getDefaultAgentId()
 		}
+		if (activeTargetType.value === 'flow' && !settingsDraft.agentId) {
+			settingsDraft.agentId = getDefaultFlowId()
+		}
+		targetSelection.value = [settingsDraft.type, settingsDraft.agentId]
+		if (settingsDraft.type === 'flow' && settingsDraft.agentId) {
+			await loadWorkflowExecuteInfo(settingsDraft.agentId)
+		}
 	} catch (error) {
 		console.error('Failed to load chat options', error)
 	}
@@ -531,7 +816,7 @@ watch(
 	([type, chatRef]) => {
 		if (type && chatRef) {
 			const chat = chatInputRef.value?.getInstance()
-			if (type !== 'model') {
+			if (type !== 'model' && type !== 'flow') {
 				chat?.openHeader()
 			} else {
 				chat?.closeHeader()
@@ -549,6 +834,7 @@ watch(
  */
 const handleAgentSelectChange = async (value: string) => {
 	settingsDraft.agentId = value
+	targetSelection.value = [settingsDraft.type, value]
 	await refreshAgentUploadCapability(value)
 }
 
@@ -777,7 +1063,9 @@ const createAiMessage = (updateTime?: string): BubbleMessage => {
 		typing: false,
 		isFog: true,
 		streamCompleted: false,
-		updateTime
+		updateTime,
+		workflowTraceVisible: activeTargetType.value === 'flow',
+		workflowTraceNodes: []
 	}
 }
 
@@ -853,6 +1141,166 @@ const normalizeReferences = (event: ChatSseMessage): ChatReference[] => {
 	}))
 }
 
+const normalizeChatContentEvent = (raw: Record<string, any>): ChatSseMessage => {
+	const data = raw.data && typeof raw.data === 'object' ? raw.data : raw
+	const content =
+		raw.content ??
+		raw.answer ??
+		raw.output ??
+		raw.delta ??
+		raw.text ??
+		data.content ??
+		data.answer ??
+		data.output ??
+		data.delta ??
+		data.text ??
+		''
+
+	return {
+		id: raw.id || data.id,
+		response_type:
+			raw.response_type ||
+			raw.responseType ||
+			raw.type ||
+			raw.event ||
+			(content ? 'agent_query' : 'complete'),
+		content: `${content || ''}`,
+		done: raw.done || raw.is_end || raw.finished,
+		data
+	}
+}
+
+const normalizeWorkflowRunnerEvent = (event: ChatSseMessage): ChatSseMessage => {
+	const raw = event as ChatSseMessage & {
+		cmd?: string
+		msg?: Record<string, any>
+		result?: unknown
+		errorMsg?: string
+	}
+
+	switch (raw.cmd) {
+		case 'CMD_AGENT_CHAT_MSG':
+			return normalizeChatContentEvent(raw.msg || {})
+		case 'CMD_AGENT_FINISH_MSG':
+			return {
+				response_type: 'complete',
+				done: true,
+				data: { result: raw.result }
+			}
+		case 'CMD_AGENT_ERROR_MSG':
+		case 'CMD_CONNECT_ERROR_MSG':
+			return {
+				response_type: 'error',
+				content: raw.errorMsg || t('pages.chat.unknownError'),
+				done: true,
+				data: raw as unknown as Record<string, any>
+			}
+		case 'CMD_AGENT_SUSPEND_MSG':
+			return {
+				response_type: 'error',
+				content: t('pages.runWorkflow.suspended'),
+				done: true,
+				data: raw as unknown as Record<string, any>
+			}
+		default:
+			return event
+	}
+}
+
+const createRunnerNodeState = (
+	node: Record<string, any>,
+	status: RunnerNodeState['status']
+): RunnerNodeState => ({
+	nodeId: node.nodeId || node.id || '',
+	nodeName: node.nodeName || node.name || '',
+	nodeType: node.nodeType || node.type || '',
+	status,
+	lastUpdateTime: node.time,
+	track: node.track
+})
+
+const syncWorkflowTraceNode = (
+	message: BubbleMessage,
+	node: Record<string, any> | undefined,
+	partial: Partial<RunnerNodeState>
+) => {
+	if (!node) return
+	const nodeId = node.nodeId || node.id
+	if (!nodeId) return
+
+	message.workflowTraceVisible = true
+	const nodes: RunnerNodeState[] = Array.isArray(message.workflowTraceNodes)
+		? message.workflowTraceNodes
+		: []
+	const index = nodes.findIndex((item) => item.nodeId === nodeId)
+	const existing = index >= 0 ? nodes[index] : undefined
+	const next: RunnerNodeState = {
+		...createRunnerNodeState(node, existing?.status || 'idle'),
+		...existing,
+		...partial,
+		nodeId
+	}
+
+	if (index >= 0) {
+		nodes[index] = next
+	} else {
+		nodes.push(next)
+	}
+
+	message.workflowTraceNodes = nodes
+}
+
+const applyWorkflowRunnerTraceEvent = (message: BubbleMessage, event: ChatSseMessage) => {
+	const raw = event as ChatSseMessage & {
+		cmd?: string
+		time?: string
+		node?: Record<string, any>
+		track?: Record<string, any>
+	}
+
+	switch (raw.cmd) {
+		case 'CMD_AGENT_RUNNING_MSG':
+			message.workflowTraceVisible = true
+			break
+		case 'CMD_NODE_RUNNING_MSG':
+		case 'CMD_NODE_ITERATION_RUNNING_MSG':
+		case 'CMD_NODE_ITERATION_STEP_MSG':
+			syncWorkflowTraceNode(message, raw.node, {
+				status: 'running',
+				lastUpdateTime: raw.time
+			})
+			break
+		case 'CMD_NODE_FINISH_MSG':
+			syncWorkflowTraceNode(message, raw.node, {
+				status: raw.track?.is_success ? 'success' : 'failed',
+				lastUpdateTime: raw.time,
+				track: raw.track
+			})
+			break
+		case 'CMD_NODE_ITERATION_FINISH_MSG':
+			syncWorkflowTraceNode(message, raw.node, {
+				status: 'success',
+				lastUpdateTime: raw.time
+			})
+			break
+		case 'CMD_AGENT_SUSPEND_MSG':
+			message.workflowTraceVisible = true
+			message.workflowTraceNodes = (message.workflowTraceNodes || []).map(
+				(node: RunnerNodeState) =>
+					node.status === 'running'
+						? {
+								...node,
+								status: 'suspended',
+								lastUpdateTime: raw.time || node.lastUpdateTime
+							}
+						: node
+			)
+			break
+		default:
+			break
+	}
+}
+
 /**
  * 将结构化事件应用到消息对象上
  * 根据事件类型更新消息的不同字段(如回答、思考、工具调用等)
@@ -869,6 +1317,8 @@ const applyStructuredEventToMessage = (message: BubbleMessage, event: ChatSseMes
 			}
 			break
 		case 'answer':
+		case 'message':
+		case 'delta':
 			if (event.content) {
 				message.answerText = `${message.answerText || ''}${event.content}`
 			}
@@ -892,7 +1342,10 @@ const applyStructuredEventToMessage = (message: BubbleMessage, event: ChatSseMes
 			break
 		case 'error':
 			syncMessageIdentity(message, event)
-			appendInlineError(message, event.content || data.error || data.message || t('pages.chat.unknownError'))
+			appendInlineError(
+				message,
+				event.content || data.error || data.message || t('pages.chat.unknownError')
+			)
 			message.streamCompleted = true
 			break
 		case 'session_title': {
@@ -918,7 +1371,10 @@ const applyStructuredEventToMessage = (message: BubbleMessage, event: ChatSseMes
 	}
 	updateMessageContent(message)
 	message.loading =
-		!message.streamCompleted && !event.done && !hasRenderableContent(message) && event.response_type !== 'complete'
+		!message.streamCompleted &&
+		!event.done &&
+		!hasRenderableContent(message) &&
+		event.response_type !== 'complete'
 	message.typing = false
 	message.isFog = false
 }
@@ -1261,6 +1717,13 @@ const handleSend = async (content?: string) => {
 		ElMessage.warning(t('pages.chat.selectAgentFirst'))
 		return
 	}
+	if (activeTargetType.value === 'flow' && !settingsDraft.agentId) {
+		ElMessage.warning(t('pages.chat.selectWorkflowFirst'))
+		return
+	}
+
+	const workflowParams = buildWorkflowChatParams()
+	if (workflowParams === false) return
 
 	const hasConversation = await ensureActiveConversation()
 	if (!hasConversation) return
@@ -1285,7 +1748,8 @@ const handleSend = async (content?: string) => {
 		activeTargetType.value,
 		currentChatConfig.value,
 		activeConversationId.value,
-		content
+		content,
+		workflowParams
 	)
 
 	const url = getChatUrl(activeTargetType.value)
@@ -1299,7 +1763,8 @@ const handleSend = async (content?: string) => {
 			if (!isActiveStream()) return
 			const msg = getAiMessage()
 			if (!msg) return
-			applyStructuredEventToMessage(msg, event)
+			applyWorkflowRunnerTraceEvent(msg, event)
+			applyStructuredEventToMessage(msg, normalizeWorkflowRunnerEvent(event))
 			scrollToBottomIfNearBottom()
 		},
 		// onComplete
@@ -1450,6 +1915,47 @@ const handleCancel = async () => {
 	margin-top: 10px;
 }
 
+.workflow-form {
+	width: 100%;
+	max-width: 100%;
+	max-height: min(360px, 42vh);
+	margin: 0;
+	padding: 16px;
+	overflow-y: auto;
+	overflow-x: hidden;
+	border: 1px solid var(--border-light);
+	border-radius: 8px;
+	background: var(--bg-base);
+	box-sizing: border-box;
+}
+
+.workflow-form__title {
+	margin-bottom: 12px;
+	font-size: 15px;
+	font-weight: 600;
+	color: var(--text-primary);
+}
+
+:global(.workflow-form-popper) {
+	max-width: calc(100vw - 24px);
+	padding: 0;
+}
+
+:global(.workflow-form-popper .el-popover__inner) {
+	padding: 0;
+}
+
+.workflow-form :deep(.input-form),
+.workflow-form :deep(.el-form),
+.workflow-form :deep(.el-form-item),
+.workflow-form :deep(.el-form-item__content),
+.workflow-form :deep(.el-input),
+.workflow-form :deep(.el-select),
+.workflow-form :deep(.el-textarea) {
+	min-width: 0;
+	max-width: 100%;
+}
+
 /* Prompts 组件暗黑模式适配 */
 :deep(.el-prompts-title) {
 	color: var(--text-tertiary) !important;
@@ -1475,4 +1981,27 @@ const handleCancel = async () => {
 :deep(.el-prompts-item-description) {
 	color: var(--text-tertiary) !important;
 }
+
+:global(.target-popper .el-cascader-node) {
+	gap: 8px;
+}
+
+/* 隐藏级联选择器默认的标签 chip,保留自定义 #tag slot 内容 */
+.chat-target-cascader :deep(.el-cascader__tags > .el-tag) {
+	display: none;
+}
+
+.chat-target-tag {
+	display: inline-flex;
+	align-items: center;
+	white-space: nowrap;
+}
+
+.chat-target-tag__name {
+	font-size: 13px;
+	color: var(--text-primary);
+	max-width: 120px;
+	overflow: hidden;
+	text-overflow: ellipsis;
+}
 </style>

+ 1 - 1
apps/web/src/views/chat/types.ts

@@ -34,7 +34,7 @@ export interface BubbleMessage {
 	[key: string]: any
 }
 
-export type ChatTargetType = 'knowledge' | 'agent' | 'model'
+export type ChatTargetType = 'knowledge' | 'agent' | 'model' | 'flow'
 
 export interface ChatTargetConfig {
 	type: ChatTargetType

+ 0 - 0
apps/web/src/views/system/401.vue


+ 1 - 0
apps/web/src/views/system/404.vue

@@ -0,0 +1 @@
+                                        

+ 159 - 0
packages/api-service/schema/agent.openapi.json

@@ -5692,6 +5692,165 @@
 				"security": []
 			}
 		},
+		"/api/agent/getExecuteInfo": {
+			"post": {
+				"summary": "获取智能编排执行信息",
+				"deprecated": false,
+				"description": "",
+				"tags": ["Agent"],
+				"parameters": [
+					{
+						"name": "Authorization",
+						"in": "header",
+						"description": "",
+						"example": "bpm_client_1524059497549533184",
+						"schema": {
+							"type": "string"
+						}
+					}
+				],
+				"requestBody": {
+					"content": {
+						"application/json": {
+							"schema": {
+								"type": "object",
+								"properties": {
+									"id": {
+										"type": "string"
+									}
+								},
+								"required": ["id"]
+							},
+							"example": {
+								"id": "b3a4aabb-a6b8-47f3-8a32-f45930f7d7b8"
+							}
+						}
+					},
+					"required": true
+				},
+				"responses": {
+					"200": {
+						"description": "",
+						"content": {
+							"application/json": {
+								"schema": {
+									"type": "object",
+									"properties": {
+										"isSuccess": {
+											"type": "boolean"
+										},
+										"code": {
+											"type": "integer"
+										},
+										"result": {
+											"type": "object",
+											"properties": {
+												"form_variables": {
+													"type": "array",
+													"items": {
+														"type": "object",
+														"properties": {
+															"default_value": {
+																"type": "string"
+															},
+															"file_extensions": {
+																"type": "array",
+																"items": {
+																	"type": "string"
+																}
+															},
+															"file_types": {
+																"type": "array",
+																"items": {
+																	"type": "string"
+																}
+															},
+															"formType": {
+																"type": "string"
+															},
+															"is_hide": {
+																"type": "boolean"
+															},
+															"is_require": {
+																"type": "boolean"
+															},
+															"json": {
+																"type": "object",
+																"properties": {}
+															},
+															"label": {
+																"type": "string"
+															},
+															"name": {
+																"type": "string"
+															},
+															"options": {
+																"type": "array",
+																"items": {
+																	"type": "string"
+																}
+															},
+															"type": {
+																"type": "string"
+															}
+														}
+													}
+												},
+												"id": {
+													"type": "string"
+												},
+												"name": {
+													"type": "string"
+												},
+												"profilePhoto": {
+													"type": "string"
+												},
+												"type": {
+													"type": "string"
+												}
+											},
+											"required": ["form_variables", "id", "name", "profilePhoto", "type"]
+										},
+										"isAuthorized": {
+											"type": "boolean"
+										}
+									},
+									"required": ["isSuccess", "code", "result", "isAuthorized"]
+								},
+								"example": {
+									"isSuccess": true,
+									"code": 1,
+									"result": {
+										"form_variables": [
+											{
+												"default_value": "",
+												"file_extensions": [],
+												"file_types": [],
+												"formType": "text-input",
+												"is_hide": false,
+												"is_require": false,
+												"json": {},
+												"label": "aa",
+												"name": "aa",
+												"options": [],
+												"type": "string"
+											}
+										],
+										"id": "b3a4aabb-a6b8-47f3-8a32-f45930f7d7b8",
+										"name": "智能编演示",
+										"profilePhoto": "fe43d182-d546-4de2-8032-0772e6dd7ca4",
+										"type": "workflow"
+									},
+									"isAuthorized": true
+								}
+							}
+						},
+						"headers": {}
+					}
+				},
+				"security": []
+			}
+		},
 		"/api/ai/agent/selectList": {
 			"post": {
 				"summary": "获取智能体选择列表",

+ 40 - 0
packages/api-service/servers/api/agent.ts

@@ -507,6 +507,46 @@ export async function postAgentGetAgentNodeLastRunnerLogs(
   })
 }
 
+/** 获取智能编排执行信息 POST /api/agent/getExecuteInfo */
+export async function postAgentGetExecuteInfo(
+  body: {
+    id: string
+  },
+  options?: { [key: string]: any }
+) {
+  return request<{
+    isSuccess: boolean
+    code: number
+    result: {
+      form_variables: {
+        default_value?: string
+        file_extensions?: string[]
+        file_types?: string[]
+        formType?: string
+        is_hide?: boolean
+        is_require?: boolean
+        json?: Record<string, any>
+        label?: string
+        name?: string
+        options?: string[]
+        type?: string
+      }[]
+      id: string
+      name: string
+      profilePhoto: string
+      type: string
+    }
+    isAuthorized: boolean
+  }>('/api/agent/getExecuteInfo', {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json'
+    },
+    data: body,
+    ...(options || {})
+  })
+}
+
 /** 根据节点id,获取节点之前的所有变量列表 POST /api/agent/getPrevNodeOutVariableList */
 export async function postAgentGetPrevNodeOutVariableList(
   body: {