|
|
@@ -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>
|