From a125f6f0926a3f35f32af5779a55e18532cc842d Mon Sep 17 00:00:00 2001 From: rd <1344903914@qq.com> Date: Tue, 26 Aug 2025 17:59:42 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AF=B9=E8=AF=9D=E5=BC=8F=E9=A6=96?= =?UTF-8?q?=E9=A1=B5=E6=8E=A5=E5=8F=A3=E5=AF=B9=E6=8E=A5=EF=BC=8C=E9=80=BB?= =?UTF-8?q?=E8=BE=91=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- env.d.ts | 1 + .../chat-view/components/right-view/index.vue | 9 +- .../components/sender-input/index.vue | 1 + src/components/xt-chat/chat-view/constants.ts | 13 +- src/components/xt-chat/chat-view/index.vue | 79 +++-- .../xt-chat/chat-view/useChatHandler.tsx | 304 +++++++++++------- src/components/xt-chat/xt-bubble/style.scss | 21 ++ .../xt-chat/xt-bubble/xt-bubbleList.vue | 1 + src/types/chat.ts | 4 +- src/types/message.ts | 19 ++ src/utils/querySSE.ts | 24 +- src/views/home/components/created/index.vue | 22 +- 12 files changed, 320 insertions(+), 178 deletions(-) create mode 100644 src/types/message.ts diff --git a/env.d.ts b/env.d.ts index fda0b3d..d21ba2c 100644 --- a/env.d.ts +++ b/env.d.ts @@ -1,4 +1,5 @@ /// + declare module '*.vue' { import type { DefineComponent } from 'vue'; const vueComponent: DefineComponent<{}, {}, any>; diff --git a/src/components/xt-chat/chat-view/components/right-view/index.vue b/src/components/xt-chat/chat-view/components/right-view/index.vue index cd48656..a684099 100644 --- a/src/components/xt-chat/chat-view/components/right-view/index.vue +++ b/src/components/xt-chat/chat-view/components/right-view/index.vue @@ -13,14 +13,16 @@ export default { type: Boolean, default: false, }, - rightViewContent: { - type: String, - default: '', + rightViewInfo: { + type: Object, + default: {}, }, }, setup(props, { emit, expose }) { const bubbleRef = ref(null); + console.log(props.rightViewInfo) + const md = markdownit({ html: true, breaks: true, @@ -85,7 +87,6 @@ export default { variant="borderless" style={{ width: '100%' }} typing={{ step: 2, interval: 100 }} - content={props.rightViewContent} onTypingComplete={() => { console.log('onTypingComplete'); }} diff --git a/src/components/xt-chat/chat-view/components/sender-input/index.vue b/src/components/xt-chat/chat-view/components/sender-input/index.vue index 251aca6..7592456 100644 --- a/src/components/xt-chat/chat-view/components/sender-input/index.vue +++ b/src/components/xt-chat/chat-view/components/sender-input/index.vue @@ -74,6 +74,7 @@ export default { expose({ focus, + searchValue: computed(() => localSearchValue.value), }); return () => ( diff --git a/src/components/xt-chat/chat-view/constants.ts b/src/components/xt-chat/chat-view/constants.ts index 727e493..0830afc 100644 --- a/src/components/xt-chat/chat-view/constants.ts +++ b/src/components/xt-chat/chat-view/constants.ts @@ -2,6 +2,8 @@ import type { Ref } from 'vue'; import type { BubbleListProps } from '@/components/xt-chat/xt-bubble/types'; // 定义角色常量 +export const LOADING_ROLE = 'loading'; // 加载中 +export const INTELLECTUAL_THINKING_ROLE = 'intellectual_thinking'; // 智能思考标题 export const QUESTION_ROLE = 'question'; export const ANSWER_ROLE = 'text'; export const FILE_ROLE = 'file'; @@ -23,9 +25,16 @@ export const ANSWER_STYLE = { export interface UseChatHandlerReturn { roles: BubbleListProps['roles']; currentTaskId: Ref; - handleMessage: (parsedData: { event: string; data: any }) => void; + handleMessage: (parsedData: { event: string; data: MESSAGE.Answer }) => void; + handleOpen: (data: Response) => void; generateLoading: Ref; conversationList: Ref; showRightView: Ref; - rightViewContent: Ref; + rightViewInfo: Ref; + senderRef: Ref +} +export enum EnumTeamRunStatus { + TeamRunStarted = 'TeamRunStarted', // 开始 + TeamRunResponseContent = 'TeamRunResponseContent', // 执行中 + TeamRunCompleted = 'TeamRunCompleted', // 完成 } diff --git a/src/components/xt-chat/chat-view/index.vue b/src/components/xt-chat/chat-view/index.vue index 64d1cc3..4aaa9b3 100644 --- a/src/components/xt-chat/chat-view/index.vue +++ b/src/components/xt-chat/chat-view/index.vue @@ -6,12 +6,11 @@ import { Typography } from 'ant-design-vue'; import RightView from './components/right-view/index.vue'; import { useRoute } from 'vue-router'; -import { genRandomId } from '@/utils/tools'; import { useChatStore } from '@/stores/modules/chat'; import querySSE from '@/utils/querySSE'; import useChatHandler from './useChatHandler'; -import { QUESTION_ROLE, ANSWER_ROLE } from './constants'; +import { QUESTION_ROLE, LOADING_ROLE } from './constants'; export default { props: { @@ -25,12 +24,9 @@ export default { const route = useRoute(); - const senderRef = ref(null); const rightViewRef = ref(null); const bubbleListRef = ref(null); - - const { roles, showRightView, rightViewContent, currentTaskId, handleMessage, conversationList, generateLoading } = - useChatHandler(); + const sseController = ref(null); const conversationId = computed(() => { return route.params.conversationId; @@ -41,42 +37,49 @@ export default { antdMessage.warning('停止生成后可发送'); return; } + conversationList.value.push({ + role: QUESTION_ROLE, + content: message, + }); initSse({ message }); }; const handleCancel = () => { - generateLoading.value = false; // 中止当前正在输出的回答 - if (currentTaskId.value && bubbleListRef.value?.abortTypingByKey) { - bubbleListRef.value.abortTypingByKey(currentTaskId.value); + if (generateLoading.value) { + bubbleListRef.value?.abortTypingByKey(currentTaskId.value); + sseController.value?.abort?.(); } if (showRightView.value) { rightViewRef.value?.abortTyping?.(); } + + generateLoading.value = false; antdMessage.info('取消生成'); }; - const initSse = (inputInfo: CHAT.TInputInfo) => { + const initSse = (inputInfo: CHAT.TInputInfo): void => { + if (sseController.value) { + sseController.value.abort?.(); + sseController.value = null; + } + try { const { message } = inputInfo; generateLoading.value = true; - conversationList.value.push({ - role: QUESTION_ROLE, - content: message, + + sseController.value = querySSE({ + method: 'POST', + handleMessage, + handleOpen, + body: JSON.stringify({ + content: message, + session_id: conversationId.value, + agent_id: chatStore.agentInfo.agent_id, + }), }); - - const taskId = genRandomId(); - currentTaskId.value = taskId; - - const url = `http://localhost:3000/agent/input?content=${message}&session_id=${conversationId.value}&agent_id=${chatStore.agentInfo?.agent_id}`; - querySSE( - { - handleMessage, - }, - url, - ); } catch (error) { console.error('Failed to initialize SSE:', error); antdMessage.error('初始化连接失败'); @@ -84,10 +87,30 @@ export default { } }; + const { + roles, + showRightView, + rightViewInfo, + currentTaskId, + handleMessage, + handleOpen, + conversationList, + generateLoading, + senderRef, + } = useChatHandler({ + initSse, + }); + watch( () => props.inputInfo, (newVal) => { - newVal && initSse(newVal); + if (newVal) { + conversationList.value.push({ + role: QUESTION_ROLE, + content: newVal, + }); + initSse(newVal); + } }, { deep: true }, ); @@ -101,14 +124,14 @@ export default { roles={roles} items={[ ...conversationList.value, - generateLoading.value ? { loading: true, role: ANSWER_ROLE } : null, + generateLoading.value ? { loading: true, role: LOADING_ROLE } : null, ].filter(Boolean)} />
(showRightView.value = false)} /> diff --git a/src/components/xt-chat/chat-view/useChatHandler.tsx b/src/components/xt-chat/chat-view/useChatHandler.tsx index ffddd49..4d2f33b 100644 --- a/src/components/xt-chat/chat-view/useChatHandler.tsx +++ b/src/components/xt-chat/chat-view/useChatHandler.tsx @@ -1,36 +1,46 @@ -import { ref, } from 'vue'; +import { ref } from 'vue'; import type { BubbleListProps } from '@/components/xt-chat/xt-bubble/types'; import markdownit from 'markdown-it'; -import { message as antdMessage } from 'ant-design-vue'; -import { IconFile, IconCaretUp, IconDownload, IconRefresh } from '@arco-design/web-vue/es/icon'; +import { message as antdMessage, Timeline } from 'ant-design-vue'; +import { IconFile, IconCaretUp, IconDownload, IconRefresh, IconCopy } from '@arco-design/web-vue/es/icon'; import { Tooltip } from 'ant-design-vue'; import TextOverTips from '@/components/text-over-tips/index.vue'; import { genRandomId } from '@/utils/tools'; -import icon1 from "@/assets/img/agent/icon-end.png" -import icon2 from "@/assets/img/agent/icon-loading.png" +import icon1 from '@/assets/img/agent/icon-end.png'; +import icon2 from '@/assets/img/agent/icon-loading.png'; import { useClipboard } from '@vueuse/core'; -import { QUESTION_ROLE, ANSWER_ROLE, FILE_ROLE, THOUGHT_ROLE, ROLE_STYLE, ANSWER_STYLE } from './constants'; -import type { UseChatHandlerReturn } from "./constants" +import { + QUESTION_ROLE, + ANSWER_ROLE, + INTELLECTUAL_THINKING_ROLE, + FILE_ROLE, + LOADING_ROLE, + THOUGHT_ROLE, + ROLE_STYLE, + EnumTeamRunStatus, +} from './constants'; +import type { UseChatHandlerReturn } from './constants'; -/** - * 聊天处理器Hook - * @returns 包含角色配置、消息处理函数和对话列表的对象 +/** + * 聊天处理器Hook + * @returns 包含角色配置、消息处理函数和对话列表的对象 */ -export default function useChatHandler(): UseChatHandlerReturn { +export default function useChatHandler({ initSse }): UseChatHandlerReturn { // 在内部定义对话列表 const { copy } = useClipboard(); + const senderRef = ref(null); const conversationList = ref([]); const generateLoading = ref(false); const currentTaskId = ref(null); const showRightView = ref(false); - const rightViewContent = ref(''); + const rightViewInfo = ref({}); - // 初始化markdown + // 初始化markdown const md = markdownit({ html: true, breaks: true, @@ -38,8 +48,19 @@ export default function useChatHandler(): UseChatHandlerReturn { typographer: true, }); - // 定义角色配置 + // 定义角色配置 const roles: BubbleListProps['roles'] = { + [LOADING_ROLE]: { + placement: 'start', + variant: 'borderless', + style: { ...ROLE_STYLE, paddingLeft: '12px' }, + }, + [INTELLECTUAL_THINKING_ROLE]: { + placement: 'start', + variant: 'borderless', + typing: { step: 2, interval: 100 }, + style: ROLE_STYLE, + }, [ANSWER_ROLE]: { placement: 'start', variant: 'borderless', @@ -47,7 +68,7 @@ export default function useChatHandler(): UseChatHandlerReturn { onTypingComplete: () => { currentTaskId.value = null; }, - style: ROLE_STYLE + style: ROLE_STYLE, }, [FILE_ROLE]: { placement: 'start', @@ -67,24 +88,23 @@ export default function useChatHandler(): UseChatHandlerReturn {
)); }, - style: ROLE_STYLE + style: ROLE_STYLE, }, [THOUGHT_ROLE]: { placement: 'start', variant: 'borderless', - style: ROLE_STYLE + style: ROLE_STYLE, }, [QUESTION_ROLE]: { placement: 'end', shape: 'round', - style: ROLE_STYLE + style: ROLE_STYLE, }, }; - // 下载处理 - const onDownload = (content: string) => { - console.log('onDownload', content); - // 这里可以添加实际的下载逻辑 + // 下载处理 + const onDownload = () => { + console.log('onDownload', rightViewInfo.value); }; const onCopy = (content: string) => { @@ -92,123 +112,161 @@ export default function useChatHandler(): UseChatHandlerReturn { antdMessage.success('复制成功!'); }; - // 开始处理 - const handleStart = (data: any) => { - const { run_id } = data; - conversationList.value.push({ - run_id, - role: ANSWER_ROLE, - content: ( -
- 智能思考 - -
- ), - }); + // 开始处理 + const handleOpen = (data: any): void => { + // const { run_id } = data; + // currentTaskId.value = run_id; + // conversationList.value.push({ + // key: run_id, + // run_id, + // role: INTELLECTUAL_THINKING_ROLE, + // content: ( + //
+ // 智能思考 + // + //
+ // ), + // }); }; - // 节点更新处理 - const handleNodeUpdate = (data: any) => { - const { run_id, status, output } = data; - - switch (status) { - case 'TeamRunResponseContent': - conversationList.value.push({ - run_id, - content: data, - role: ANSWER_ROLE, - messageRender: (item) => ( -
- - {item.message} -
- ) - }); - break; - case 'TeamRunCompleted': - conversationList.value.push({ - run_id, - content: output, - role: ANSWER_ROLE, - messageRender: (content: string) =>
, - style: ANSWER_STYLE - }); - break; - } - }; - - // 最终结果处理 - const handleFinalResult = (data: any) => { + // 最终结果处理 + const handleFileReview = (data: MESSAGE.Answer) => { const { run_id, output } = data; showRightView.value = true; - const _files = output?.files; - rightViewContent.value = _files?.[0]?.content || ''; + // const _files = output?.files; + // rightViewInfo.value = _files?.[0]?.content || ''; - conversationList.value.push({ - run_id, - id: currentTaskId.value, - role: FILE_ROLE, - content: _files, - style: ANSWER_STYLE, - footer: ({ item }: { item: any }) => { - const nonQuestionElements = conversationList.value.filter((item) => item.role !== QUESTION_ROLE); - const isLastAnswer = nonQuestionElements[nonQuestionElements.length - 1]?.id === item.id; - - // return ( - //
- // onDownload(rightViewContent?.value || '')}> - // - // - // {isLastAnswer && onRefresh && ( - // onRefresh(currentTaskId.value!, conversationList.value.length)}> - // - // - // )} - //
- // ); - }, - }); + // conversationList.value.push({ + // run_id, + // role: FILE_ROLE, + // content: _files, + // style: ANSWER_STYLE, + // footer: ({ item }: { item: any }) => { + // const nonQuestionElements = conversationList.value.filter((item) => item.role !== QUESTION_ROLE); + // const isLastAnswer = nonQuestionElements[nonQuestionElements.length - 1]?.id === item.id; + // }, + // }); }; - // 重置生成状态 + // 重置生成状态 const resetGenerateStatus = () => { generateLoading.value = false; }; - // 错误处理 - const handleError = () => { - resetGenerateStatus(); - antdMessage.error('连接服务器失败'); - }; + // // 错误处理 + // const handleError = () => { + // resetGenerateStatus(); + // antdMessage.error('连接服务器失败'); + // }; - const onRefresh = (tempId: string, tempIndex: number) => { - generateLoading.value = true; - conversationList.value.splice(tempIndex, 1, { - id: tempId, - loading: true, + const handleTaskStart = (data: MESSAGE.Answer) => { + const { run_id } = data; + currentTaskId.value = run_id; + conversationList.value.push({ + run_id, + content: data, + output: data.output, + role: ANSWER_ROLE, + messageRender: (data: MESSAGE.Answer) => { + let outputEleClass: string = `thought-chain-output border-l-#E6E6E8 border-l-1px pl-12px relative left-6px mb-4px`; + !isLastAnswer(data) && (outputEleClass += ' hasLine pb-12px pt-4px'); + return ( + <> +
+ 智能思考 + +
+
+
+ +
{data.node}
+
+
+
+ + ); + }, }); }; - // 消息处理主函数 - const handleMessage = (parsedData: { event: string; data: any }) => { - const { event, data } = parsedData; - switch (event) { - case 'start': - handleStart(data); + const onRefresh = (run_id: string) => { + generateLoading.value = true; + conversationList.value = conversationList.value.filter((item) => item.run_id !== run_id); + initSse({ message: senderRef.value?.searchValue }); + }; + + const isLastAnswer = (data: MESSAGE.Answer) => { + const { run_id } = data; + const nonQuestionElements = conversationList.value.filter( + (item) => item.role === ANSWER_ROLE && item.run_id === run_id, + ); + return nonQuestionElements[nonQuestionElements.length - 1]?.run_id === run_id; + }; + + const isLastTask = (data: MESSAGE.Answer) => { + const { run_id } = data; + const lastElement = conversationList.value[conversationList.value.length - 1]; + return lastElement && lastElement.run_id === run_id; + }; + + // 节点更新处理 + const handleTaskUpdate = (data: MESSAGE.Answer) => { + const { run_id, output } = data; + + const existingItemIndex = conversationList.value.findIndex((item) => item.run_id === run_id); + + if (existingItemIndex !== -1 && output) { + const existingItem = conversationList.value[existingItemIndex]; + existingItem.content.output += output; + } + }; + + const handleTaskEnd = (data: MESSAGE.Answer) => { + const { run_id, output, team_session_state } = data; + + const existingItem = conversationList.value.find((item) => item.run_id === run_id); + resetGenerateStatus(); + + if (existingItem) { + existingItem.content.output += output; + existingItem.footer = ({ item: any }) => { + return ( +
+ onCopy(existingItem.content.output)}> + + + + + + {isLastTask(data) && ( + onRefresh(run_id)}> + + + )} +
+ ); + }; + } + + if (team_session_state) { + handleFileReview(data); + } + }; + // 消息处理主函数 + const handleMessage = (parsedData: { event: string; data: MESSAGE.Answer }) => { + const { data } = parsedData; + const { status } = data; + switch (status) { + case EnumTeamRunStatus.TeamRunStarted: + handleTaskStart(data); break; - case 'node_update': - handleNodeUpdate(data); + case EnumTeamRunStatus.TeamRunResponseContent: + handleTaskUpdate(data); break; - case 'final_result': - handleFinalResult(data); - break; - case 'end': - resetGenerateStatus(); - break; - case 'error': - handleError(); + case EnumTeamRunStatus.TeamRunCompleted: + handleTaskEnd(data); break; default: break; @@ -217,11 +275,13 @@ export default function useChatHandler(): UseChatHandlerReturn { return { roles, + senderRef, currentTaskId, handleMessage, + handleOpen, generateLoading, conversationList, showRightView, - rightViewContent, + rightViewInfo, }; -} \ No newline at end of file +} diff --git a/src/components/xt-chat/xt-bubble/style.scss b/src/components/xt-chat/xt-bubble/style.scss index 3821521..7b01927 100644 --- a/src/components/xt-chat/xt-bubble/style.scss +++ b/src/components/xt-chat/xt-bubble/style.scss @@ -28,6 +28,27 @@ flex-direction: row-reverse; } + .thought-chain-item { + position: relative; + margin: 0; + font-size: 14px; + list-style: none; + .thought-chain-output { + position: relative; + &.hasLine { + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border-left: 1px solid #E6E6E8; + } + } + } + } + .xt-bubble-avatar { } diff --git a/src/components/xt-chat/xt-bubble/xt-bubbleList.vue b/src/components/xt-chat/xt-bubble/xt-bubbleList.vue index 89026b3..8404bcf 100644 --- a/src/components/xt-chat/xt-bubble/xt-bubbleList.vue +++ b/src/components/xt-chat/xt-bubble/xt-bubbleList.vue @@ -105,6 +105,7 @@ export default defineComponent({ // 暴露控制方法 const abortTypingByKey = (key: string | number) => { bubbleRefs.value[key]?.abortTyping?.(); + console.log('abortTypingByKey----', bubbleRefs.value[key]) }; // 对外暴露能力 expose({ diff --git a/src/types/chat.ts b/src/types/chat.ts index 2116a91..5e25cc6 100644 --- a/src/types/chat.ts +++ b/src/types/chat.ts @@ -1,7 +1,9 @@ declare global { namespace CHAT { - export type TInputInfo = { + type TInputInfo = { message: string; }; } } + +export default {}; diff --git a/src/types/message.ts b/src/types/message.ts new file mode 100644 index 0000000..fcd7483 --- /dev/null +++ b/src/types/message.ts @@ -0,0 +1,19 @@ +declare global { + namespace MESSAGE { + type TASK_STATUS = 'TeamRunStarted' | 'TeamRunResponseContent' | 'TeamRunCompleted'; + + interface Answer { + message: string; + node: string; + output: string; + run_id: string; + status: TASK_STATUS; + extra_data: { + type: string, + data: Record + }, + team_session_state: any + } + } +} +export default {}; diff --git a/src/utils/querySSE.ts b/src/utils/querySSE.ts index 7850465..7f58191 100644 --- a/src/utils/querySSE.ts +++ b/src/utils/querySSE.ts @@ -3,8 +3,10 @@ import type { EventSourceMessage } from '@microsoft/fetch-event-source'; import { useEnterpriseStore } from '@/stores/modules/enterprise'; import { glsWithCatch } from '@/utils/stroage'; -const customHost = 'http://localhost:3000'; -const DEFAULT_SSE_URL = `${customHost}/agent/input`; +const customHost = 'http://192.168.40.41:8001'; +// +// const customHost = 'http://localhost:3000'; +const DEFAULT_SSE_URL = `${customHost}/api/agent/runs`; const SSE_HEADERS = { 'Content-Type': 'application/json', @@ -20,26 +22,30 @@ interface SSEConfig { handleMessage?: (data: any) => void; handleError?: (err: any) => number | null | undefined | void; handleClose?: () => void; - handleOpen?: (response: Response) => Promise; + handleOpen?: (response: Response) => void; } /** * 创建服务器发送事件(SSE)连接 * @param config SSE 配置 * @param url 可选的自定义 URL + * @returns 包含abort方法的对象,用于中断SSE连接 */ -export default async (config: SSEConfig, url: string = DEFAULT_SSE_URL): Promise => { +export default async (config: SSEConfig, url: string = DEFAULT_SSE_URL): Promise<{ abort: () => void }> => { const { body = undefined, headers = {}, - method = 'get', + method = 'post', handleMessage, handleError, handleOpen, handleClose, } = config; const store = useEnterpriseStore(); - + + // 创建AbortController实例用于中断请求 + const abortController = new AbortController(); + fetchEventSource(url, { method, // credentials: 'include', @@ -50,6 +56,7 @@ export default async (config: SSEConfig, url: string = DEFAULT_SSE_URL): Promise ...headers, }, body, + signal: abortController.signal, // 传递signal给fetchEventSource openWhenHidden: true, // 用户切换到另一个页面后仍能保持SSE连接 onmessage(event: EventSourceMessage) { if (event.data) { @@ -75,4 +82,9 @@ export default async (config: SSEConfig, url: string = DEFAULT_SSE_URL): Promise handleOpen?.(response); }, }); + + // 返回abort方法供外部调用 + return { + abort: () => abortController.abort() + }; }; diff --git a/src/views/home/components/created/index.vue b/src/views/home/components/created/index.vue index 3705d91..dd88f6a 100644 --- a/src/views/home/components/created/index.vue +++ b/src/views/home/components/created/index.vue @@ -21,21 +21,13 @@ export default { }; const tagList = [ - '人工智能', - '人工智能与应用', - '行业分析与市场数据', - '标签标签标签标签标签标签标签', - '标签标签标签标签标签标签标签', - '标签标签标签标签标签标签标签', - '标签标签标签标签标签标签标签', - '标签A', - '啊啊啊', - '宝宝贝贝', - '微信', - '吧啊啊', - '哦哦哦哦哦哦哦哦', - '人工智能', - '人工智能与应用', + '汇总账号昨天的运营情况', + '查账号本月点赞量 Top3 的笔记详情', + '统计投流账户上周的消耗金额 + 点击率', + '把昨天漏采数据的账号重新抓取一次数据', + '规划账号未来 2 周的内容发布排期和选题', + '根据账号的已有选题生成具体内容稿件', + '根据热点生成账号的选题及内容', ]; const handleTagClick = (tag: string) => {