| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008 |
- <template>
- <div class="chat-container">
- <!-- 左侧边栏 -->
- <ChatSidebar
- :conversations="conversations"
- :active-id="activeConversationId"
- :has-more="hasMore"
- :is-loading-more="isLoadingMore"
- @new-chat="handleNewChat"
- @select-conversation="handleSelectConversation"
- @conv-command="handleConvCommand"
- @load-more="loadMoreConversations"
- />
- <!-- 右侧聊天区域 -->
- <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">
- <span class="flex items-center gap-4px">
- <Icon icon="lucide:share-2" />
- <span>{{ t('pages.chat.generateLink') }}</span>
- </span>
- </el-button>
- </template>
- </ChatHeader>
- <MessageList
- ref="messageListRef"
- :messages="messages"
- :loading="isLoading"
- @retry="handleRetry"
- @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%"
- >
- <Prompts
- :title="t('pages.chat.quickStartTitle')"
- :items="agentPromptsItems"
- @item-click="handlePromptItemClick"
- wrap
- />
- </div>
- </MessageList>
- <!-- 对话框 -->
- <ChatInput
- v-model="senderValue"
- ref="chatInputRef"
- :loading="isLoading"
- :attachments="currentAttachments"
- @submit="handleSend"
- @cancel="handleCancel"
- >
- <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!"
- :placeholder="t('pages.chat.selectKnowledgeBasePlaceholder')"
- v-model="settingsDraft.knowledgeBaseIds"
- :options="knowledgeBaseOptions"
- @change="fetchKnowledgeOptions"
- multiple
- />
- </div>
- <div class="flex items-center gap-4px">
- <!-- <div class="title">知识:</div> -->
- <el-select
- class="w-180px!"
- :placeholder="t('pages.chat.selectKnowledgePlaceholder')"
- v-model="settingsDraft.knowledgeIds"
- :options="knowledgeOptions"
- multiple
- />
- </div>
- </div>
- </template>
- <template #prefix-extra>
- <!-- 智能体与智能体编排 -->
- <el-cascader
- v-if="!isPresetMode"
- v-model="targetSelection"
- :options="getTargetOptions"
- :placeholder="t('pages.chat.selectPlaceholder')"
- popper-class="target-popper"
- class="chat-target-cascader"
- @change="handleChangeTarget"
- filterable
- />
- <!-- 附件 -->
- <el-badge :value="currentAttachments.length" :hidden="!currentAttachments.length">
- <el-button
- v-if="showImageUploadButton"
- round
- plain
- color="#626aef"
- @click="imageUploadDialogVisible = true"
- >
- <el-icon>
- <PictureFilled />
- </el-icon>
- </el-button>
- </el-badge>
- </template>
- <template #action>
- <el-select
- v-if="!isPresetMode && settingsDraft.type !== 'flow'"
- :placeholder="t('pages.chat.selectModelPlaceholder')"
- placement="top"
- v-model="settingsDraft.summaryModelId"
- :options="modelOptions"
- class="w-120px!"
- />
- </template>
- </ChatInput>
- </div>
- <!-- 重命名对话弹窗 -->
- <el-dialog
- v-model="renameDialogVisible"
- :title="t('pages.chat.renameDialogTitle')"
- width="400px"
- >
- <el-input v-model="renameInput" :placeholder="t('pages.chat.renamePlaceholder')" />
- <template #footer>
- <el-button @click="renameDialogVisible = false">{{ t('pages.chat.cancel') }}</el-button>
- <el-button type="primary" @click="handleRename">{{ t('common.confirm') }}</el-button>
- </template>
- </el-dialog>
- <!-- 图片上传 -->
- <el-dialog
- v-model="imageUploadDialogVisible"
- :title="t('pages.chat.imageUploadTitle')"
- width="560px"
- >
- <FileUploadInput
- v-model="currentAttachments"
- :multiple="true"
- :allow-link-input="false"
- :tip="t('pages.chat.imageUploadTip')"
- style="width: 100%"
- />
- <template #footer>
- <el-button @click="imageUploadDialogVisible = false">{{
- t('pages.chat.cancel')
- }}</el-button>
- <el-button type="primary" @click="imageUploadDialogVisible = false">{{
- t('pages.chat.imageUploadDone')
- }}</el-button>
- </template>
- </el-dialog>
- <el-dialog v-model="shareDialogVisible" :title="t('pages.chat.shareDialogTitle')" width="620px">
- <div class="share-dialog">
- <div class="share-dialog__tip">
- {{ t('pages.chat.shareDialogTip') }}
- </div>
- <el-input v-model="shareUrl" type="textarea" :rows="4" readonly />
- </div>
- <template #footer>
- <el-button @click="shareDialogVisible = false">{{ t('common.close') }}</el-button>
- <el-button type="primary" @click="copyShareUrl">{{
- t('pages.chat.copyShareLink')
- }}</el-button>
- </template>
- </el-dialog>
- </div>
- <AddKbModal ref="addkbRef" />
- </template>
- <script setup lang="ts">
- import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
- import { useRoute } from 'vue-router'
- 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 {
- buildChatRequestBody,
- createSession,
- deleteSession,
- getAgentInfo,
- getAgentOptions,
- getFlowOptions,
- getChatUrl,
- getKnowledgeBaseOptions,
- getModelOptions,
- getSessionList,
- getSessionMessages,
- updateSessionName,
- type ChatOptionItem
- } from './api/chat.api'
- import ChatHeader from './components/ChatHeader.vue'
- 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 WorkflowTraceBubble from '@/features/ChatDrawer/WorkflowTraceBubble.vue'
- import { agent, agentApplication, knowledge, aiChat } from '@repo/api-service'
- import type {
- BubbleMessage,
- ChatReference,
- ChatSseMessage,
- ChatTargetConfig,
- ChatTargetType,
- ChatToolCall,
- ChatToolResult,
- Conversation
- } from './types'
- import type { PromptsItemsProps } from 'vue-element-plus-x/types/Prompts'
- import type { WorkflowUploadFile } from '@/features/fileUpload/shared'
- 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()
- const route = useRoute()
- const conversations = ref<Conversation[]>([])
- const sessionConfigMap = reactive<Record<string, ChatTargetConfig>>({})
- const activeConversationId = ref('')
- const activeTargetType = ref<ChatTargetType>('agent')
- 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[]>([])
- const currentPage = ref(1)
- const pageSize = ref(20)
- const hasMore = ref(true)
- const isLoadingMore = ref(false)
- const messages = ref<BubbleMessage[]>([])
- const renameDialogVisible = ref(false)
- const renameInput = ref('')
- const renamingConvId = ref('')
- const senderValue = ref('')
- const messageListRef = ref()
- const activeStreamToken = ref('')
- const chatInputRef = ref<InstanceType<typeof ChatInput>>()
- const agentPromptsItems = ref<PromptsItemsProps[]>()
- const addkbRef = ref<InstanceType<typeof AddKbModal>>()
- 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
- await nextTick()
- scrollToBottomPending = false
- messageListRef.value?.scrollToBottom?.()
- }
- const scrollToBottomIfNearBottom = () => {
- if (messageListRef.value?.isNearBottom?.() === false) return
- scrollToBottom()
- }
- /**
- * 创建知识
- */
- const handleAddKb = (txt: string) => {
- addkbRef.value?.open(txt)
- }
- /**
- * 计算当前激活会话的标题
- */
- const activeConversationTitle = computed(() => {
- const conv = conversations.value.find((c) => c.id === activeConversationId.value)
- return conv ? conv.title : t('pages.chat.newConversation')
- })
- /**
- * 判断是否显示图片上传按钮
- * agent 模式:根据智能体配置决定是否显示
- * flow 模式:始终显示
- */
- const showImageUploadButton = computed(
- () =>
- (activeTargetType.value === 'agent' && allowImageUpload.value) ||
- activeTargetType.value === 'flow'
- )
- /**
- * 计算当前聊天配置对象,用于发送消息时使用
- */
- const currentChatConfig = computed<ChatTargetConfig>(() => ({
- type: activeTargetType.value,
- 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,
- agentEnabled: settingsDraft.agentEnabled,
- webSearchEnabled: settingsDraft.webSearchEnabled,
- images: currentAttachments.value.map((item) => item.path || item.id || item.name).filter(Boolean)
- }))
- /**
- * 获取默认的智能体 ID
- * @returns 第一个智能体的 value,若无则返回空字符串
- */
- const getDefaultAgentId = () => {
- return agentOptions.value[0]?.value || ''
- }
- const getDefaultFlowId = () => {
- return flowOptions.value[0]?.value || ''
- }
- onMounted(async () => {
- presetConfig.value = parsePresetConfigFromRoute()
- await loadChatOptions()
- if (presetConfig.value) {
- await initializePresetConversation(presetConfig.value)
- return
- }
- await loadConversations()
- // 默认选中第一个会话
- // if (conversations.value.length > 0 && !activeConversationId.value) {
- // activeConversationId.value = conversations.value?.[0]?.id!
- // await loadConversationMessages(activeConversationId.value)
- // await scrollToBottom()
- // }
- })
- /**
- * 创建默认的聊天目标配置
- * @param type - 聊天目标类型,默认为 'agent'
- * @returns 默认配置对象
- */
- const createDefaultConfig = (type: ChatTargetType = 'agent'): ChatTargetConfig => {
- return {
- type,
- knowledgeBaseIds: [],
- knowledgeIds: [],
- summaryModelId: '',
- disableTitle: false,
- enableMemory: true,
- agentId: type === 'flow' ? getDefaultFlowId() : type === 'agent' ? getDefaultAgentId() : '',
- agentEnabled: true,
- webSearchEnabled: false,
- images: []
- }
- }
- 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
- }
- /**
- * 深拷贝聊天配置对象
- * @param config - 源配置对象
- * @returns 拷贝后的新配置对象
- */
- const cloneConfig = (config: ChatTargetConfig): ChatTargetConfig => {
- const isFlow = config.type === 'flow'
- return {
- type: config.type,
- knowledgeBaseIds: isFlow ? [] : [...config.knowledgeBaseIds],
- knowledgeIds: isFlow ? [] : [...config.knowledgeIds],
- summaryModelId: isFlow ? '' : config.summaryModelId,
- disableTitle: config.disableTitle,
- enableMemory: config.enableMemory,
- agentId: config.agentId,
- agentEnabled: config.agentEnabled,
- webSearchEnabled: config.webSearchEnabled,
- images: [...config.images]
- }
- }
- /**
- * 选择智能体活智能编排
- */
- 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,
- 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 || (isFlow ? getDefaultFlowId() : type === 'agent' ? getDefaultAgentId() : ''),
- agentEnabled: config.agentEnabled ?? base.agentEnabled,
- webSearchEnabled: config.webSearchEnabled ?? base.webSearchEnabled,
- images: []
- }
- }
- const parsePresetConfigFromRoute = () => {
- if (route.query.preset !== '1' || typeof route.query.chatConfig !== 'string') return null
- try {
- const rawConfig = JSON.parse(decodeURIComponent(route.query.chatConfig))
- return normalizeConfig(rawConfig)
- } catch (error) {
- console.error('Failed to parse chat preset config', error)
- ElMessage.error(t('pages.chat.invalidShareLink'))
- return null
- }
- }
- const buildShareUrl = () => {
- const config = normalizeConfig(currentChatConfig.value)
- const url = new URL(window.location.href)
- const hashPath = route.path || '/chat'
- const params = new URLSearchParams()
- params.set('preset', '1')
- params.set('chatConfig', encodeURIComponent(JSON.stringify(config)))
- url.hash = `${hashPath}?${params.toString()}`
- return url.toString()
- }
- const shareConversation = () => {
- shareUrl.value = buildShareUrl()
- shareDialogVisible.value = true
- }
- const copyShareUrl = async () => {
- if (!shareUrl.value) return
- try {
- await navigator.clipboard.writeText(shareUrl.value)
- ElMessage.success(t('pages.chat.shareLinkCopied'))
- } catch {
- ElMessage.warning(t('pages.chat.shareLinkCopyFailed'))
- }
- }
- const initializePresetConversation = async (config: ChatTargetConfig) => {
- syncSettingsDraft(config)
- if (config.type === 'flow') {
- await loadWorkflowExecuteInfo(config.agentId)
- } else {
- await fetchKnowledgeOptions(config.knowledgeBaseIds)
- await refreshAgentUploadCapability(config.agentId)
- }
- await loadConversations(true)
- const res = await createSession()
- if (!res.isSuccess || !res.result) return
- await loadConversations(true)
- const targetConv = conversations.value.find((c) => c.id === res.result)
- if (targetConv) {
- sessionConfigMap[targetConv.id] = cloneConfig(config)
- await handleSelectConversation(targetConv.id)
- }
- }
- /**
- * 同步配置到当前的设置草稿中
- * @param config - 要同步的配置对象
- */
- const syncSettingsDraft = (config: ChatTargetConfig) => {
- const next = createDefaultConfig(config.type)
- Object.assign(next, config)
- activeTargetType.value = next.type
- Object.assign(settingsDraft, next)
- targetSelection.value = [next.type, next.agentId]
- }
- /**
- * 加载聊天所需的选项数据(智能体、知识库、模型)
- * 并在加载完成后设置默认的模型和智能体
- */
- const loadChatOptions = async () => {
- try {
- const [agents, knowledgeBases, models, flows] = await Promise.all([
- getAgentOptions(),
- getKnowledgeBaseOptions(),
- 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)
- }
- }
- /**
- * 刷新智能体的图片上传能力配置
- * @param agentId - 智能体 ID
- */
- const refreshAgentUploadCapability = async (agentId: string) => {
- allowImageUpload.value = false
- if (!agentId || activeTargetType.value !== 'agent') return
- try {
- const res = await getAgentInfo(agentId)
- allowImageUpload.value = !!res.result?.config?.img_vlm_config?.image_upload_enabled
- if (!allowImageUpload.value) {
- currentAttachments.value = []
- }
- } catch (error) {
- console.error('Failed to load agent info', error)
- }
- }
- watch(
- () => [settingsDraft.type, chatInputRef.value],
- ([type, chatRef]) => {
- if (type && chatRef) {
- const chat = chatInputRef.value?.getInstance()
- if (type !== 'model' && type !== 'flow') {
- chat?.openHeader()
- } else {
- chat?.closeHeader()
- }
- }
- },
- {
- immediate: true
- }
- )
- /**
- * 处理智能体选择变更事件
- * @param value - 选中的智能体 ID
- */
- const handleAgentSelectChange = async (value: string) => {
- settingsDraft.agentId = value
- targetSelection.value = [settingsDraft.type, value]
- await refreshAgentUploadCapability(value)
- }
- /**
- * 获取建议问题列表并更新提示项
- */
- const getSuggestQuestion = async () => {
- if (!settingsDraft.agentId) return
- // 获取建议问题
- const res = await agentApplication.postAiAgentSuggestedQuestions({
- id: settingsDraft.agentId,
- limit: 6
- })
- if (res.result) {
- agentPromptsItems.value = res.result.map((item, index) => {
- return {
- key: index,
- label: item.question,
- itemStyle: {
- height: 'auto',
- width: 'calc(50% - 6px)',
- boxSizing: 'border-box'
- }
- }
- })
- }
- }
- watch(
- () => settingsDraft.agentId,
- async () => {
- if (settingsDraft.type === 'agent') getSuggestQuestion()
- },
- {
- immediate: true
- }
- )
- /**
- * 处理提示项点击事件
- * @param item - 被点击的提示项
- */
- const handlePromptItemClick = (item: PromptsItemsProps) => {
- handleSend(item.label)
- }
- /**
- * 追加思考内容到消息对象
- * @param message - 消息对象
- * @param text - 要追加的思考文本
- */
- const appendThinking = (message: BubbleMessage, text?: string) => {
- const thought = text
- if (!thought) return
- message.eventThinkingText = `${message.eventThinkingText || ''}${thought}`
- updateMessageThinking(message)
- }
- /**
- * 解析回答中的思考状态
- * 提取 <think> 标签内的内容和标签外的回答内容
- * @param text - 原始文本
- * @returns 包含思考内容、回答内容及标签状态的對象
- */
- const parseAnswerThinkState = (text?: string) => {
- const raw = `${text || ''}`
- if (!raw) return { thinking: '', answer: '', hasThinkStart: false, hasThinkEnd: false }
- const hasThinkStart = /^<think\b[^>]*>/i.test(raw.trimStart())
- const thinking: string[] = []
- let answer = raw
- if (hasThinkStart) {
- answer = answer.replace(
- /^\s*<think\b[^>]*>([\s\S]*?)(?:<\/think>|$)/i,
- (_match, inner: string) => {
- const piece = inner
- if (piece) thinking.push(piece)
- return ''
- }
- )
- }
- return {
- thinking: thinking.join('\n\n'),
- answer: answer,
- hasThinkStart,
- hasThinkEnd: /<\/think>/i.test(raw)
- }
- }
- /**
- * 更新消息对象的思考内容字段
- * @param message - 消息对象
- * @param answerThinking - 回答中的思考内容
- */
- const updateMessageThinking = (message: BubbleMessage, answerThinking = '') => {
- message.answerThinkingText = answerThinking
- message.thinking = [message.eventThinkingText, message.answerThinkingText]
- .map((item) => item)
- .filter(Boolean)
- .join('\n\n')
- }
- /**
- * 更新消息对象的内容字段,并处理思考状态
- * @param message - 消息对象
- */
- const updateMessageContent = (message: BubbleMessage) => {
- const answerParts = parseAnswerThinkState(message.answerText || message.output)
- message.content = answerParts.answer
- updateMessageThinking(message, answerParts.thinking)
- if (answerParts.hasThinkStart) {
- message.thinkingOpen = !answerParts.hasThinkEnd
- } else if (message.streamCompleted) {
- message.thinkingOpen = false
- }
- }
- /**
- * 判断消息是否具有可渲染的内容
- * @param message - 消息对象
- * @returns 是否有可渲染内容
- */
- const hasRenderableContent = (message: BubbleMessage) => {
- return !!(
- message.content ||
- message.answerText ||
- message.thinking ||
- (message.toolCalls || []).length ||
- (message.toolResults || []).length ||
- (message.references || []).length ||
- message.error
- )
- }
- const appendInlineError = (message: BubbleMessage, error: string) => {
- const trimmedError = error.trim()
- if (!trimmedError) return
- const inlineErrors = message.inlineErrors || []
- if (!inlineErrors.includes(trimmedError)) {
- message.inlineErrors = [...inlineErrors, trimmedError]
- }
- }
- /**
- * 创建用户消息对象
- * @param content - 消息内容
- * @param updateTime - 更新时间
- * @returns 用户消息对象
- */
- const createUserMessage = (content: string, updateTime?: string): BubbleMessage => {
- const id = `user-${Date.now()}-${Math.random()}`
- return {
- id,
- key: id,
- role: 'user',
- placement: 'end',
- content: content,
- rawText: content,
- loading: false,
- shape: 'corner',
- variant: 'outlined',
- isMarkdown: false,
- typing: false,
- isFog: false,
- streamCompleted: true,
- updateTime
- }
- }
- /**
- * 获取当前附件的文件 ID 列表
- * @returns 文件 ID 数组
- */
- const getAttachmentFileIds = () => {
- return currentAttachments.value
- .map((item) => item.path || item.id || item.name)
- .filter(Boolean) as string[]
- }
- /**
- * 创建带有附件的用户消息对象
- * @param content - 消息内容
- * @param updateTime - 更新时间
- * @returns 用户消息对象
- */
- const createUserMessageWithAttachments = (content: string, updateTime?: string): BubbleMessage => {
- const message = createUserMessage(content, updateTime)
- const fileIds = getAttachmentFileIds()
- if (fileIds.length) {
- message.message_files = fileIds
- }
- return message
- }
- /**
- * 创建 AI 消息对象
- * @param updateTime - 更新时间
- * @returns AI 消息对象
- */
- const createAiMessage = (updateTime?: string): BubbleMessage => {
- const id = `ai-${Date.now()}-${Math.random()}`
- return {
- id,
- msgId: '',
- key: id,
- role: 'ai',
- placement: 'start',
- content: '',
- answerText: '',
- thinking: '',
- thinkingOpen: false,
- toolCalls: [],
- toolResults: [],
- references: [],
- assistantMessageId: '',
- loading: true,
- shape: 'corner',
- variant: 'filled',
- isMarkdown: true,
- typing: false,
- isFog: true,
- streamCompleted: false,
- updateTime,
- workflowTraceVisible: activeTargetType.value === 'flow',
- workflowTraceNodes: []
- }
- }
- /**
- * 同步消息身份
- * @param message
- * @param event
- */
- const syncMessageIdentity = (message: BubbleMessage, event: ChatSseMessage) => {
- if (event?.assistant_message_id) {
- message.assistantMessageId = event.assistant_message_id
- message.msgId = event.id
- }
- }
- /**
- * 标准化工具调用事件数据
- * @param event - SSE 事件对象
- * @returns 标准化的工具调用对象
- */
- const normalizeToolCall = (event: ChatSseMessage): ChatToolCall => {
- const data = event.data || {}
- return {
- id: data.id || event.id,
- toolCallId: data.tool_call_id || data.toolCallId,
- toolName: data.tool_name || data.toolName,
- arguments: data.arguments || data.params || data.args || {},
- content: event.content || data.content
- }
- }
- /**
- * 标准化工具结果事件数据
- * @param event - SSE 事件对象
- * @returns 标准化的工具结果对象
- */
- const normalizeToolResult = (event: ChatSseMessage): ChatToolResult => {
- const data = event.data || {}
- return {
- id: data.id || event.id,
- toolCallId: data.tool_call_id || data.toolCallId,
- toolName: data.tool_name || data.toolName,
- success: data.success,
- error: data.error,
- output: data.output,
- thought: data.thought,
- durationMs: data.duration_ms || data.durationMs,
- displayType: data.display_type || data.displayType,
- contentItems: data.content_items || data.contentItems
- }
- }
- /**
- * 标准化引用文献事件数据
- * @param event - SSE 事件对象
- * @returns 标准化的引用文献数组
- */
- const normalizeReferences = (event: ChatSseMessage): ChatReference[] => {
- const data = event.data || {}
- const refs = data.knowledge_references || data.references || []
- if (!Array.isArray(refs)) return []
- return refs.map((item: Record<string, any>) => ({
- id: item.id,
- knowledgeBaseId: item.knowledge_base_id,
- knowledgeId: item.knowledge_id,
- knowledgeTitle: item.knowledge_title,
- knowledgeFilename: item.knowledge_filename,
- knowledgeDescription: item.knowledge_description,
- content: item.content,
- matchedContent: item.matched_content || item.matchedContent,
- score: item.score,
- chunkType: item.chunk_type || item.chunkType
- }))
- }
- 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
- }
- }
- /**
- * 将结构化事件应用到消息对象上
- * 根据事件类型更新消息的不同字段(如回答、思考、工具调用等)
- * @param message - 消息对象
- * @param event - SSE 事件对象
- */
- const applyStructuredEventToMessage = (message: BubbleMessage, event: ChatSseMessage) => {
- const data = event.data || {}
- switch (event.response_type) {
- case 'agent_query':
- syncMessageIdentity(message, event)
- if (event.content) {
- message.answerText = `${message.answerText || ''}${event.content}`
- }
- break
- case 'answer':
- case 'message':
- case 'delta':
- if (event.content) {
- message.answerText = `${message.answerText || ''}${event.content}`
- }
- break
- case 'thinking': {
- const thought = event.content || data.thought || data.content
- appendThinking(message, thought)
- message.thinkingOpen = true
- break
- }
- case 'tool_call':
- message.toolCalls = [...(message.toolCalls || []), normalizeToolCall(event)]
- break
- case 'tool_result': {
- message.toolResults = [...(message.toolResults || []), normalizeToolResult(event)]
- appendThinking(message, data.thought)
- break
- }
- case 'references':
- message.references = [...(message.references || []), ...normalizeReferences(event)]
- break
- case 'error':
- syncMessageIdentity(message, event)
- appendInlineError(
- message,
- event.content || data.error || data.message || t('pages.chat.unknownError')
- )
- message.streamCompleted = true
- break
- case 'session_title': {
- const title = data.title || event.content
- if (title) {
- const conv = conversations.value.find((item) => item.id === activeConversationId.value)
- if (conv) {
- conv.title = title
- }
- }
- break
- }
- case 'complete':
- message.streamCompleted = true
- message.total_duration_ms = data?.total_duration_ms
- message.total_steps = data?.total_steps
- break
- default:
- if (event.content) {
- message.answerText = `${message.answerText || ''}${event.content}`
- }
- break
- }
- updateMessageContent(message)
- message.loading =
- !message.streamCompleted &&
- !event.done &&
- !hasRenderableContent(message) &&
- event.response_type !== 'complete'
- message.typing = false
- message.isFog = false
- }
- /**
- * 解析历史记录条目为消息对象数组
- * @param item - 历史记录条目
- * @returns 消息对象数组
- */
- const parseHistoryRecord = (item: Record<string, any>): BubbleMessage[] => {
- const updateTime = item.updateTime || item.creationTime
- const messagesForRow: BubbleMessage[] = []
- const attachmentFiles = (item?.message_files || '').split(',')
- if (item.query) {
- const userMessage = createUserMessage(item.query, updateTime)
- if (attachmentFiles.length) {
- userMessage.message_files = attachmentFiles
- }
- messagesForRow.push(userMessage)
- }
- const hasStructuredData =
- item.response_type ||
- item.thinking ||
- item.tool_call ||
- item.tool_result ||
- item.knowledge_references ||
- item.references ||
- item.answer ||
- item.content
- if (!hasStructuredData) {
- return messagesForRow
- }
- const aiMessage = createAiMessage(updateTime)
- aiMessage.id = item.id || item.msgId || aiMessage.id
- aiMessage.key = aiMessage.id
- aiMessage.msgId = item.msgId || item.id || ''
- aiMessage.assistantMessageId = item.assistant_message_id || item.assistantMessageId || ''
- aiMessage.answerText = item.answer || item.content || item.output
- aiMessage.thinking = item.thinking || item.thought
- aiMessage.toolCalls = Array.isArray(item.tool_calls)
- ? item.tool_calls
- : Array.isArray(item.toolCalls)
- ? item.toolCalls
- : []
- aiMessage.toolResults = Array.isArray(item.tool_results)
- ? item.tool_results
- : Array.isArray(item.toolResults)
- ? item.toolResults
- : []
- aiMessage.references = Array.isArray(item.knowledge_references)
- ? normalizeReferences({ data: { knowledge_references: item.knowledge_references } })
- : Array.isArray(item.references)
- ? normalizeReferences({ data: { references: item.references } })
- : []
- const answerParts = parseAnswerThinkState(aiMessage.answerText)
- if (!aiMessage.thinking && answerParts.thinking) {
- aiMessage.thinking = answerParts.thinking
- }
- aiMessage.answerText = answerParts.answer
- aiMessage.thinkingOpen = false
- updateMessageContent(aiMessage)
- aiMessage.loading = false
- aiMessage.isFog = false
- aiMessage.streamCompleted = true
- messagesForRow.push(aiMessage)
- return messagesForRow
- }
- /**
- * 加载会话列表
- * @param isRefresh - 是否为刷新操作,如果是则重置分页和列表
- */
- const loadConversations = async (isRefresh = false) => {
- if (isRefresh) {
- currentPage.value = 1
- conversations.value = []
- hasMore.value = true
- }
- try {
- const res = await getSessionList(currentPage.value, pageSize.value)
- if (res.isSuccess && res.result?.model) {
- const newList = res.result.model.map((item) => {
- const config = sessionConfigMap[item.id]
- return {
- id: item.id,
- sessionId: item.sessionId,
- title: item.name,
- updatedAt: new Date(item.updateTime).getTime(),
- targetType: config?.type,
- targetConfig: config ? cloneConfig(config) : undefined
- }
- })
- conversations.value = isRefresh ? newList : [...conversations.value, ...newList]
- if (newList.length < pageSize.value) {
- hasMore.value = false
- } else {
- currentPage.value += 1
- }
- } else {
- hasMore.value = false
- }
- } catch (error) {
- console.error('Failed to load conversations', error)
- ElMessage.error(t('common.error.network'))
- }
- }
- /**
- * 加载更多会话
- */
- const loadMoreConversations = async () => {
- if (isLoadingMore.value || !hasMore.value) return
- isLoadingMore.value = true
- try {
- await loadConversations(false)
- } finally {
- isLoadingMore.value = false
- }
- }
- /**
- * 加载指定会话的消息历史
- * @param conversationId - 会话 ID
- */
- const loadConversationMessages = async (conversationId: string) => {
- if (!conversationId) {
- messages.value = []
- return
- }
- try {
- const res = await getSessionMessages(conversationId)
- if (res.isSuccess && res.result?.model) {
- messages.value = res.result.model.flatMap((item) =>
- parseHistoryRecord(item as Record<string, any>)
- )
- } else {
- messages.value = []
- }
- } catch (error) {
- console.error('Failed to load messages', error)
- messages.value = []
- }
- }
- /**
- * 处理选择会话事件
- * @param bizId - 会话业务 ID
- */
- const handleSelectConversation = async (bizId: string) => {
- activeConversationId.value = bizId
- messages.value = []
- const targetConv = conversations.value.find((c) => c.id === bizId)
- if (!targetConv) {
- console.warn('Selected conversation not found in local list')
- return
- }
- if (presetConfig.value) {
- sessionConfigMap[targetConv.id] = cloneConfig(presetConfig.value)
- }
- await loadConversationMessages(targetConv.id)
- await scrollToBottom()
- }
- const ensureActiveConversation = async () => {
- if (activeConversationId.value) {
- sessionConfigMap[activeConversationId.value] = cloneConfig(currentChatConfig.value)
- return true
- }
- try {
- const config = cloneConfig(currentChatConfig.value)
- const res = await createSession()
- if (!res.isSuccess || !res.result) {
- ElMessage.error(t('common.error.network'))
- return false
- }
- await loadConversations(true)
- const targetConv = conversations.value.find((c) => c.id === res.result)
- if (targetConv) {
- sessionConfigMap[targetConv.id] = config
- activeConversationId.value = targetConv.id
- messages.value = []
- return true
- }
- ElMessage.error(t('common.error.network'))
- return false
- } catch (error) {
- console.error(error)
- ElMessage.error(t('common.error.network'))
- return false
- }
- }
- /**
- * 处理新建会话事件
- */
- const handleNewChat = async () => {
- try {
- // const defaultName = `${t('pages.chat.newConversation')} ${new Date().toLocaleTimeString()}`
- const res = await createSession()
- if (res.isSuccess && res.result) {
- await loadConversations(true)
- const targetConv = conversations.value.find((c) => c.id === res.result)
- if (targetConv) {
- sessionConfigMap[targetConv.id] = presetConfig.value
- ? cloneConfig(presetConfig.value)
- : cloneConfig(currentChatConfig.value)
- await handleSelectConversation(targetConv.id)
- ElMessage.success(t('pages.chat.createSuccess'))
- } else if (conversations.value.length > 0) {
- await handleSelectConversation(conversations.value?.[0]?.id!)
- }
- }
- } catch (error) {
- console.error(error)
- ElMessage.error(t('common.error.network'))
- }
- }
- /**
- * 处理会话命令(重命名、删除等)
- * @param command - 命令名称
- * @param bizId - 会话业务 ID
- */
- const handleConvCommand = (command: string | number, bizId: string) => {
- const cmd = String(command)
- if (cmd === 'rename') {
- renamingConvId.value = bizId
- const conv = conversations.value.find((c) => c.id === bizId)
- renameInput.value = conv?.title || ''
- renameDialogVisible.value = true
- return
- }
- if (cmd === 'delete') {
- ElMessageBox.confirm(t('pages.chat.deleteConfirm'), t('common.dialog.tip'), {
- confirmButtonText: t('common.confirm'),
- cancelButtonText: t('common.cancel'),
- type: 'warning'
- })
- .then(async () => {
- try {
- await deleteSession(bizId)
- ElMessage.success(t('pages.chat.deleteSuccess'))
- if (activeConversationId.value === bizId) {
- activeConversationId.value = ''
- messages.value = []
- }
- await loadConversations(true)
- if (conversations.value.length > 0 && !activeConversationId.value) {
- activeConversationId.value = conversations.value?.[0]?.id!
- await loadConversationMessages(activeConversationId.value)
- await scrollToBottom()
- }
- } catch (error) {
- ElMessage.error(t('common.error.network'))
- }
- })
- .catch(() => {})
- }
- }
- /**
- * 根据选中的知识库 ID 获取知识选项
- * @param baseIds - 知识库 ID 数组
- */
- const fetchKnowledgeOptions = async (baseIds: string[]) => {
- if (!baseIds.length) {
- knowledgeOptions.value = []
- return
- }
- const results = await Promise.all(
- baseIds.map((knowledgeBaseId) =>
- knowledge.postAiKnowledgeSelectList({
- knowledge_base_id: knowledgeBaseId,
- title: ''
- })
- )
- )
- const optionMap: Record<string, { label: string; value: string }> = {}
- results.forEach((res: any) => {
- if (!res?.isSuccess) return
- ;(res.result || []).forEach((item: any) => {
- if (!item.id) return
- optionMap[item.id] = {
- label: item.title!,
- value: item.id
- }
- })
- })
- knowledgeOptions.value = Object.values(optionMap)
- }
- /**
- * 处理重命名会话
- */
- const handleRename = async () => {
- if (!renameInput.value.trim()) {
- ElMessage.warning(t('pages.chat.renameEmpty'))
- return
- }
- try {
- await updateSessionName(renamingConvId.value, renameInput.value)
- renameDialogVisible.value = false
- ElMessage.success(t('pages.chat.renameSuccess'))
- const conv = conversations.value.find((c) => c.id === renamingConvId.value)
- if (conv) conv.title = renameInput.value
- } catch (error) {
- ElMessage.error(t('common.error.network'))
- }
- }
- /**
- * 处理发送消息
- * @param content - 可选的消息内容,若不提供则使用 senderValue
- */
- const handleSend = async (content?: string) => {
- if (!content || !content.trim()) {
- ElMessage.warning(t('pages.chat.inputRequired'))
- return
- }
- if (activeTargetType.value === 'agent' && !settingsDraft.agentId) {
- 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
- senderValue.value = ''
- const userMsg = createUserMessageWithAttachments(content)
- messages.value.push(userMsg)
- await scrollToBottom()
- const aiMsg = createAiMessage()
- messages.value.push(aiMsg)
- const aiMessageKey = aiMsg.key as string
- const streamToken = `${aiMessageKey}-${Date.now()}`
- activeStreamToken.value = streamToken
- await scrollToBottom()
- const getAiMessage = () => messages.value.find((item) => item.key === aiMessageKey)
- const isActiveStream = () => activeStreamToken.value === streamToken
- const requestBody = buildChatRequestBody(
- activeTargetType.value,
- currentChatConfig.value,
- activeConversationId.value,
- content,
- workflowParams
- )
- const url = getChatUrl(activeTargetType.value)
- currentAttachments.value = []
- streamChat(
- url,
- requestBody,
- // onChunk
- (event: ChatSseMessage) => {
- if (!isActiveStream()) return
- const msg = getAiMessage()
- if (!msg) return
- applyWorkflowRunnerTraceEvent(msg, event)
- applyStructuredEventToMessage(msg, normalizeWorkflowRunnerEvent(event))
- scrollToBottomIfNearBottom()
- },
- // onComplete
- () => {
- if (!isActiveStream()) return
- const msg = getAiMessage()
- if (msg) {
- msg.loading = false
- msg.isFog = false
- msg.typing = false
- msg.streamCompleted = true
- updateMessageContent(msg)
- }
- },
- // onError
- (err) => {
- if (!isActiveStream()) return
- console.error('Stream chat error:', err)
- const msg = getAiMessage()
- if (msg) {
- msg.loading = false
- msg.isFog = false
- msg.typing = false
- msg.streamCompleted = true
- msg.inlineErrors = [
- ...(msg.inlineErrors || []),
- err?.message || err?.name || t('common.error.network')
- ]
- updateMessageContent(msg)
- }
- }
- )
- }
- /**
- * 发送消息的包装方法
- * @param content - 消息内容
- */
- const sendMessage = async (content: string) => {
- await handleSend(content)
- }
- /**
- * 处理重试消息
- * @param message - 要重试的消息对象
- */
- const handleRetry = async (message: BubbleMessage) => {
- if (isLoading.value) {
- ElMessage.warning(t('pages.chat.requestInProgress'))
- return
- }
- const messageIndex = messages.value.findIndex((item) => item.id === message.id)
- if (messageIndex <= 0) return
- const userMessage = [...messages.value.slice(0, messageIndex)]
- .reverse()
- .find((item) => item.role === 'user')
- const query = userMessage?.rawText || userMessage?.content
- if (!query) {
- ElMessage.warning(t('pages.chat.retrySourceNotFound'))
- return
- }
- messages.value = messages.value.slice(0, messageIndex)
- senderValue.value = query
- await sendMessage(query)
- }
- /**
- * 处理取消请求
- */
- const handleCancel = async () => {
- cancelRequest()
- activeStreamToken.value = ''
- const msg = [...messages.value]
- .reverse()
- .find((item) => item.role === 'ai' && !item.streamCompleted)
- if (!msg) return
- msg.loading = false
- msg.isFog = false
- msg.typing = false
- msg.streamCompleted = true
- msg.stopped = true
- updateMessageContent(msg)
- if (msg.assistantMessageId) {
- try {
- await aiChat.postChatStopAnswer({
- session_id: activeConversationId.value,
- msgId: msg.assistantMessageId
- })
- } catch (error) {
- console.error('stop answer error', error)
- }
- }
- }
- </script>
- <style lang="less" scoped>
- .chat-container {
- display: flex;
- height: 100vh;
- background: var(--bg-page);
- .chat-main {
- flex: 1;
- display: flex;
- flex-direction: column;
- min-height: 0;
- overflow: hidden;
- background: var(--bg-page);
- }
- }
- .title {
- font-size: 12px;
- color: var(--text-secondary);
- flex-shrink: 0;
- }
- :deep(.el-bubble-end .el-bubble-content) {
- background-color: var(--el-color-primary-light-9);
- border-radius: 8px 0px 8px 8px;
- color: var(--text-primary);
- border: none;
- }
- :deep(.el-bubble-start .el-bubble-content) {
- background-color: var(--bg-container);
- border: none;
- color: var(--text-primary);
- }
- .chat-target-select {
- width: 132px;
- }
- .settings-switches {
- display: flex;
- align-items: center;
- flex-wrap: wrap;
- gap: 18px;
- }
- .settings-note {
- 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);
- border-radius: 20px !important;
- }
- :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;
- }
- :deep(.el-prompts-item) {
- background: var(--bg-container) !important;
- border-color: var(--border-light) !important;
- }
- :deep(.el-prompts-item.hovered) {
- background: var(--bg-overlay) !important;
- }
- :deep(.el-prompts-item.actived) {
- background: var(--bg-page) !important;
- }
- :deep(.el-prompts-item-label) {
- color: var(--text-primary) !important;
- }
- :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>
|