mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-19 03:54:19 +00:00
feat(web): Add markdown rendering support to pipeline chat messages with toggle (#1826)
* Initial plan * Add markdown rendering support to pipeline debug dialog messages with toggle button Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com> * Fix code review feedback: remove conflicting styles and imports Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com> * perf: styles * fix: websocket message broadcasting cross-contamination between person and group channels --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com> Co-authored-by: Junyan Qin <rockchinq@gmail.com>
This commit is contained in:
@@ -117,7 +117,7 @@ class WebSocketAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter)
|
|||||||
|
|
||||||
# 从message_source获取pipeline_uuid和connection_id
|
# 从message_source获取pipeline_uuid和connection_id
|
||||||
pipeline_uuid = self.ap.platform_mgr.websocket_proxy_bot.bot_entity.use_pipeline_uuid
|
pipeline_uuid = self.ap.platform_mgr.websocket_proxy_bot.bot_entity.use_pipeline_uuid
|
||||||
# session_type = 'group' if isinstance(message_source, platform_events.GroupMessage) else 'person'
|
session_type = 'group' if isinstance(message_source, platform_events.GroupMessage) else 'person'
|
||||||
|
|
||||||
# 生成新的消息ID
|
# 生成新的消息ID
|
||||||
msg_id = len(session.get_message_list(pipeline_uuid)) + 1
|
msg_id = len(session.get_message_list(pipeline_uuid)) + 1
|
||||||
@@ -134,13 +134,15 @@ class WebSocketAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter)
|
|||||||
# 保存到历史记录
|
# 保存到历史记录
|
||||||
session.get_message_list(pipeline_uuid).append(message_data)
|
session.get_message_list(pipeline_uuid).append(message_data)
|
||||||
|
|
||||||
# 直接广播到所有该pipeline的连接
|
# 直接广播到所有该pipeline的连接,包含session_type信息
|
||||||
await ws_connection_manager.broadcast_to_pipeline(
|
await ws_connection_manager.broadcast_to_pipeline(
|
||||||
pipeline_uuid,
|
pipeline_uuid,
|
||||||
{
|
{
|
||||||
'type': 'response',
|
'type': 'response',
|
||||||
|
'session_type': session_type,
|
||||||
'data': message_data.model_dump(),
|
'data': message_data.model_dump(),
|
||||||
},
|
},
|
||||||
|
session_type=session_type,
|
||||||
)
|
)
|
||||||
|
|
||||||
return message_data.model_dump()
|
return message_data.model_dump()
|
||||||
@@ -162,6 +164,7 @@ class WebSocketAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter)
|
|||||||
)
|
)
|
||||||
|
|
||||||
pipeline_uuid = self.ap.platform_mgr.websocket_proxy_bot.bot_entity.use_pipeline_uuid
|
pipeline_uuid = self.ap.platform_mgr.websocket_proxy_bot.bot_entity.use_pipeline_uuid
|
||||||
|
session_type = 'group' if isinstance(message_source, platform_events.GroupMessage) else 'person'
|
||||||
message_list = session.get_message_list(pipeline_uuid)
|
message_list = session.get_message_list(pipeline_uuid)
|
||||||
|
|
||||||
# 检查是否是新的流式消息(通过bot_message对象判断)
|
# 检查是否是新的流式消息(通过bot_message对象判断)
|
||||||
@@ -197,13 +200,15 @@ class WebSocketAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter)
|
|||||||
if is_final and bot_message.tool_calls is None:
|
if is_final and bot_message.tool_calls is None:
|
||||||
message_list[-1] = message_data
|
message_list[-1] = message_data
|
||||||
|
|
||||||
# 直接广播到所有该pipeline的连接
|
# 直接广播到所有该pipeline的连接,包含session_type信息
|
||||||
await ws_connection_manager.broadcast_to_pipeline(
|
await ws_connection_manager.broadcast_to_pipeline(
|
||||||
pipeline_uuid,
|
pipeline_uuid,
|
||||||
{
|
{
|
||||||
'type': 'response',
|
'type': 'response',
|
||||||
|
'session_type': session_type,
|
||||||
'data': message_data.model_dump(),
|
'data': message_data.model_dump(),
|
||||||
},
|
},
|
||||||
|
session_type=session_type,
|
||||||
)
|
)
|
||||||
|
|
||||||
return message_data.model_dump()
|
return message_data.model_dump()
|
||||||
@@ -344,13 +349,15 @@ class WebSocketAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter)
|
|||||||
)
|
)
|
||||||
use_session.get_message_list(pipeline_uuid).append(user_message)
|
use_session.get_message_list(pipeline_uuid).append(user_message)
|
||||||
|
|
||||||
# 广播用户消息到所有连接(包括发送者)
|
# 广播用户消息到所有连接(包括发送者),包含session_type信息
|
||||||
await ws_connection_manager.broadcast_to_pipeline(
|
await ws_connection_manager.broadcast_to_pipeline(
|
||||||
pipeline_uuid,
|
pipeline_uuid,
|
||||||
{
|
{
|
||||||
'type': 'user_message',
|
'type': 'user_message',
|
||||||
|
'session_type': session_type,
|
||||||
'data': user_message.model_dump(),
|
'data': user_message.model_dump(),
|
||||||
},
|
},
|
||||||
|
session_type=session_type,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 添加消息源
|
# 添加消息源
|
||||||
|
|||||||
@@ -134,9 +134,20 @@ class WebSocketConnectionManager:
|
|||||||
connection_ids = self.session_connections.get(session_type, set())
|
connection_ids = self.session_connections.get(session_type, set())
|
||||||
return [self.connections[cid] for cid in connection_ids if cid in self.connections]
|
return [self.connections[cid] for cid in connection_ids if cid in self.connections]
|
||||||
|
|
||||||
async def broadcast_to_pipeline(self, pipeline_uuid: str, message: dict):
|
async def broadcast_to_pipeline(self, pipeline_uuid: str, message: dict, session_type: str = None):
|
||||||
"""向指定流水线的所有连接广播消息"""
|
"""向指定流水线的所有连接广播消息
|
||||||
|
|
||||||
|
Args:
|
||||||
|
pipeline_uuid: 流水线UUID
|
||||||
|
message: 要广播的消息
|
||||||
|
session_type: 可选的会话类型过滤器,如果提供则只向匹配的session_type连接广播
|
||||||
|
"""
|
||||||
connections = await self.get_connections_by_pipeline(pipeline_uuid)
|
connections = await self.get_connections_by_pipeline(pipeline_uuid)
|
||||||
|
|
||||||
|
# 如果指定了session_type,只向匹配的连接广播
|
||||||
|
if session_type is not None:
|
||||||
|
connections = [conn for conn in connections if conn.session_type == session_type]
|
||||||
|
|
||||||
tasks = []
|
tasks = []
|
||||||
for conn in connections:
|
for conn in connections:
|
||||||
tasks.append(self.send_to_connection(conn.connection_id, message))
|
tasks.append(self.send_to_connection(conn.connection_id, message))
|
||||||
|
|||||||
@@ -74,11 +74,18 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3",
|
"@eslint/eslintrc": "^3",
|
||||||
|
"@types/debug": "^4.1.12",
|
||||||
|
"@types/estree": "^1.0.8",
|
||||||
|
"@types/estree-jsx": "^1.0.5",
|
||||||
|
"@types/hast": "^3.0.4",
|
||||||
"@types/lodash": "^4.17.16",
|
"@types/lodash": "^4.17.16",
|
||||||
|
"@types/mdast": "^4.0.4",
|
||||||
|
"@types/ms": "^2.1.0",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"@types/react-syntax-highlighter": "^15.5.13",
|
"@types/react-syntax-highlighter": "^15.5.13",
|
||||||
|
"@types/unist": "^3.0.3",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "15.2.4",
|
"eslint-config-next": "15.2.4",
|
||||||
"eslint-config-prettier": "^10.1.2",
|
"eslint-config-prettier": "^10.1.2",
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ export default function PipelineDialog({
|
|||||||
// 编辑流水线时的对话框
|
// 编辑流水线时的对话框
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="overflow-hidden p-0 !max-w-[50rem] h-[75vh] flex">
|
<DialogContent className="overflow-hidden p-0 !max-w-[80vw] h-[75vh] flex">
|
||||||
<SidebarProvider className="items-start w-full flex h-full min-h-0">
|
<SidebarProvider className="items-start w-full flex h-full min-h-0">
|
||||||
<Sidebar
|
<Sidebar
|
||||||
collapsible="none"
|
collapsible="none"
|
||||||
|
|||||||
@@ -20,6 +20,13 @@ import { toast } from 'sonner';
|
|||||||
import AtBadge from './AtBadge';
|
import AtBadge from './AtBadge';
|
||||||
import { WebSocketClient } from '@/app/infra/websocket/WebSocketClient';
|
import { WebSocketClient } from '@/app/infra/websocket/WebSocketClient';
|
||||||
import ImagePreviewDialog from './ImagePreviewDialog';
|
import ImagePreviewDialog from './ImagePreviewDialog';
|
||||||
|
import ReactMarkdown from 'react-markdown';
|
||||||
|
import remarkGfm from 'remark-gfm';
|
||||||
|
import rehypeHighlight from 'rehype-highlight';
|
||||||
|
import rehypeRaw from 'rehype-raw';
|
||||||
|
import rehypeSlug from 'rehype-slug';
|
||||||
|
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
|
||||||
|
import '@/styles/github-markdown.css';
|
||||||
|
|
||||||
interface DebugDialogProps {
|
interface DebugDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -50,7 +57,9 @@ export default function DebugDialog({
|
|||||||
const [previewImageUrl, setPreviewImageUrl] = useState<string>('');
|
const [previewImageUrl, setPreviewImageUrl] = useState<string>('');
|
||||||
const [showImagePreview, setShowImagePreview] = useState(false);
|
const [showImagePreview, setShowImagePreview] = useState(false);
|
||||||
const [quotedMessage, setQuotedMessage] = useState<Message | null>(null);
|
const [quotedMessage, setQuotedMessage] = useState<Message | null>(null);
|
||||||
const [hoveredMessageId, setHoveredMessageId] = useState<number | null>(null);
|
const [rawModeMessages, setRawModeMessages] = useState<Set<string>>(
|
||||||
|
new Set(),
|
||||||
|
);
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const popoverRef = useRef<HTMLDivElement>(null);
|
const popoverRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -195,6 +204,8 @@ export default function DebugDialog({
|
|||||||
// 监听 sessionType 和 selectedPipelineId 变化,重新加载消息和连接
|
// 监听 sessionType 和 selectedPipelineId 变化,重新加载消息和连接
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
|
// 清空当前消息,避免显示旧的消息
|
||||||
|
setMessages([]);
|
||||||
loadMessages(selectedPipelineId);
|
loadMessages(selectedPipelineId);
|
||||||
initWebSocket(selectedPipelineId);
|
initWebSocket(selectedPipelineId);
|
||||||
}
|
}
|
||||||
@@ -554,7 +565,111 @@ export default function DebugDialog({
|
|||||||
return t('bots.earlier');
|
return t('bots.earlier');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Generate a unique key for a message
|
||||||
|
const getMessageKey = (message: Message): string => {
|
||||||
|
return `${message.id}-${message.timestamp}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Toggle raw mode for a message (by default, messages are in markdown mode)
|
||||||
|
const toggleRawMode = (message: Message) => {
|
||||||
|
const key = getMessageKey(message);
|
||||||
|
setRawModeMessages((prev) => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
if (newSet.has(key)) {
|
||||||
|
newSet.delete(key);
|
||||||
|
} else {
|
||||||
|
newSet.add(key);
|
||||||
|
}
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if message has any Plain text content
|
||||||
|
const hasPlainText = (message: Message): boolean => {
|
||||||
|
return message.message_chain.some((c) => c.type === 'Plain');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Extract plain text from message chain
|
||||||
|
const getPlainText = (message: Message): string => {
|
||||||
|
return message.message_chain
|
||||||
|
.filter((c) => c.type === 'Plain')
|
||||||
|
.map((c) => (c as Plain).text)
|
||||||
|
.join('');
|
||||||
|
};
|
||||||
|
|
||||||
const renderMessageContent = (message: Message) => {
|
const renderMessageContent = (message: Message) => {
|
||||||
|
const key = getMessageKey(message);
|
||||||
|
const isRawMode = rawModeMessages.has(key);
|
||||||
|
|
||||||
|
// By default, render with markdown if there's plain text (unless raw mode is enabled)
|
||||||
|
if (!isRawMode && hasPlainText(message)) {
|
||||||
|
const plainText = getPlainText(message);
|
||||||
|
const nonPlainComponents = message.message_chain.filter(
|
||||||
|
(c) => c.type !== 'Plain' && c.type !== 'Source',
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="text-base leading-relaxed align-middle">
|
||||||
|
{/* Render non-Plain components first */}
|
||||||
|
{nonPlainComponents.map((component, index) =>
|
||||||
|
renderMessageComponent(component, index),
|
||||||
|
)}
|
||||||
|
{/* Render Plain text as markdown */}
|
||||||
|
<div className="markdown-body">
|
||||||
|
<ReactMarkdown
|
||||||
|
remarkPlugins={[remarkGfm]}
|
||||||
|
rehypePlugins={[
|
||||||
|
rehypeRaw,
|
||||||
|
rehypeHighlight,
|
||||||
|
rehypeSlug,
|
||||||
|
[
|
||||||
|
rehypeAutolinkHeadings,
|
||||||
|
{
|
||||||
|
behavior: 'wrap',
|
||||||
|
properties: {
|
||||||
|
className: ['anchor'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]}
|
||||||
|
components={{
|
||||||
|
ul: ({ children }) => <ul className="list-disc">{children}</ul>,
|
||||||
|
ol: ({ children }) => (
|
||||||
|
<ol className="list-decimal">{children}</ol>
|
||||||
|
),
|
||||||
|
li: ({ children }) => <li className="ml-4">{children}</li>,
|
||||||
|
img: ({ src, alt, ...props }) => {
|
||||||
|
const imageSrc = src || '';
|
||||||
|
|
||||||
|
if (typeof imageSrc !== 'string') {
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
src={src}
|
||||||
|
alt={alt || ''}
|
||||||
|
className="max-w-full h-auto rounded-lg my-4"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
src={imageSrc}
|
||||||
|
alt={alt || ''}
|
||||||
|
className="max-w-lg h-auto my-4"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{plainText}
|
||||||
|
</ReactMarkdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="text-base leading-relaxed align-middle whitespace-pre-wrap">
|
<div className="text-base leading-relaxed align-middle whitespace-pre-wrap">
|
||||||
{message.message_chain.map((component, index) =>
|
{message.message_chain.map((component, index) =>
|
||||||
@@ -619,68 +734,109 @@ export default function DebugDialog({
|
|||||||
<div
|
<div
|
||||||
key={message.id + message.timestamp}
|
key={message.id + message.timestamp}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex group',
|
'flex',
|
||||||
message.role === 'user' ? 'justify-end' : 'justify-start',
|
message.role === 'user' ? 'justify-end' : 'justify-start',
|
||||||
)}
|
)}
|
||||||
onMouseEnter={() => setHoveredMessageId(message.id)}
|
|
||||||
onMouseLeave={() => setHoveredMessageId(null)}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative flex items-end gap-2',
|
'max-w-3xl px-5 py-3 rounded-2xl',
|
||||||
message.role === 'user' ? 'flex-row-reverse' : 'flex-row',
|
message.role === 'user'
|
||||||
|
? 'user-message-bubble bg-blue-100 dark:bg-blue-900 text-gray-900 dark:text-gray-100 rounded-br-none'
|
||||||
|
: 'bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-gray-100 rounded-bl-none',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
{renderMessageContent(message)}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'max-w-md px-5 py-3 rounded-2xl',
|
'text-xs mt-2 flex items-center justify-between gap-2',
|
||||||
message.role === 'user'
|
message.role === 'user'
|
||||||
? 'bg-[#2288ee] text-white rounded-br-none'
|
? 'text-gray-600 dark:text-gray-300'
|
||||||
: 'bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-gray-100 rounded-bl-none',
|
: 'text-gray-500 dark:text-gray-400',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{renderMessageContent(message)}
|
<div className="flex items-center gap-2">
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'text-xs mt-2 flex items-center justify-between gap-2',
|
|
||||||
message.role === 'user'
|
|
||||||
? 'text-white/70'
|
|
||||||
: 'text-gray-500 dark:text-gray-400',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span>
|
<span>
|
||||||
{message.role === 'user'
|
{message.role === 'user'
|
||||||
? t('pipelines.debugDialog.userMessage')
|
? t('pipelines.debugDialog.userMessage')
|
||||||
: t('pipelines.debugDialog.botMessage')}
|
: t('pipelines.debugDialog.botMessage')}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[10px]">
|
{hasPlainText(message) && (
|
||||||
{formatTimestamp(getMessageTimestamp(message))}
|
<button
|
||||||
</span>
|
onClick={() => toggleRawMode(message)}
|
||||||
</div>
|
className={cn(
|
||||||
</div>
|
'px-1.5 py-0.5 rounded text-[10px] transition-colors',
|
||||||
{hoveredMessageId === message.id && (
|
message.role === 'user'
|
||||||
<Button
|
? 'hover:bg-blue-200 dark:hover:bg-blue-800'
|
||||||
variant="ghost"
|
: 'hover:bg-gray-200 dark:hover:bg-gray-700',
|
||||||
size="sm"
|
)}
|
||||||
className="h-6 px-2 text-xs opacity-0 group-hover:opacity-100 transition-opacity bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 whitespace-nowrap"
|
title={
|
||||||
onClick={() => setQuotedMessage(message)}
|
rawModeMessages.has(getMessageKey(message))
|
||||||
>
|
? t('pipelines.debugDialog.showMarkdown')
|
||||||
<svg
|
: t('pipelines.debugDialog.showRaw')
|
||||||
className="w-3 h-3 mr-1"
|
}
|
||||||
fill="none"
|
>
|
||||||
viewBox="0 0 24 24"
|
{rawModeMessages.has(getMessageKey(message)) ? (
|
||||||
stroke="currentColor"
|
<span className="flex items-center gap-0.5">
|
||||||
|
<svg
|
||||||
|
className="w-3 h-3"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path d="M14.85 3H1.15C.52 3 0 3.52 0 4.15v7.69C0 12.48.52 13 1.15 13h13.69c.64 0 1.15-.52 1.15-1.15v-7.7C16 3.52 15.48 3 14.85 3zM9 11H7V8L5.5 9.92 4 8v3H2V5h2l1.5 2L7 5h2v6zm2.99.5L9.5 8H11V5h2v3h1.5l-2.51 3.5z" />
|
||||||
|
</svg>
|
||||||
|
MD
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="flex items-center gap-0.5">
|
||||||
|
<svg
|
||||||
|
className="w-3 h-3"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M4 6h16M4 12h16M4 18h7"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{t('pipelines.debugDialog.showRaw')}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={() => setQuotedMessage(message)}
|
||||||
|
className={cn(
|
||||||
|
'px-1.5 py-0.5 rounded text-[10px] transition-colors flex items-center gap-0.5',
|
||||||
|
message.role === 'user'
|
||||||
|
? 'hover:bg-blue-200 dark:hover:bg-blue-800'
|
||||||
|
: 'hover:bg-gray-200 dark:hover:bg-gray-700',
|
||||||
|
)}
|
||||||
|
title={t('pipelines.debugDialog.reply')}
|
||||||
>
|
>
|
||||||
<path
|
<svg
|
||||||
strokeLinecap="round"
|
className="w-3 h-3"
|
||||||
strokeLinejoin="round"
|
fill="none"
|
||||||
strokeWidth={2}
|
viewBox="0 0 24 24"
|
||||||
d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6"
|
stroke="currentColor"
|
||||||
/>
|
>
|
||||||
</svg>
|
<path
|
||||||
{t('pipelines.debugDialog.reply')}
|
strokeLinecap="round"
|
||||||
</Button>
|
strokeLinejoin="round"
|
||||||
)}
|
strokeWidth={2}
|
||||||
|
d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{t('pipelines.debugDialog.reply')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<span className="text-[10px]">
|
||||||
|
{formatTimestamp(getMessageTimestamp(message))}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import rehypeHighlight from 'rehype-highlight';
|
|||||||
import rehypeSlug from 'rehype-slug';
|
import rehypeSlug from 'rehype-slug';
|
||||||
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
|
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
|
||||||
import { getAPILanguageCode } from '@/i18n/I18nProvider';
|
import { getAPILanguageCode } from '@/i18n/I18nProvider';
|
||||||
import './github-markdown.css';
|
import '@/styles/github-markdown.css';
|
||||||
|
|
||||||
export default function PluginReadme({
|
export default function PluginReadme({
|
||||||
pluginAuthor,
|
pluginAuthor,
|
||||||
|
|||||||
@@ -149,12 +149,28 @@ export class WebSocketClient {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'response':
|
case 'response':
|
||||||
|
// 检查 session_type 是否匹配 - 如果消息没有 session_type 或者不匹配当前session,都忽略
|
||||||
|
if (!data.session_type || data.session_type !== this.sessionType) {
|
||||||
|
// 忽略不匹配的 session_type 消息
|
||||||
|
console.debug(
|
||||||
|
`忽略不匹配的消息: 当前session=${this.sessionType}, 消息session=${data.session_type}`,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
if (data.data) {
|
if (data.data) {
|
||||||
this.onMessageCallback?.(data.data);
|
this.onMessageCallback?.(data.data);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'user_message':
|
case 'user_message':
|
||||||
|
// 检查 session_type 是否匹配 - 如果消息没有 session_type 或者不匹配当前session,都忽略
|
||||||
|
if (!data.session_type || data.session_type !== this.sessionType) {
|
||||||
|
// 忽略不匹配的 session_type 消息
|
||||||
|
console.debug(
|
||||||
|
`忽略不匹配的用户消息: 当前session=${this.sessionType}, 消息session=${data.session_type}`,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
// 用户消息广播(包括自己发送的消息)
|
// 用户消息广播(包括自己发送的消息)
|
||||||
if (data.data) {
|
if (data.data) {
|
||||||
this.onMessageCallback?.(data.data);
|
this.onMessageCallback?.(data.data);
|
||||||
|
|||||||
@@ -525,6 +525,8 @@ const enUS = {
|
|||||||
imageUploadFailed: 'Image upload failed',
|
imageUploadFailed: 'Image upload failed',
|
||||||
reply: 'Reply',
|
reply: 'Reply',
|
||||||
replyTo: 'Reply to',
|
replyTo: 'Reply to',
|
||||||
|
showMarkdown: 'Show Markdown',
|
||||||
|
showRaw: 'Show Raw',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
knowledge: {
|
knowledge: {
|
||||||
|
|||||||
@@ -529,6 +529,8 @@ const jaJP = {
|
|||||||
imageUploadFailed: '画像のアップロードに失敗しました',
|
imageUploadFailed: '画像のアップロードに失敗しました',
|
||||||
reply: '返信',
|
reply: '返信',
|
||||||
replyTo: '返信先',
|
replyTo: '返信先',
|
||||||
|
showMarkdown: 'Markdownで表示',
|
||||||
|
showRaw: '原文で表示',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
knowledge: {
|
knowledge: {
|
||||||
|
|||||||
@@ -508,6 +508,8 @@ const zhHans = {
|
|||||||
imageUploadFailed: '图片上传失败',
|
imageUploadFailed: '图片上传失败',
|
||||||
reply: '回复',
|
reply: '回复',
|
||||||
replyTo: '回复给',
|
replyTo: '回复给',
|
||||||
|
showMarkdown: '渲染',
|
||||||
|
showRaw: '原文',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
knowledge: {
|
knowledge: {
|
||||||
|
|||||||
@@ -506,6 +506,8 @@ const zhHant = {
|
|||||||
imageUploadFailed: '圖片上傳失敗',
|
imageUploadFailed: '圖片上傳失敗',
|
||||||
reply: '回覆',
|
reply: '回覆',
|
||||||
replyTo: '回覆給',
|
replyTo: '回覆給',
|
||||||
|
showMarkdown: '渲染',
|
||||||
|
showRaw: '原文',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
knowledge: {
|
knowledge: {
|
||||||
|
|||||||
Reference in New Issue
Block a user