请注意,功能正在开发中,代码和注释不全
场景: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', // 支持 POST
headers: {
'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); // 用于中断流式请求
// ds
const [, 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、初始化 AbortController
abortControllerRef.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 事件中更新状态为 completed
if (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', // 明确更新状态为 completed
isCompleted: true,
};
}
return msg;
}),
);
// AI回答完成后,调用接口获取建议问题列表
fetchSuggestedQuestionsList(data.message_id);
return; // 确保不再执行后续逻辑
}
}
} else {
// 如果不是 SSE 格式的数据,直接解析为 JSON
const data = JSON.parse(event.data);
// console.log('33、接口返回的参数', data);
// 提取需要显示的内容
if (data?.event === 'message') {
console.log('44', data?.event);
content = data?.answer;
}
// 确保在 message_end 事件中更新状态为 completed
if (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', // 明确更新状态为 completed
isCompleted: 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: () => {
// 确保在流式传输结束时更新状态为 completed
setMessages(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 (
<>
<div
className="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}>
<TextArea
name="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">
<div
className="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()}
>
<img
src={
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>
<div
className="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">
<img
src={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 => (
<div
key={item.id}
className={`chat-dialog__wapper__messages__message ${item.type}`}
>
{/* 头像 */}
<div className="message-avatar">
<img
src={
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">
{/* 消息气泡 */}
<div
className="message-bubble"
onClick={() => {
if (item.status === 'error' && item.type === 'ai') {
handleRetry(item.originalQuestion);
}
}}
>
{item.type == 'user' ? (
<>{item.content}</>
) : (
<>
<div className="markdown-content">
<div
dangerouslySetInnerHTML={{
__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}>
<div
className="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">
<div
className="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) => (
<div
key={index}
className="suggested-question"
onClick={() => handleSelectQuestion(question)}
>
{question}
<Icon
type="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-wrapper
label
.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();
};
<div
dangerouslySetInnerHTML={{
__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": {}}}