refactor: switch webchat from sse to websocket (#1808)

* refactor: switch webchat from sse to websocket

* perf: image preview dialog

* chore: remove console.log
This commit is contained in:
Junyan Qin (Chin)
2025-11-28 14:54:01 +08:00
committed by GitHub
parent 348620ac0a
commit d09b823c49
39 changed files with 2656 additions and 783 deletions

View File

@@ -129,7 +129,6 @@ export default function BotForm({
form.setValue('adapter_config', val.adapter_config);
form.setValue('enable', val.enable);
form.setValue('use_pipeline_uuid', val.use_pipeline_uuid || '');
console.log('form', form.getValues());
handleAdapterSelect(val.adapter);
// dynamicForm.setFieldsValue(val.adapter_config);
})
@@ -145,7 +144,6 @@ export default function BotForm({
async function initBotFormComponent() {
// 初始化流水线列表
const pipelinesRes = await httpClient.getPipelines();
console.log('rawPipelineList', pipelinesRes);
setPipelineNameList(
pipelinesRes.pipelines.map((item) => {
return {
@@ -157,7 +155,6 @@ export default function BotForm({
// 拉取adapter
const adaptersRes = await httpClient.getAdapters();
console.log('rawAdapterList', adaptersRes);
setAdapterNameList(
adaptersRes.adapters.map((item) => {
return {
@@ -253,12 +250,10 @@ export default function BotForm({
}
// 只有通过外层固定表单验证才会走到这里,真正的提交逻辑在这里
function onDynamicFormSubmit(value: object) {
function onDynamicFormSubmit() {
setIsLoading(true);
console.log('set loading', true);
if (initBotId) {
// 编辑提交
// console.log('submit edit', form.getFieldsValue(), value);
const updateBot: Bot = {
uuid: initBotId,
name: form.getValues().name,
@@ -270,8 +265,7 @@ export default function BotForm({
};
httpClient
.updateBot(initBotId, updateBot)
.then((res) => {
console.log('update bot success', res);
.then(() => {
onFormSubmit(form.getValues());
toast.success(t('bots.saveSuccess'));
})
@@ -285,7 +279,6 @@ export default function BotForm({
});
} else {
// 创建提交
console.log('submit create', form.getValues(), value);
const newBot: Bot = {
name: form.getValues().name,
description: form.getValues().description,
@@ -295,7 +288,6 @@ export default function BotForm({
httpClient
.createBot(newBot)
.then((res) => {
console.log('create bot success', res);
toast.success(t('bots.createSuccess'));
initBotId = res.uuid;

View File

@@ -86,7 +86,6 @@ export default function BotConfigPage() {
}
function handleNewBotCreated(botId: string) {
console.log('new bot created', botId);
getBotList();
setSelectedBotId(botId);
}

View File

@@ -110,8 +110,6 @@ export default function DynamicFormComponent({
// 当 initialValues 变化时更新表单值
// 但要避免因为内部表单更新触发的 onSubmit 导致的 initialValues 变化而重新设置表单
useEffect(() => {
console.log('initialValues', initialValues);
// 首次挂载时,使用 initialValues 初始化表单
if (isInitialMount.current) {
isInitialMount.current = false;
@@ -148,7 +146,6 @@ export default function DynamicFormComponent({
const subscription = form.watch(() => {
// 获取完整的表单值,确保包含所有默认值
const formValues = form.getValues();
console.log('formValues', formValues);
const finalValues = itemConfigList.reduce(
(acc, item) => {
acc[item.name] = formValues[item.name] ?? item.default;
@@ -156,7 +153,6 @@ export default function DynamicFormComponent({
},
{} as Record<string, object>,
);
console.log('finalValues', finalValues);
onSubmit?.(finalValues);
});
return () => subscription.unsubscribe();

View File

@@ -66,7 +66,6 @@ export default function HomeSidebar({
.catch((error) => {
console.error('Failed to fetch GitHub star count:', error);
});
return () => console.log('sidebar.unmounted');
}, []);
function handleChildClick(child: SidebarChildVO) {
@@ -90,7 +89,6 @@ export default function HomeSidebar({
}
function handleRoute(child: SidebarChildVO) {
console.log(child);
router.push(`${child.route}`);
}
@@ -102,7 +100,6 @@ export default function HomeSidebar({
routeList[1] === 'home' &&
sidebarConfigList.find((childConfig) => childConfig.route === pathname)
) {
console.log('find success');
const routeSelectChild = sidebarConfigList.find(
(childConfig) => childConfig.route === pathname,
);
@@ -144,7 +141,6 @@ export default function HomeSidebar({
<div
key={config.id}
onClick={() => {
console.log('click:', config.id);
handleChildClick(config);
}}
>

View File

@@ -103,8 +103,6 @@ export default function KBForm({
};
const onSubmit = (data: z.infer<typeof formSchema>) => {
console.log('data', data);
if (initKbId) {
// update knowledge base
const updateKb: KnowledgeBase = {
@@ -116,7 +114,6 @@ export default function KBForm({
httpClient
.updateKnowledgeBase(initKbId, updateKb)
.then((res) => {
console.log('update knowledge base success', res);
onKbUpdated(res.uuid);
toast.success(t('knowledge.updateKnowledgeBaseSuccess'));
})
@@ -135,7 +132,6 @@ export default function KBForm({
httpClient
.createKnowledgeBase(newKb)
.then((res) => {
console.log('create knowledge base success', res);
onNewKbCreated(res.uuid);
})
.catch((err) => {
@@ -200,7 +196,6 @@ export default function KBForm({
disabled={!!initKbId}
onValueChange={(value) => {
field.onChange(value);
console.log('value', value);
}}
value={field.value}
>

View File

@@ -326,8 +326,7 @@ export default function EmbeddingForm({
api_keys: apiKey ? [apiKey] : [],
extra_args: extraArgsObj,
})
.then((res) => {
console.log(res);
.then(() => {
toast.success(t('models.testSuccess'));
})
.catch(() => {

View File

@@ -341,8 +341,7 @@ export default function LLMForm({
abilities: form.getValues('abilities'),
extra_args: extraArgsObj,
})
.then((res) => {
console.log(res);
.then(() => {
toast.success(t('models.testSuccess'));
})
.catch(() => {

View File

@@ -54,7 +54,6 @@ export default function LLMConfigPage() {
.getProviderLLMModels()
.then((resp) => {
const llmModelList: LLMCardVO[] = resp.models.map((model: LLMModel) => {
console.log('model', model);
return new LLMCardVO({
id: model.uuid,
iconURL: httpClient.getProviderRequesterIconURL(model.requester),
@@ -66,7 +65,6 @@ export default function LLMConfigPage() {
abilities: model.abilities || [],
});
});
console.log('get llmModelList', llmModelList);
setCardList(llmModelList);
})
.catch((err) => {
@@ -78,7 +76,6 @@ export default function LLMConfigPage() {
function selectLLM(cardVO: LLMCardVO) {
setIsEditForm(true);
setNowSelectedLLM(cardVO);
console.log('set now vo', cardVO);
setModalOpen(true);
}
function handleCreateModelClick() {

View File

@@ -49,6 +49,7 @@ export default function PipelineDialog({
propPipelineId,
);
const [currentMode, setCurrentMode] = useState<DialogMode>('config');
const [isWebSocketConnected, setIsWebSocketConnected] = useState(false);
useEffect(() => {
setPipelineId(propPipelineId);
@@ -184,10 +185,29 @@ export default function PipelineDialog({
</Sidebar>
<main className="flex flex-1 flex-col h-full min-h-0">
<DialogHeader
className="px-6 pt-6 pb-4 shrink-0"
className="px-6 pt-6 pb-4 shrink-0 flex flex-row items-center justify-start"
style={{ height: '4rem' }}
>
<DialogTitle>{getDialogTitle()}</DialogTitle>
{currentMode === 'debug' && (
<div className="flex items-center gap-2 ml-2">
<div
className={`w-2.5 h-2.5 rounded-full ${
isWebSocketConnected ? 'bg-green-500' : 'bg-red-500'
}`}
title={
isWebSocketConnected
? t('pipelines.debugDialog.connected')
: t('pipelines.debugDialog.disconnected')
}
/>
<span className="text-sm text-gray-600 dark:text-gray-400">
{isWebSocketConnected
? t('pipelines.debugDialog.connected')
: t('pipelines.debugDialog.disconnected')}
</span>
</div>
)}
</DialogHeader>
<div
className="flex-1 overflow-y-auto px-6 pb-4 w-full"
@@ -217,6 +237,7 @@ export default function PipelineDialog({
open={true}
pipelineId={pipelineId}
isEmbedded={true}
onConnectionStatusChange={setIsWebSocketConnected}
/>
)}
</div>

View File

@@ -4,30 +4,32 @@ import { httpClient } from '@/app/infra/http/HttpClient';
import { DialogContent } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { ScrollArea } from '@/components/ui/scroll-area';
import { cn } from '@/lib/utils';
import { Message } from '@/app/infra/entities/message';
import {
Message,
MessageChainComponent,
Image,
Plain,
At,
} from '@/app/infra/entities/message';
import { toast } from 'sonner';
import AtBadge from './AtBadge';
import { Switch } from '@/components/ui/switch';
interface MessageComponent {
type: 'At' | 'Plain';
target?: string;
text?: string;
}
import { WebSocketClient } from '@/app/infra/websocket/WebSocketClient';
import ImagePreviewDialog from './ImagePreviewDialog';
interface DebugDialogProps {
open: boolean;
pipelineId: string;
isEmbedded?: boolean;
onConnectionStatusChange?: (isConnected: boolean) => void;
}
export default function DebugDialog({
open,
pipelineId,
isEmbedded = false,
onConnectionStatusChange,
}: DebugDialogProps) {
const { t } = useTranslation();
const [selectedPipelineId, setSelectedPipelineId] = useState(pipelineId);
@@ -37,10 +39,19 @@ export default function DebugDialog({
const [showAtPopover, setShowAtPopover] = useState(false);
const [hasAt, setHasAt] = useState(false);
const [isHovering, setIsHovering] = useState(false);
const [isStreaming, setIsStreaming] = useState(true);
const [isConnected, setIsConnected] = useState(false);
const [selectedImages, setSelectedImages] = useState<
Array<{ file: File; preview: string; fileKey?: string }>
>([]);
const [isUploading, setIsUploading] = useState(false);
const [previewImageUrl, setPreviewImageUrl] = useState<string>('');
const [showImagePreview, setShowImagePreview] = useState(false);
const messagesEndRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
const popoverRef = useRef<HTMLDivElement>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const wsClientRef = useRef<WebSocketClient | null>(null);
const isInitializingRef = useRef<boolean>(false);
const scrollToBottom = useCallback(() => {
// 使用setTimeout确保在DOM更新后执行滚动
@@ -60,7 +71,7 @@ export default function DebugDialog({
const loadMessages = useCallback(
async (pipelineId: string) => {
try {
const response = await httpClient.getWebChatHistoryMessages(
const response = await httpClient.getWebSocketHistoryMessages(
pipelineId,
sessionType,
);
@@ -71,23 +82,123 @@ export default function DebugDialog({
},
[sessionType],
);
// 初始化WebSocket连接
const initWebSocket = useCallback(
async (pipelineId: string) => {
// 防止重复初始化
if (isInitializingRef.current) {
return;
}
try {
isInitializingRef.current = true;
// 断开旧连接
if (wsClientRef.current) {
wsClientRef.current.disconnect();
wsClientRef.current = null;
}
// 创建新连接
const wsClient = new WebSocketClient(pipelineId, sessionType);
wsClient
.onConnected(() => {
setIsConnected(true);
isInitializingRef.current = false;
})
.onMessage((wsMessage) => {
// 将 WebSocketMessage 转换为 Message 类型
const message: Message = {
...wsMessage,
message_chain: wsMessage.message_chain as MessageChainComponent[],
};
setMessages((prevMessages) => {
// 查找是否已存在相同ID的消息
const existingIndex = prevMessages.findIndex(
(m) => m.id === message.id,
);
if (existingIndex >= 0) {
// 更新已存在的消息(流式输出)
const newMessages = [...prevMessages];
newMessages[existingIndex] = message;
return newMessages;
} else {
// 添加新消息
return [...prevMessages, message];
}
});
})
.onError((error) => {
console.error('WebSocket错误:', error);
setIsConnected(false);
isInitializingRef.current = false;
toast.error(t('pipelines.debugDialog.connectionError'));
})
.onClose(() => {
setIsConnected(false);
isInitializingRef.current = false;
})
.onBroadcast((message) => {
toast.info(message);
});
await wsClient.connect();
wsClientRef.current = wsClient;
} catch (error) {
console.error('WebSocket连接失败:', error);
setIsConnected(false);
isInitializingRef.current = false;
toast.error(t('pipelines.debugDialog.connectionFailed'));
}
},
[sessionType, t],
);
// 在useEffect中监听messages变化时滚动
useEffect(() => {
scrollToBottom();
}, [messages, scrollToBottom]);
// 监听 open 和 pipelineId 变化,进入时连接,离开时断开
useEffect(() => {
if (open) {
setSelectedPipelineId(pipelineId);
loadMessages(pipelineId);
} else {
// 关闭对话框时立即断开WebSocket
if (wsClientRef.current) {
wsClientRef.current.disconnect();
wsClientRef.current = null;
setIsConnected(false);
isInitializingRef.current = false;
}
}
return () => {
// 组件卸载时断开WebSocket
if (wsClientRef.current) {
wsClientRef.current.disconnect();
wsClientRef.current = null;
isInitializingRef.current = false;
}
};
}, [open, pipelineId]);
// 监听 sessionType 和 selectedPipelineId 变化,重新加载消息和连接
useEffect(() => {
if (open) {
loadMessages(selectedPipelineId);
initWebSocket(selectedPipelineId);
}
}, [sessionType, selectedPipelineId, open, loadMessages]);
}, [sessionType, selectedPipelineId, open, loadMessages, initWebSocket]);
// 通知父组件连接状态变化
useEffect(() => {
onConnectionStatusChange?.(isConnected);
}, [isConnected, onConnectionStatusChange]);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
@@ -147,10 +258,42 @@ export default function DebugDialog({
}
};
const handleImageSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (!files || files.length === 0) return;
const newImages: Array<{ file: File; preview: string }> = [];
for (let i = 0; i < files.length; i++) {
const file = files[i];
if (file.type.startsWith('image/')) {
const preview = URL.createObjectURL(file);
newImages.push({ file, preview });
}
}
setSelectedImages((prev) => [...prev, ...newImages]);
};
const handleRemoveImage = (index: number) => {
setSelectedImages((prev) => {
const newImages = [...prev];
URL.revokeObjectURL(newImages[index].preview);
newImages.splice(index, 1);
return newImages;
});
};
const sendMessage = async () => {
if (!inputValue.trim() && !hasAt) return;
if (!inputValue.trim() && !hasAt && selectedImages.length === 0) return;
if (!isConnected || !wsClientRef.current) {
toast.error(t('pipelines.debugDialog.notConnected'));
return;
}
try {
setIsUploading(true);
const messageChain = [];
let text_content = inputValue.trim();
@@ -161,142 +304,133 @@ export default function DebugDialog({
if (hasAt) {
messageChain.push({
type: 'At',
target: 'webchatbot',
target: 'websocketbot',
display: 'websocketbot',
});
}
messageChain.push({
type: 'Plain',
text: text_content,
});
if (hasAt) {
// for showing
text_content = '@webchatbot' + text_content;
// 添加文本
if (text_content) {
messageChain.push({
type: 'Plain',
text: text_content,
});
}
const userMessage: Message = {
id: -1,
role: 'user',
content: text_content,
timestamp: new Date().toISOString(),
message_chain: messageChain,
};
// 根据isStreaming状态决定使用哪种传输方式
if (isStreaming) {
// streaming
// 创建初始bot消息
const placeholderRandomId = Math.floor(Math.random() * 1000000);
const botMessagePlaceholder: Message = {
id: placeholderRandomId,
role: 'assistant',
content: 'Generating...',
timestamp: new Date().toISOString(),
message_chain: [{ type: 'Plain', text: 'Generating...' }],
};
// 添加用户消息和初始bot消息到状态
setMessages((prevMessages) => [
...prevMessages,
userMessage,
botMessagePlaceholder,
]);
setInputValue('');
setHasAt(false);
// 上传图片并添加到消息链
for (const image of selectedImages) {
try {
await httpClient.sendStreamingWebChatMessage(
sessionType,
messageChain,
const result = await httpClient.uploadWebSocketImage(
selectedPipelineId,
(data) => {
// 处理流式响应数据
console.log('data', data);
if (data.message) {
// 更新完整内容
setMessages((prevMessages) => {
const updatedMessages = [...prevMessages];
const botMessageIndex = updatedMessages.findIndex(
(message) => message.id === placeholderRandomId,
);
if (botMessageIndex !== -1) {
updatedMessages[botMessageIndex] = {
...updatedMessages[botMessageIndex],
content: data.message.content,
message_chain: [
{ type: 'Plain', text: data.message.content },
],
};
}
return updatedMessages;
});
}
},
() => {},
(error) => {
// 处理错误
console.error('Streaming error:', error);
if (sessionType === 'person') {
toast.error(t('pipelines.debugDialog.sendFailed'));
}
},
image.file,
);
messageChain.push({
type: 'Image',
path: result.file_key,
});
} catch (error) {
console.error('Failed to send streaming message:', error);
if (sessionType === 'person') {
toast.error(t('pipelines.debugDialog.sendFailed'));
}
console.error('图片上传失败:', error);
toast.error(t('pipelines.debugDialog.imageUploadFailed'));
}
} else {
// non-streaming
setMessages((prevMessages) => [...prevMessages, userMessage]);
setInputValue('');
setHasAt(false);
}
const response = await httpClient.sendWebChatMessage(
sessionType,
messageChain,
selectedPipelineId,
180000,
// 清空输入框和图片
setInputValue('');
setHasAt(false);
selectedImages.forEach((img) => URL.revokeObjectURL(img.preview));
setSelectedImages([]);
// 通过WebSocket发送消息
// 不在本地添加消息等待后端广播回来带有正确的ID
wsClientRef.current.sendMessage(messageChain);
} catch (error) {
console.error('Failed to send message:', error);
toast.error(t('pipelines.debugDialog.sendFailed'));
} finally {
setIsUploading(false);
inputRef.current?.focus();
}
};
const renderMessageComponent = (
component: MessageChainComponent,
index: number,
) => {
switch (component.type) {
case 'Plain':
return <span key={index}>{(component as Plain).text}</span>;
case 'At': {
const atComponent = component as At;
// 优先使用 display如果没有则使用 target
const displayName =
atComponent.display || atComponent.target?.toString() || '';
return (
<span key={index} className="inline-flex align-middle mx-1">
<AtBadge targetName={displayName} readonly={true} />
</span>
);
}
case 'AtAll':
return (
<span key={index} className="inline-flex align-middle mx-1">
<AtBadge targetName="全体成员" readonly={true} />
</span>
);
setMessages((prevMessages) => [...prevMessages, response.message]);
}
} catch (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
error: any
) {
console.log(error, 'type of error', typeof error);
console.error('Failed to send message:', error);
case 'Image': {
const img = component as Image;
const imageUrl = img.url || (img.base64 ? img.base64 : '');
if (!error.message.includes('timeout') && sessionType === 'person') {
toast.error(t('pipelines.debugDialog.sendFailed'));
if (!imageUrl) return null;
return (
<div key={index} className="my-2">
<img
src={imageUrl}
alt="Image"
className="max-w-full max-h-96 rounded-lg cursor-pointer hover:opacity-90 transition-opacity"
onClick={() => {
setPreviewImageUrl(imageUrl);
setShowImagePreview(true);
}}
/>
</div>
);
}
} finally {
inputRef.current?.focus();
case 'File': {
const file = component as MessageChainComponent & { name?: string };
return (
<div key={index} className="my-2 flex items-center gap-2 text-sm">
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path d="M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z" />
</svg>
<span>[] {file.name || 'Unknown'}</span>
</div>
);
}
case 'Voice':
return <span key={index}>[]</span>;
case 'Source':
// Source 不显示
return null;
default:
return <span key={index}>[{component.type}]</span>;
}
};
const renderMessageContent = (message: Message) => {
return (
<span className="text-base leading-relaxed align-middle whitespace-pre-wrap">
{(message.message_chain as MessageComponent[]).map(
(component, index) => {
if (component.type === 'At') {
return (
<AtBadge
key={index}
targetName={component.target || ''}
readonly={true}
/>
);
} else if (component.type === 'Plain') {
return <span key={index}>{component.text}</span>;
}
return null;
},
<div className="text-base leading-relaxed align-middle whitespace-pre-wrap">
{message.message_chain.map((component, index) =>
renderMessageComponent(component, index),
)}
</span>
</div>
);
};
@@ -341,11 +475,10 @@ export default function DebugDialog({
<path d="M2 22C2 17.5817 5.58172 14 10 14C14.4183 14 18 17.5817 18 22H16C16 18.6863 13.3137 16 10 16C6.68629 16 4 18.6863 4 22H2ZM10 13C6.685 13 4 10.315 4 7C4 3.685 6.685 1 10 1C13.315 1 16 3.685 16 7C16 10.315 13.315 13 10 13ZM10 11C12.21 11 14 9.21 14 7C14 4.79 12.21 3 10 3C7.79 3 6 4.79 6 7C6 9.21 7.79 11 10 11ZM18.2837 14.7028C21.0644 15.9561 23 18.752 23 22H21C21 19.564 19.5483 17.4671 17.4628 16.5271L18.2837 14.7028ZM17.5962 3.41321C19.5944 4.23703 21 6.20361 21 8.5C21 11.3702 18.8042 13.7252 16 13.9776V11.9646C17.6967 11.7222 19 10.264 19 8.5C19 7.11935 18.2016 5.92603 17.041 5.35635L17.5962 3.41321Z"></path>
</svg>
</Button>
<div className="flex-1" />
</div>
<div className="flex-1 flex flex-col w-[10rem] h-full min-h-0">
<ScrollArea className="flex-1 p-6 overflow-y-auto min-h-0 bg-white dark:bg-black">
<ScrollArea className="flex-1 p-6 overflow-y-auto min-h-0 bg-white dark:bg-black scroll-area">
<div className="space-y-6">
{messages.length === 0 ? (
<div className="text-center text-muted-foreground py-12 text-lg">
@@ -389,16 +522,65 @@ export default function DebugDialog({
</div>
</ScrollArea>
{/* 图片预览区域 */}
{selectedImages.length > 0 && (
<div className="px-4 pb-2 bg-white dark:bg-black">
<div className="flex gap-2 flex-wrap">
{selectedImages.map((image, index) => (
<div key={index} className="relative group">
<img
src={image.preview}
alt={`preview-${index}`}
className="w-20 h-20 object-cover rounded-lg border border-gray-300 dark:border-gray-600"
/>
<button
onClick={() => handleRemoveImage(index)}
className="absolute -top-2 -right-2 w-5 h-5 bg-red-500 text-white rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity"
>
×
</button>
</div>
))}
</div>
</div>
)}
<div className="p-4 pb-0 bg-white dark:bg-black flex gap-2">
<div className="flex items-center gap-2">
<span className="text-sm text-gray-600">
{t('pipelines.debugDialog.streaming')}
</span>
<Switch checked={isStreaming} onCheckedChange={setIsStreaming} />
<div className="flex gap-2">
<input
ref={fileInputRef}
type="file"
accept="image/*"
multiple
onChange={handleImageSelect}
className="hidden"
/>
<Button
variant="ghost"
size="icon"
onClick={() => fileInputRef.current?.click()}
disabled={!isConnected || isUploading}
className="w-10 h-10 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700"
title="上传图片"
>
<svg
className="w-5 h-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
</Button>
</div>
<div className="flex-1 flex items-center gap-2">
{hasAt && (
<AtBadge targetName="webchatbot" onRemove={handleAtRemove} />
<AtBadge targetName="websocketbot" onRemove={handleAtRemove} />
)}
<div className="relative flex-1">
<Input
@@ -412,7 +594,8 @@ export default function DebugDialog({
? t('pipelines.debugDialog.privateChat')
: t('pipelines.debugDialog.groupChat'),
})}
className="flex-1 rounded-md px-3 py-2 border border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 focus:border-[#2288ee] transition-none text-base"
disabled={!isConnected || isUploading}
className="flex-1 rounded-md px-3 py-2 border border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 focus:border-[#2288ee] transition-none text-base disabled:opacity-50"
/>
{showAtPopover && (
<div
@@ -431,7 +614,7 @@ export default function DebugDialog({
onMouseLeave={() => setIsHovering(false)}
>
<span className="text-gray-800 dark:text-gray-200">
@webchatbot - {t('pipelines.debugDialog.atTips')}
@websocketbot - {t('pipelines.debugDialog.atTips')}
</span>
</div>
</div>
@@ -440,10 +623,14 @@ export default function DebugDialog({
</div>
<Button
onClick={sendMessage}
disabled={!inputValue.trim() && !hasAt}
className="rounded-md bg-[#2288ee] hover:bg-[#2288ee] w-20 text-white px-6 py-2 text-base font-medium transition-none flex items-center gap-2 shadow-none"
disabled={
(!inputValue.trim() && !hasAt && selectedImages.length === 0) ||
!isConnected ||
isUploading
}
className="rounded-md bg-[#2288ee] hover:bg-[#2288ee] w-20 text-white px-6 py-2 text-base font-medium transition-none flex items-center gap-2 shadow-none disabled:opacity-50"
>
<>{t('pipelines.debugDialog.send')}</>
{isUploading ? '上传中...' : t('pipelines.debugDialog.send')}
</Button>
</div>
</div>
@@ -453,16 +640,30 @@ export default function DebugDialog({
// 如果是嵌入模式,直接返回内容
if (isEmbedded) {
return (
<div className="flex flex-col h-full min-h-0">
<div className="flex-1 min-h-0 flex flex-col">{renderContent()}</div>
</div>
<>
<div className="flex flex-col h-full min-h-0">
<div className="flex-1 min-h-0 flex flex-col">{renderContent()}</div>
</div>
<ImagePreviewDialog
open={showImagePreview}
imageUrl={previewImageUrl}
onClose={() => setShowImagePreview(false)}
/>
</>
);
}
// 原有的Dialog包装
return (
<DialogContent className="!max-w-[70vw] max-w-6xl h-[70vh] p-6 flex flex-col rounded-2xl shadow-2xl bg-white dark:bg-black">
{renderContent()}
</DialogContent>
<>
<DialogContent className="!max-w-[70vw] max-w-6xl h-[70vh] p-6 flex flex-col rounded-2xl shadow-2xl bg-white dark:bg-black">
{renderContent()}
</DialogContent>
<ImagePreviewDialog
open={showImagePreview}
imageUrl={previewImageUrl}
onClose={() => setShowImagePreview(false)}
/>
</>
);
}

View File

@@ -0,0 +1,56 @@
import React from 'react';
interface ImagePreviewDialogProps {
open: boolean;
imageUrl: string;
onClose: () => void;
}
export default function ImagePreviewDialog({
open,
imageUrl,
onClose,
}: ImagePreviewDialogProps) {
if (!open) return null;
return (
<div
className="fixed inset-0 z-[100] flex items-center justify-center p-8 animate-in fade-in duration-200"
onClick={onClose}
>
{/* 背景遮罩 */}
<div className="absolute inset-0 bg-black/20 " />
{/* 内容区域 */}
<div className="relative z-10 flex flex-col items-center gap-2">
{/* 关闭按钮 - 在图片上方 */}
<button
onClick={onClose}
className="self-end w-9 h-9 rounded-full bg-white hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 text-gray-800 dark:text-gray-100 shadow-lg transition-all hover:scale-105 flex items-center justify-center"
>
<svg
className="w-4 h-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
{/* 图片 */}
<img
src={imageUrl}
alt="Preview"
className="max-w-[50vw] max-h-[50vh] object-contain rounded-lg shadow-2xl bg-white"
onClick={(e) => e.stopPropagation()}
/>
</div>
</div>
);
}

View File

@@ -159,7 +159,6 @@ export default function PipelineFormComponent({
}, [form, isEditMode]);
function handleFormSubmit(values: FormValues) {
console.log('handleFormSubmit', values);
if (isEditMode) {
handleModify(values);
} else {
@@ -168,7 +167,6 @@ export default function PipelineFormComponent({
}
function handleCreate(values: FormValues) {
console.log('handleCreate', values);
const pipeline: Pipeline = {
config: {},
description: values.basic.description,

View File

@@ -75,7 +75,6 @@ export default function PluginConfigPage() {
setPipelineList(pipelineList);
})
.catch((error) => {
console.log(error);
toast.error(t('pipelines.getPipelineListError') + error.message);
});
}

View File

@@ -329,7 +329,6 @@ function MarketPageContent({
// 安装插件
// const handleInstallPlugin = (plugin: PluginV4) => {
// console.log('install plugin', plugin);
// };
return (

View File

@@ -123,7 +123,6 @@
// function handleDragEnd(event: DragEndEvent) {
// const { active, over } = event;
// console.log('Drag end event:', { active, over });
// if (over && active.id !== over.id) {
// setSortedPlugins((items) => {

View File

@@ -266,7 +266,6 @@ export default function PluginConfigPage() {
watchTask(taskId);
})
.catch((err) => {
console.log('error when install plugin:', err);
setInstallError(err.message);
setPluginInstallStatus(PluginInstallStatus.ERROR);
});
@@ -278,7 +277,6 @@ export default function PluginConfigPage() {
watchTask(taskId);
})
.catch((err) => {
console.log('error when install plugin:', err);
setInstallError(err.message);
setPluginInstallStatus(PluginInstallStatus.ERROR);
});
@@ -431,7 +429,9 @@ export default function PluginConfigPage() {
return (
<div
className={`${styles.pageContainer} h-full flex flex-col ${isDragOver ? 'bg-blue-50' : ''}`}
className={`${styles.pageContainer} h-full flex flex-col ${
isDragOver ? 'bg-blue-50' : ''
}`}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}

View File

@@ -1,7 +1,148 @@
// Message component base interface
export interface MessageComponent {
type: string;
}
// Source component
export interface Source extends MessageComponent {
type: 'Source';
id: number | string;
timestamp: number;
}
// Plain text component
export interface Plain extends MessageComponent {
type: 'Plain';
text: string;
}
// Quote component
export interface Quote extends MessageComponent {
type: 'Quote';
id?: number;
group_id?: number | string;
sender_id?: number | string;
target_id?: number | string;
origin: MessageComponent[];
}
// At component
export interface At extends MessageComponent {
type: 'At';
target: number | string;
display?: string;
}
// AtAll component
export interface AtAll extends MessageComponent {
type: 'AtAll';
}
// Image component
export interface Image extends MessageComponent {
type: 'Image';
image_id?: string;
url?: string;
path?: string;
base64?: string;
}
// Voice component
export interface Voice extends MessageComponent {
type: 'Voice';
voice_id?: string;
url?: string;
path?: string;
base64?: string;
length?: number;
}
// File component
export interface File extends MessageComponent {
type: 'File';
id?: string;
name?: string;
size?: number;
url?: string;
}
// Unknown component
export interface Unknown extends MessageComponent {
type: 'Unknown';
text?: string;
}
// Forward message node
export interface ForwardMessageNode {
sender_id?: number | string;
sender_name?: string;
message_chain?: MessageComponent[];
message_id?: number;
}
// Forward message display
export interface ForwardMessageDisplay {
title?: string;
brief?: string;
source?: string;
preview?: string[];
summary?: string;
}
// Forward component
export interface Forward extends MessageComponent {
type: 'Forward';
display?: ForwardMessageDisplay;
node_list?: ForwardMessageNode[];
}
// WeChat specific components
export interface WeChatMiniPrograms extends MessageComponent {
type: 'WeChatMiniPrograms';
mini_app_id: string;
user_name: string;
display_name?: string;
page_path?: string;
title?: string;
image_url?: string;
}
export interface WeChatEmoji extends MessageComponent {
type: 'WeChatEmoji';
emoji_md5: string;
emoji_size: number;
}
export interface WeChatLink extends MessageComponent {
type: 'WeChatLink';
link_title?: string;
link_desc?: string;
link_url?: string;
link_thumb_url?: string;
}
// Union type for all message components
export type MessageChainComponent =
| Source
| Plain
| Quote
| At
| AtAll
| Image
| Voice
| File
| Unknown
| Forward
| WeChatMiniPrograms
| WeChatEmoji
| WeChatLink;
// Message interface
export interface Message {
id: number;
role: 'user' | 'assistant';
content: string;
message_chain: object[];
message_chain: MessageChainComponent[];
timestamp: string;
is_final?: boolean;
}

View File

@@ -22,7 +22,6 @@ import {
GetPipelineResponseData,
GetPipelineMetadataResponseData,
AsyncTask,
ApiRespWebChatMessage,
ApiRespWebChatMessages,
ApiRespKnowledgeBases,
ApiRespKnowledgeBase,
@@ -199,136 +198,58 @@ export class BackendClient extends BaseHttpClient {
});
}
// ============ Debug WebChat API ============
// ============ Debug WebChat API ============
public sendWebChatMessage(
sessionType: string,
messageChain: object[],
pipelineId: string,
timeout: number = 15000,
): Promise<ApiRespWebChatMessage> {
return this.post(
`/api/v1/pipelines/${pipelineId}/chat/send`,
{
session_type: sessionType,
message: messageChain,
},
{
timeout,
},
);
}
public async sendStreamingWebChatMessage(
sessionType: string,
messageChain: object[],
pipelineId: string,
onMessage: (data: ApiRespWebChatMessage) => void,
onComplete: () => void,
onError: (error: Error) => void,
): Promise<void> {
try {
// 构造完整的URL处理相对路径的情况
let url = `${this.baseURL}/api/v1/pipelines/${pipelineId}/chat/send`;
if (this.baseURL === '/') {
// 获取用户访问的完整URL
const baseURL = window.location.origin;
url = `${baseURL}/api/v1/pipelines/${pipelineId}/chat/send`;
}
// 使用fetch发送流式请求因为axios在浏览器环境中不直接支持流式响应
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${this.getSessionSync()}`,
},
body: JSON.stringify({
session_type: sessionType,
message: messageChain,
is_stream: true,
}),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
if (!response.body) {
throw new Error('ReadableStream not supported');
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
// 读取流式响应
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
onComplete();
break;
}
// 解码数据
buffer += decoder.decode(value, { stream: true });
// 处理完整的JSON对象
const lines = buffer.split('\n\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('data:')) {
try {
const data = JSON.parse(line.slice(5));
if (data.type === 'end') {
// 流传输结束
reader.cancel();
onComplete();
return;
}
if (data.type === 'start') {
console.log(data.type);
}
if (data.message) {
// 处理消息数据
onMessage(data);
}
} catch (error) {
console.error('Error parsing streaming data:', error);
}
}
}
}
} finally {
reader.releaseLock();
}
} catch (error) {
onError(error as Error);
}
}
public getWebChatHistoryMessages(
// ============ WebSocket Chat API ============
public getWebSocketHistoryMessages(
pipelineId: string,
sessionType: string,
): Promise<ApiRespWebChatMessages> {
return this.get(
`/api/v1/pipelines/${pipelineId}/chat/messages/${sessionType}`,
`/api/v1/pipelines/${pipelineId}/ws/messages/${sessionType}`,
);
}
public resetWebChatSession(
public async uploadWebSocketImage(
pipelineId: string,
imageFile: File,
): Promise<{ file_key: string }> {
const formData = new FormData();
formData.append('file', imageFile);
return this.postFile(`/api/v1/files/images`, formData);
}
public resetWebSocketSession(
pipelineId: string,
sessionType: string,
): Promise<{ message: string }> {
return this.post(
`/api/v1/pipelines/${pipelineId}/chat/reset/${sessionType}`,
);
return this.post(`/api/v1/pipelines/${pipelineId}/ws/reset/${sessionType}`);
}
public getWebSocketConnections(pipelineId: string): Promise<{
stats: {
total_connections: number;
pipelines: number;
connections_by_pipeline: Record<string, number>;
connections_by_session_type: Record<string, number>;
};
connections: Array<{
connection_id: string;
session_type: string;
created_at: string;
last_active: string;
is_active: boolean;
}>;
}> {
return this.get(`/api/v1/pipelines/${pipelineId}/ws/connections`);
}
public broadcastWebSocketMessage(
pipelineId: string,
message: string,
): Promise<{ message: string }> {
return this.post(`/api/v1/pipelines/${pipelineId}/ws/broadcast`, {
message,
});
}
// ============ Platform API ============

View File

@@ -97,8 +97,6 @@ export abstract class BaseHttpClient {
switch (status) {
case 401:
console.log('401 error: ', errMessage, error.request);
console.log('responseURL', error.request.responseURL);
if (typeof window !== 'undefined') {
localStorage.removeItem('token');
if (!error.request.responseURL.includes('/check-token')) {

View File

@@ -0,0 +1,295 @@
/**
* WebSocket客户端类
* 用于管理WebSocket连接和消息处理
*/
export interface WebSocketMessage {
id: number;
role: 'user' | 'assistant';
content: string;
message_chain: Array<{ type: string; text?: string; target?: string }>;
timestamp: string;
is_final?: boolean;
connection_id?: string;
}
export interface WebSocketResponse {
type:
| 'connected'
| 'response'
| 'user_message'
| 'pong'
| 'broadcast'
| 'error';
connection_id?: string;
pipeline_uuid?: string;
session_type?: string;
timestamp?: string;
data?: WebSocketMessage;
message?: string;
}
export class WebSocketClient {
private ws: WebSocket | null = null;
private connectionId: string | null = null;
private reconnectAttempts = 0;
private maxReconnectAttempts = 5;
private reconnectDelay = 3000; // 3秒重连间隔
private heartbeatInterval: NodeJS.Timeout | null = null;
private heartbeatIntervalMs = 30000; // 30秒
private isConnecting = false; // 防止重复连接
// 事件回调
private onConnectedCallback?: (data: WebSocketResponse) => void;
private onMessageCallback?: (data: WebSocketMessage) => void;
private onErrorCallback?: (error: Error) => void;
private onCloseCallback?: () => void;
private onBroadcastCallback?: (message: string) => void;
constructor(
private pipelineId: string,
private sessionType: 'person' | 'group' = 'person',
private token?: string,
) {}
/**
* 连接到WebSocket服务器
*/
public connect(): Promise<string> {
return new Promise((resolve, reject) => {
try {
// 防止重复连接
if (
this.isConnecting ||
(this.ws && this.ws.readyState === WebSocket.CONNECTING)
) {
console.warn('WebSocket正在连接中忽略重复连接请求');
reject(new Error('Connection already in progress'));
return;
}
// 如果已经连接,直接返回
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
console.warn('WebSocket已连接忽略重复连接请求');
resolve(this.connectionId || '');
return;
}
this.isConnecting = true;
// 构建WebSocket URL
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
// extract host from process.env.NEXT_PUBLIC_API_BASE_URL
const host =
process.env.NEXT_PUBLIC_API_BASE_URL?.split('://')[1] || '';
const url = `${protocol}//${host}/api/v1/pipelines/${this.pipelineId}/ws/connect?session_type=${this.sessionType}`;
this.ws = new WebSocket(url);
// 连接打开
this.ws.onopen = () => {
this.reconnectAttempts = 0;
this.isConnecting = false;
this.startHeartbeat();
};
// 接收消息
this.ws.onmessage = (event) => {
try {
const data: WebSocketResponse = JSON.parse(event.data);
this.handleMessage(data);
// 第一次连接成功
if (data.type === 'connected' && data.connection_id) {
this.connectionId = data.connection_id;
resolve(data.connection_id);
}
} catch (error) {
console.error('解析WebSocket消息失败:', error);
this.onErrorCallback?.(error as Error);
}
};
// 连接关闭
this.ws.onclose = () => {
this.isConnecting = false;
this.stopHeartbeat();
this.onCloseCallback?.();
// 自动重连
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
setTimeout(() => {
this.connect().catch(console.error);
}, this.reconnectDelay * this.reconnectAttempts);
}
};
// 连接错误
this.ws.onerror = (event) => {
console.error('WebSocket错误:', event);
this.isConnecting = false;
const error = new Error('WebSocket连接失败');
this.onErrorCallback?.(error);
reject(error);
};
} catch (error) {
this.isConnecting = false;
reject(error);
}
});
}
/**
* 处理接收到的消息
*/
private handleMessage(data: WebSocketResponse) {
switch (data.type) {
case 'connected':
this.onConnectedCallback?.(data);
break;
case 'response':
if (data.data) {
this.onMessageCallback?.(data.data);
}
break;
case 'user_message':
// 用户消息广播(包括自己发送的消息)
if (data.data) {
this.onMessageCallback?.(data.data);
}
break;
case 'pong':
// 心跳响应
break;
case 'broadcast':
if (data.message) {
this.onBroadcastCallback?.(data.message);
}
break;
case 'error':
const error = new Error(data.message || '未知错误');
this.onErrorCallback?.(error);
break;
default:
console.warn('未知消息类型:', data);
}
}
/**
* 发送消息
*/
public sendMessage(
messageChain: Array<{ type: string; text?: string; target?: string }>,
) {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
throw new Error('WebSocket未连接');
}
const message = {
type: 'message',
message: messageChain,
};
this.ws.send(JSON.stringify(message));
}
/**
* 发送心跳
*/
private sendHeartbeat() {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
return;
}
this.ws.send(JSON.stringify({ type: 'ping' }));
}
/**
* 启动心跳
*/
private startHeartbeat() {
this.stopHeartbeat();
this.heartbeatInterval = setInterval(() => {
this.sendHeartbeat();
}, this.heartbeatIntervalMs);
}
/**
* 停止心跳
*/
private stopHeartbeat() {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
}
}
/**
* 断开连接
*/
public disconnect() {
if (this.ws) {
this.stopHeartbeat();
// 停止自动重连
this.reconnectAttempts = this.maxReconnectAttempts;
// 发送断开消息
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({ type: 'disconnect' }));
}
this.ws.close();
this.ws = null;
this.connectionId = null;
this.isConnecting = false;
}
}
/**
* 获取连接ID
*/
public getConnectionId(): string | null {
return this.connectionId;
}
/**
* 获取连接状态
*/
public isConnected(): boolean {
return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
}
// ===== 事件回调设置 =====
public onConnected(callback: (data: WebSocketResponse) => void) {
this.onConnectedCallback = callback;
return this;
}
public onMessage(callback: (data: WebSocketMessage) => void) {
this.onMessageCallback = callback;
return this;
}
public onError(callback: (error: Error) => void) {
this.onErrorCallback = callback;
return this;
}
public onClose(callback: () => void) {
this.onCloseCallback = callback;
return this;
}
public onBroadcast(callback: (message: string) => void) {
this.onBroadcastCallback = callback;
return this;
}
}

View File

@@ -61,9 +61,7 @@ export default function Login() {
router.push('/register');
}
})
.catch((err) => {
console.log('error at getIsInitialized: ', err);
});
.catch(() => {});
}
function checkIfAlreadyLoggedIn() {
@@ -75,9 +73,7 @@ export default function Login() {
router.push('/home');
}
})
.catch((err) => {
console.log('error at checkIfAlreadyLoggedIn: ', err);
});
.catch(() => {});
}
function onSubmit(values: z.infer<ReturnType<typeof formSchema>>) {
handleLogin(values.email, values.password);
@@ -89,13 +85,10 @@ export default function Login() {
.then((res) => {
localStorage.setItem('token', res.token);
localStorage.setItem('userEmail', username);
console.log('login success: ', res);
router.push('/home');
toast.success(t('common.loginSuccess'));
})
.catch((err) => {
console.log('login error: ', err);
.catch(() => {
toast.error(t('common.loginFailed'));
});
}

View File

@@ -59,9 +59,7 @@ export default function Register() {
router.push('/login');
}
})
.catch((err) => {
console.log('error at getIsInitialized: ', err);
});
.catch(() => {});
}
function onSubmit(values: z.infer<ReturnType<typeof formSchema>>) {
@@ -71,13 +69,11 @@ export default function Register() {
function handleRegister(username: string, password: string) {
httpClient
.initUser(username, password)
.then((res) => {
console.log('init user success: ', res);
.then(() => {
toast.success(t('register.initSuccess'));
router.push('/login');
})
.catch((err) => {
console.log('init user error: ', err);
.catch((err: Error) => {
toast.error(t('register.initFailed') + err.message);
});
}

View File

@@ -70,13 +70,11 @@ export default function ResetPassword() {
setIsResetting(true);
httpClient
.resetPassword(email, recoveryKey, newPassword)
.then((res) => {
console.log('reset password success: ', res);
.then(() => {
toast.success(t('resetPassword.resetSuccess'));
router.push('/login');
})
.catch((err) => {
console.log('reset password error: ', err);
.catch(() => {
toast.error(t('resetPassword.resetFailed'));
})
.finally(() => {

View File

@@ -23,8 +23,6 @@ export default function I18nProvider({ children }: I18nProviderProps) {
export const extractI18nObject = (i18nObject: I18nObject): string => {
// 根据当前语言返回对应的值, fallback优先级en_US、zh_Hans、zh_Hant、ja_JP
const language = i18n.language.replace('-', '_');
console.log('language:', language);
console.log('i18nObject:', i18nObject);
if (language === 'en_US' && i18nObject.en_US) return i18nObject.en_US;
if (language === 'zh_Hans' && i18nObject.zh_Hans) return i18nObject.zh_Hans;
if (language === 'zh_Hant' && i18nObject.zh_Hant) return i18nObject.zh_Hant;

View File

@@ -518,6 +518,12 @@ const enUS = {
loadPipelinesFailed: 'Failed to load pipelines',
atTips: 'Mention the bot',
streaming: 'Streaming',
connected: 'WebSocket connected',
disconnected: 'WebSocket disconnected',
connectionError: 'WebSocket connection error',
connectionFailed: 'WebSocket connection failed',
notConnected: 'WebSocket not connected, please try again later',
imageUploadFailed: 'Image upload failed',
},
},
knowledge: {

View File

@@ -521,6 +521,13 @@ const jaJP = {
loadPipelinesFailed: 'パイプラインの読み込みに失敗しました',
atTips: 'ボットをメンション',
streaming: 'ストリーミング',
connected: 'WebSocket接続済み',
disconnected: 'WebSocket未接続',
connectionError: 'WebSocket接続エラー',
connectionFailed: 'WebSocket接続に失敗しました',
notConnected:
'WebSocketに接続されていません。しばらくしてからやり直してください',
imageUploadFailed: '画像のアップロードに失敗しました',
},
},
knowledge: {

View File

@@ -500,6 +500,12 @@ const zhHans = {
loadPipelinesFailed: '加载流水线失败',
atTips: '提及机器人',
streaming: '流式传输',
connected: 'WebSocket已连接',
disconnected: 'WebSocket未连接',
connectionError: 'WebSocket连接错误',
connectionFailed: 'WebSocket连接失败',
notConnected: 'WebSocket未连接请稍后重试',
imageUploadFailed: '图片上传失败',
},
},
knowledge: {

View File

@@ -497,6 +497,13 @@ const zhHant = {
loadMessagesFailed: '載入訊息失敗',
loadPipelinesFailed: '載入流程線失敗',
atTips: '提及機器人',
streaming: '串流傳輸',
connected: 'WebSocket已連接',
disconnected: 'WebSocket未連接',
connectionError: 'WebSocket連接錯誤',
connectionFailed: 'WebSocket連接失敗',
notConnected: 'WebSocket未連接請稍後重試',
imageUploadFailed: '圖片上傳失敗',
},
},
knowledge: {