AI对话框实现
请注意,功能正在开发中,代码和注释不全
场景:AI对话框实现,后端调用AI大模型。前端发送请求后端返回流式数据,进行一问一答的对话功能(场景和现在市面上多个AI模型差不多,但是没人家功能健全)。
1、功能还没完全实现,显示部分代码。
2、弹框型式,dialog简单的手动封装。
3、本场景请求头需要传入信息,引用的第三方。
参考文档:使用服务器发送事件 - Web API | MDN
一、解决方案
1、常规调用方法
const evtSource = new EventSource("//api.example.com/ssedemo.php", {withCredentials: true,
});
2、安装第三方插件
【注】由于该发送请求接口需要传header信息,原生的不支持。
npm install @microsoft/fetch-event-source
3、使用方法
import { fetchEventSource } from '@microsoft/fetch-event-source';const handleSend = async () => {// ... 之前的用户消息处理逻辑 ...try {const params = {conversationId: '',query: userMessage,};await fetchEventSource(`${prefixPath}/inoAi/chatMessages`, {method: 'POST', // 支持 POSTheaders: {'Content-Type': 'application/json','Authorization': 'Bearer your_token', // 自定义请求头},body: JSON.stringify(params), // 请求体onopen: async (response) => {if (response.ok) return;throw new Error(`Server error: ${response.status}`);},onmessage: (event) => {try {const data = JSON.parse(event.data);if (data.event === 'node_finished') {// 更新 AI 消息内容setMessages(prev => /* ... */);}} catch (e) {console.error('解析失败:', e);}},onclose: () => {// 流式结束处理setMessages(prev => /* ... */);},onerror: (err) => {throw err; // 错误处理},});} catch (error) {console.error('请求失败:', error);// 错误状态更新}
};
二、代码实现
相关代码及注释请看下面。
1、页面代码
import React, { useEffect, useRef, useState } from 'react';
import './index.less';
import { Form, Icon, TextArea, useDataSet, Button } from 'choerodon-ui/pro';
import { message } from 'choerodon-ui';
import { LabelLayout } from 'choerodon-ui/pro/lib/form/enum';
import { FieldType } from 'choerodon-ui/dataset/data-set/enum';
import { postChatMessages, postSuggested, postTaskStop } from './api';
import { ChatMessageParams } from '../interface';
import { prefixPath } from '@/api/config';
import { getEnvConfig } from 'utils/iocUtils';
import { getAccessToken } from 'utils/utils';
import { fetchEventSource } from '@microsoft/fetch-event-source';
import { CopyToClipboard } from 'react-copy-to-clipboard';
import { processMarkdown } from './store';
const Dialog = ({isOpen,title,content,confirmText = '确定',cancelText = '取消',onConfirm,onCancel,
}) => {const { API_HOST } = getEnvConfig();const token = getAccessToken();const [visible, setVisible] = useState(isOpen); // 控制弹框显示状态const [messages, setMessages] = useState<any>([]); // 存储消息列表const [inputValue, setInputValue] = useState('');const [suggestedQuestions, setSuggestedQuestions] = useState<string[]>([]); // 存储建议问题列表const [showSuggestedQuestions, setShowSuggestedQuestions] = useState(false); // 控制是否显示建议问题列表const abortControllerRef = useRef<AbortController | null>(null); // 用于中断流式请求// dsconst [, setUpdateDs] = useState(new Date().getTime());const formDataDs = useDataSet(() => {return {autoCreate: true,fields: [{name: 'ask',type: FieldType.string,label: '输入框',placeholder: '给AI发送消息',},],events: {update: () => {setUpdateDs(new Date().getTime());},},};}, []);/** 添加自动滚动效果 */const messagesEndRef = useRef<HTMLDivElement>(null);useEffect(() => {messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });}, [messages]);/** 获取建议问题列表 */const fetchSuggestedQuestionsList = async (messageId: string) => {// console.log('获取建议问题列表', messageId);try {const res = await postSuggested({ messageId });// console.log('获取建议问题列表成功:', res);setSuggestedQuestions(res.data || []); // 更新建议问题列表setShowSuggestedQuestions(true); // 显示建议问题列表} catch (error) {// console.error('获取建议问题列表失败:', error);}};/** 发送信息 */const handleSend = async () => {console.log('1、click:发送请求', formDataDs.current?.get('ask'));const userMessage = formDataDs.current?.get('ask');if (!userMessage) return;// 1、添加用户消息setMessages(prev => [...prev,{type: 'user',content: userMessage,id: Date.now().toString(),},]);// 2、添加初始AI消息占位const aiMessageId = Date.now().toString() + '-ai';setMessages(prev => [...prev,{type: 'ai',content: '', // 初始为空内容id: aiMessageId,status: 'streaming', // 添加状态标识isCompleted: false, // 标记消息是否完成conversation_id: '', // 预留字段message_id: '', // 预留字段created_at: 0, // 预留字段task_id: '', // 预留字段},]);formDataDs.current?.set('ask', '');// 3、发送请求的参数const params = {conversationId: '',query: userMessage,};console.log('2、发送请求的参数:', params);// 4、初始化 AbortControllerabortControllerRef.current = new AbortController();// 5、调用接口await fetchEventSource(`${API_HOST}${prefixPath}/inoAi/chatMessages`, {method: 'POST',body: JSON.stringify(params),headers: {'Content-type': 'application/json',Authorization: token,},signal: abortControllerRef.current.signal, // 绑定中断信号onopen: async response => {if (!response.ok) {message.error(`服务错误: ${response.status}`, 1.5, 'top');return;}},onmessage: event => {try {let content = '';// 检查 event.data 是否是字符串if (typeof event.data === 'string') {// 检查是否是 SSE 格式的数据if (event.data.startsWith('event:')) {// 解析 SSE 格式的数据const lines = event.data.split('\n');const eventData: any = {};lines.forEach(line => {if (line.startsWith('event:')) {eventData.event = line.replace(/^event:/, '').trim();} else if (line.startsWith('data:')) {eventData.data = line.replace(/^data:/, '').trim();}});// 如果是心跳消息,直接返回if (eventData.event === 'ping') {console.log('心跳消息,忽略');return;}// 如果是其他事件,解析 data 字段if (eventData.data) {const data = JSON.parse(eventData.data);// 提取需要显示的内容(根据实际数据结构调整)if (data?.event === 'node_finished') {content = data.data?.outputs?.sys?.query || '';}// 确保在 message_end 事件中更新状态为 completedif (data?.event === 'message_end') {setMessages(prev =>prev.map(msg => {if (msg.id === aiMessageId) {return {...msg,conversation_id: data.conversation_id,message_id: data.message_id,created_at: data.created_at,task_id: data.task_id,id: data.id,status: 'completed', // 明确更新状态为 completedisCompleted: true,};}return msg;}),);// AI回答完成后,调用接口获取建议问题列表fetchSuggestedQuestionsList(data.message_id);return; // 确保不再执行后续逻辑}}} else {// 如果不是 SSE 格式的数据,直接解析为 JSONconst data = JSON.parse(event.data);// console.log('33、接口返回的参数', data);// 提取需要显示的内容if (data?.event === 'message') {console.log('44', data?.event);content = data?.answer;}// 确保在 message_end 事件中更新状态为 completedif (data?.event === 'message_end') {setMessages(prev =>prev.map(msg => {if (msg.id === aiMessageId) {return {...msg,conversation_id: data.conversation_id,message_id: data.message_id,created_at: data.created_at,task_id: data.task_id,id: data.id,status: 'completed', // 明确更新状态为 completedisCompleted: true,};}return msg;}),);// AI回答完成后,调用接口获取建议问题列表fetchSuggestedQuestionsList(data.message_id);return; // 确保不再执行后续逻辑}}}// console.log('karla', content);// 更新AI消息if (content) {setMessages(prev =>prev.map(msg =>msg.id === aiMessageId? { ...msg, content: msg.content + content }: msg,),);}} catch (e) {console.error('数据解析失败:', e);}},onclose: () => {// 确保在流式传输结束时更新状态为 completedsetMessages(prev =>prev.map(msg => {if (msg.id === aiMessageId && msg.status !== 'completed') {return {...msg,status: 'completed', // 兜底逻辑,确保状态更新isCompleted: true,};}return msg;}),);},onerror: err => {console.error('流式传输错误:', err);setMessages(prev =>prev.map(msg => {if (msg.id === aiMessageId) {return { ...msg, content: '请求异常中断', status: 'error' };}return msg;}),);},});};const handleRetry = (question: string) => {// 清空旧消息setMessages(prev =>prev.filter(msg => msg.originalQuestion !== question || msg.type === 'user',),);// 自动填充输入框formDataDs.current?.set('ask', question);// 延迟触发发送(等待状态更新)setTimeout(() => {handleSend();}, 100);};/** 停止输出 */const handleStop = async (val: any) => {console.log('click:停止输入', val);// 1、请求'中断'const res = await postTaskStop({ taskId: val.task_id });console.log('中断接口返回:', res);// 2、中断流式传输if (abortControllerRef.current) {abortControllerRef.current.abort();abortControllerRef.current = null;}// 3、更新消息状态setMessages(prev =>prev.map(msg => {if (msg.task_id === val.task_id) {return {...msg,status: 'stopped', // 标记为已停止content: msg.content + '\n\n(已经停止请求)', // 添加提示};}return msg;}),);};/** 用户选择建议问题 */const handleSelectQuestion = (question: string) => {formDataDs.current?.set('ask', question); // 填充到输入框setSuggestedQuestions([]); // 清除建议问题列表setShowSuggestedQuestions(false); // 隐藏建议问题列表handleSend(); // 发送问题};/** 如果弹框不显示,则不渲染任何内容 */if (!visible) return null;/** 模块:输入框和发送按钮 */const Footer = () => {return (<><divclassName="chat-dialog__chat-editor"style={{ width: messages.length > 0 ? '100%' : '60%' }}><div className="chat-dialog__chat-editor__box">{/* 输入框 */}<div className="chat-dialog__chat-editor__box__input"><Form dataSet={formDataDs} labelLayout={LabelLayout.none}><TextAreaname="ask"// valueChangeAction={ValueChangeAction.input}autoSize={{ minRows: 2, maxRows: 6 }}// onEnterDown={(event: any) => {// if (event.key === 'Enter') {// handleSend();// }// }}/></Form></div>{/* 发送按钮 */}<div className="chat-dialog__chat-editor__box__action"><divclassName="chat-dialog__chat-editor__box__action__btn"style={{cursor: formDataDs.current?.get('ask')? 'pointer': 'not-allowed',backgroundColor: formDataDs.current?.get('ask')? '#0099F2': 'rgba(0, 0, 0, 0.04)',}}onClick={() => handleSend()}><imgsrc={formDataDs.current?.get('ask')? require('@/components/ContactUs/chat/img/btn_active.png'): require('@/components/ContactUs/chat/img/btn.png')}/></div></div></div></div></>);};return (<div className="dialog-overlay"><div className="chat-dialog">{/* Header */}<div className="chat-dialog__header"><div className="chat-dialog__header__title">AI对话框</div><divclassName="chat-dialog__header__close"onClick={() => setVisible(false)}><Icon type="close" style={{ fontSize: 16 }} /></div></div>{/* 场景一:打开时 */}{/* {messages.length == 0 && (<><div className="chat-dialog__content"><div className="chat-dialog__content__home"><div className="chat-dialog__content__home__banner"><imgsrc={require('@/components/ContactUs/chat/img/logo.png')}/></div></div><Footer /></div></>)} */}{/* 场景二:发送请求后 */}{/* {messages.length > 0 && (<> */}<div className="chat-dialog__wapper">{/* 消息框 */}<div className="chat-dialog__wapper__messages">{messages.map(item => (<divkey={item.id}className={`chat-dialog__wapper__messages__message ${item.type}`}>{/* 头像 */}<div className="message-avatar"><imgsrc={item.type === 'ai'? require('@/components/ContactUs/chat/img/ai-avatar.png'): require('@/components/ContactUs/chat/img/user-avatar.png')}alt={item.type === 'ai' ? 'AI Avatar' : 'User Avatar'}/></div>{/* 消息列 */}<div className="message-content">{/* 消息气泡 */}<divclassName="message-bubble"onClick={() => {if (item.status === 'error' && item.type === 'ai') {handleRetry(item.originalQuestion);}}}>{item.type == 'user' ? (<>{item.content}</>) : (<><div className="markdown-content"><divdangerouslySetInnerHTML={{__html: processMarkdown(item.content || ''),}}/></div></>)}{/* loading */}{item.status === 'streaming' && (<span className="streaming-indicator"></span>)}</div>{item.type === 'ai' && (<>{/* 操作栏按钮:复制按钮(流式传输中只显示内容,传输完成显示按钮) */}{item.status === 'completed' && (<div className="message-operation"><CopyToClipboard text={item.content}><divclassName="copy-button"onClick={() => {message.success('复制成功',undefined,undefined,'top',);}}><Icon type="content_copy" className="icon_copy" />复制</div></CopyToClipboard></div>)}{/* 停止输出 */}{item.status === 'streaming' && (<><div className="message-stop"><divclassName="message-stop__btn"onClick={() => handleStop(item)}>停止输出</div></div></>)}{/* 停止提示 */}{item.status === 'stopped' && (<div className="message-stopped-tip">(用户停止)</div>)}{/* 错误提示 */}{item.status === 'error' && (<div className="message-error-tip">(点击重试)</div>)}{/* 建议问题列表 */}{showSuggestedQuestions && suggestedQuestions.length > 0 && (<div className="suggested-questions">{suggestedQuestions.map((question, index) => (<divkey={index}className="suggested-question"onClick={() => handleSelectQuestion(question)}>{question}<Icontype="navigate_next"className="icon_question"/></div>))}</div>)}</>)}</div></div>))}<div ref={messagesEndRef} /></div><Footer /></div>{/* </>)} */}</div></div>);
};export default Dialog;
2、css样式
.dialog-overlay {position: fixed;top: 0;left: 0;width: 100%;height: 100%;background-color: rgba(0, 0, 0, 0.5);display: flex;justify-content: center;align-items: center;z-index: 1000;font-size: 14px;:global {// 边框去除.c7n-pro-textarea-wrapper.c7n-pro-textarea-wrapper label .c7n-pro-textarea {border: 1px solid transparent !important;}// 阴影去除.c7n-pro-textarea-wrapper.c7n-pro-textarea-wrapper.c7n-pro-textarea-focused.c7n-pro-textarea {box-shadow: none;}.c7n-pro-textarea-wrapper.c7n-pro-textarea-wrapperlabel.c7n-pro-textarea.c7n-pro-textarea {padding: 3px;}}
}/* Markdown 表格样式 */
.markdown-content {p {margin: 0;padding: 0;}table {width: 100%;border-collapse: collapse;margin: 20px 0;font-family: Arial, sans-serif;font-size: 12px;color: #333;border: 1px solid #ddd;border-radius: 8px;overflow: hidden;box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);}th,td {padding: 4px 8px;text-align: left;border-bottom: 1px solid #ddd;border-right: 1px solid #ddd;}th {background-color: #f2f2f2;font-weight: bold;color: #2c3e50;}td {background-color: #fff;}tr:nth-child(even) {background-color: #f9f9f9;}tr:hover {background-color: #f1f1f1;}th:last-child,td:last-child {border-right: none;}/* 针对第一列的样式 */th:first-child,td:first-child {white-space: nowrap; /* 防止内容换行 */// overflow: hidden; /* 隐藏溢出的内容 */// text-overflow: ellipsis; /* 显示省略号 */max-width: 200px; /* 设置最大宽度 */}
}/** dialog */
.chat-dialog {background: #f3f5fa;border-radius: 8px;padding: 0 16px 16px 16px;width: 1000px;box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);/** Header */&__header {display: flex;justify-content: space-between;align-items: center;width: 100%;height: 40px;// background: pink;border-bottom: 1px solid #c6cfd8;&__title {color: #222222;font-size: 16px;font-weight: bold;}&__close {display: flex;align-items: center;cursor: pointer;}}/** 输框和发送按钮 */&__chat-editor {width: 100%;// background: white;padding-top: 12px;&__box {width: 100%;border-radius: 12px;border: 1px solid #c6cfd8;background: white;&__input {width: 100%;}&__action {display: flex;justify-content: end;padding: 0 12px 8px 8px;&__btn {width: 28px;height: 28px;border-radius: 8px;background: rgba(0, 0, 0, 0.04);display: flex;align-items: center;justify-content: center;img {width: 80%;height: 80%;}}}}}/** 场景一:打开时 */&__content {height: 500px;overflow: auto;border: 1px solid #c6cfd8;margin: 12px 0;display: flex;align-items: center;justify-content: center;flex-direction: column;&__home {width: 100%;display: flex;flex-direction: column;align-items: center;justify-content: center;&__banner {width: 191px;height: 68px;img {width: 100%;height: 100%;}}}}/** 场景二:发送请求后 */&__wapper {display: flex;flex-direction: column;height: 500px;overflow: hidden;border: 1px solid #c6cfd8;margin: 12px 0;&__messages {flex: 1;overflow-y: auto;// padding: 16px;display: flex;flex-direction: column;gap: 12px;&__message {max-width: 70%;display: flex; // 使用 flex 布局align-items: flex-start; // 确保头像和内容顶部对齐// 头像.message-avatar {width: 40px;height: 40px;border-radius: 50%;flex-shrink: 0; // 防止头像被压缩img {width: 100%;height: 100%;object-fit: cover; // 确保图片填充容器}}// 消息内容.message-content {flex: 1; // 内容占据剩余空间background: transparent; // 内容模块的背景色}// 用户消息(靠右)&.user {align-self: flex-end;flex-direction: row-reverse; // 头像在右侧.message-content {.message-bubble {background: #0099f2; // 用户消息背景色color: white;border-radius: 12px 12px 0 12px;padding: 8px 12px;}}.message-avatar {margin-left: 8px; // 头像与内容的间距}}// AI 消息(靠左)&.ai {align-self: flex-start;flex-direction: row; // 头像在左侧.message-content {.message-bubble {background: #fff; // AI 消息背景色// border: 1px solid #c6cfd8;// border-radius: 12px 12px 12px 0;border-radius: 12px 12px 0 0;padding: 8px 12px;}}.message-avatar {margin-right: 8px; // 头像与内容的间距}.message-bubble {position: relative;min-height: 20px; // 保持最小高度}/** 方案 3:旋转的加载图标 */.streaming-indicator {display: inline-block;margin-left: 8px;width: 16px;height: 16px;border: 2px solid #0099f2;border-top-color: transparent;border-radius: 50%;animation: spin 1s linear infinite;}@keyframes spin {0% {transform: rotate(0deg);}100% {transform: rotate(360deg);}}/** 方案 1:渐隐渐现的省略号*/// .streaming-indicator {// display: inline-block;// margin-left: 8px;// animation: blink 1s infinite;// &::after {// content: '...';// }// }// @keyframes blink {// 0%,// 100% {// opacity: 1;// }// 50% {// opacity: 0.3;// }// }/** 方案 2:打字机效果 */// .streaming-indicator {// display: inline-block;// margin-left: 8px;// font-size: 14px;// overflow: hidden;// white-space: nowrap;// animation: typing 1.5s steps(3, end) infinite;// }// @keyframes typing {// 0% {// width: 0;// }// 100% {// width: 36px; // 3个字符的宽度// }// }}// 消息气泡.message-bubble {cursor: default;&[data-status='error'] {cursor: pointer;border: 1px solid #ff4d4f;&:hover {background: #fff2f0;}}}// 操作栏按钮.message-operation {width: 100%;padding: 0 12px 8px;background: white;display: flex;border-radius: 0 0 12px 0;font-size: 10px;color: #5e6772;font-weight: inherit;.icon_copy {color: #909090;font-size: 16px;margin-right: 3px;}.copy-button {cursor: pointer;padding: 4px;border-radius: 4px;transition: background-color 0.3s;&:hover {background-color: #f5f5f5;}}}// 停止输出.message-stop {padding: 8px 0;&__btn {padding: 0 8px;border: 1px solid #fb4242;border-radius: 4px;color: #fb4242;line-height: 25px;display: inline-block;cursor: pointer;}}.message-stopped-tip {color: #b5b5b5;font-size: 12px;margin-top: 8px;}// 错误提示.message-error-tip {color: #ff4d4f;font-size: 12px;margin-top: 4px;cursor: pointer;&:hover {text-decoration: underline;}}}}// 建议问题列表容器.suggested-questions {margin-top: 8px;border-radius: 8px;color: #060607;// 单个建议问题.suggested-question {display: table;margin-bottom: 8px;padding: 8px 12px;background-color: #ffffff;border: 1px solid #e5e5e5;border-radius: 10px;cursor: pointer;transition: background-color 0.3s ease, border-color 0.3s ease;// icon_问题箭头.icon_question {color: #060607;font-size: 15px;font-weight: bold;margin-top: -2px;}&:hover {background-color: #e5e7ed;border-color: #e5e7ed;}&:active {background-color: #e5e7ed;}}}}
}
3、使用的插件
【注】本来想用react-markdown 的,但是版本遇到问题,react是 16的,安装有问题,就换成了 remark。也有其他插件可以安装,自行选择。
(1)安装的插件emark、remark-gfm、remark-rehype、rehype-stringify
npm install remark remark-gfm remark-rehype rehype-stringify
(2)将 Markdown 转换为 HTML
使用 remark 和 remark-rehype 将 Markdown 转换为 HTML
import { remark } from 'remark';
import remarkGfm from 'remark-gfm';
import rehypeStringify from 'rehype-stringify'; // 这个必加,不然html显示不了// markdown 处理成html 的方法
const processMarkdown = (markdown: string) => {return remark().use(remarkGfm).processSync(markdown).toString();
};<divdangerouslySetInnerHTML={{__html: processMarkdown(demo),}}
/>
4、后端返回的数据
data:{"event": "workflow_started", "conversation_id": "9a2fbc29-01e2-4158-b548-1cb8389fbd6f", "message_id": "e48e20bb-62c8-4b2f-b53b-708002767b37", "created_at": 1742402172, "task_id": "7babb429-c95b-4011-b984-e1ee3d7d56d9", "workflow_run_id": "b88d72a1-fd92-4db6-9523-2ab29b9c1a5d", "data": {"id": "b88d72a1-fd92-4db6-9523-2ab29b9c1a5d", "workflow_id": "70c7b2c5-bffd-4648-9e83-d7e935490998", "sequence_number": 379, "inputs": {"sys.query": "\u82cf\u5dde\u6c47\u5ddd\u57fa\u672c\u4fe1\u606f", "sys.files": [], "sys.conversation_id": "9a2fbc29-01e2-4158-b548-1cb8389fbd6f", "sys.user_id": "ltc-plat-portal-wb01790t115886", "sys.app_id": "4d109bac-cc7f-4cfb-84ca-3d8f05c791f2", "sys.workflow_id": "70c7b2c5-bffd-4648-9e83-d7e935490998", "sys.workflow_run_id": "b88d72a1-fd92-4db6-9523-2ab29b9c1a5d"}, "created_at": 1742402172}}data:{"event": "node_started", "conversation_id": "9a2fbc29-01e2-4158-b548-1cb8389fbd6f", "message_id": "e48e20bb-62c8-4b2f-b53b-708002767b37", "created_at": 1742402172, "task_id": "7babb429-c95b-4011-b984-e1ee3d7d56d9", "workflow_run_id": "b88d72a1-fd92-4db6-9523-2ab29b9c1a5d", "data": {"id": "1dde4fa1-caa1-4a0a-86f7-5ffb5b9e7fb5", "node_id": "1724901426507", "node_type": "start", "title": "\u5f00\u59cb", "index": 1, "predecessor_node_id": null, "inputs": null, "created_at": 1742373372, "extras": {}}}data:{"event": "node_finished", "conversation_id": "9a2fbc29-01e2-4158-b548-1cb8389fbd6f", "message_id": "e48e20bb-62c8-4b2f-b53b-708002767b37", "created_at": 1742402172, "task_id": "7babb429-c95b-4011-b984-e1ee3d7d56d9", "workflow_run_id": "b88d72a1-fd92-4db6-9523-2ab29b9c1a5d", "data": {"id": "1dde4fa1-caa1-4a0a-86f7-5ffb5b9e7fb5", "node_id": "1724901426507", "node_type": "start", "title": "\u5f00\u59cb", "index": 1, "predecessor_node_id": null, "inputs": {"sys.query": "\u82cf\u5dde\u6c47\u5ddd\u57fa\u672c\u4fe1\u606f", "sys.files": [], "sys.conversation_id": "9a2fbc29-01e2-4158-b548-1cb8389fbd6f", "sys.user_id": "ltc-plat-portal-wb01790t115886", "sys.app_id": "4d109bac-cc7f-4cfb-84ca-3d8f05c791f2", "sys.workflow_id": "70c7b2c5-bffd-4648-9e83-d7e935490998", "sys.workflow_run_id": "b88d72a1-fd92-4db6-9523-2ab29b9c1a5d"}, "process_data": null, "outputs": {"sys.query": "\u82cf\u5dde\u6c47\u5ddd\u57fa\u672c\u4fe1\u606f", "sys.files": [], "sys.conversation_id": "9a2fbc29-01e2-4158-b548-1cb8389fbd6f", "sys.user_id": "ltc-plat-portal-wb01790t115886", "sys.app_id": "4d109bac-cc7f-4cfb-84ca-3d8f05c791f2", "sys.workflow_id": "70c7b2c5-bffd-4648-9e83-d7e935490998", "sys.workflow_run_id": "b88d72a1-fd92-4db6-9523-2ab29b9c1a5d"}, "status": "succeeded", "error": null, "elapsed_time": 0.007522, "execution_metadata": null, "created_at": 1742373372, "finished_at": 1742373372, "files": []}}data:{"event": "node_started", "conversation_id": "9a2fbc29-01e2-4158-b548-1cb8389fbd6f", "message_id": "e48e20bb-62c8-4b2f-b53b-708002767b37", "created_at": 1742402172, "task_id": "7babb429-c95b-4011-b984-e1ee3d7d56d9", "workflow_run_id": "b88d72a1-fd92-4db6-9523-2ab29b9c1a5d", "data": {"id": "0308fa85-77b2-4ace-b042-d4770a6b371b", "node_id": "1730859408629", "node_type": "http-request", "title": "\u83b7\u53d6paas\u5e73\u53f0\u5ba2\u6237\u7aeftoken", "index": 2, "predecessor_node_id": "1724901426507", "inputs": null, "created_at": 1742373372, "extras": {}}}