fix(wecombot): improve event routing and sandbox attachment delivery

This commit is contained in:
Hyu
2026-10-10 18:41:57 +08:00
parent a2b7f6b7bb
commit 4f43da0fb3
24 changed files with 840 additions and 92 deletions
@@ -90,6 +90,14 @@ const DEBUG_EVENT_DEFINITIONS: Record<string, DebugEventDefinition> = {
messageField: 'text',
},
'message.deleted': deletion,
'wecombot.enter_chat': {
fields: [user],
defaults: { chat_id: 'debug-user', chat_type: 'private', message_id: 'debug-entry', data: {} },
},
'wecombot.template_card_event': {
fields: [user],
defaults: { chat_id: 'debug-user', chat_type: 'private', message_id: 'debug-card-click', task_id: 'debug-card', event_key: 'confirm', card_type: 'button_interaction', selected_items: [], data: {} },
},
'wecomcs.enter_session': {
fields: [user],
defaults: { open_kfid: 'debug-kf', external_userid: 'debug-user', chat_id: 'debug-user|debug-kf', scene: 'debug', scene_param: '', welcome_code: '', wechat_channels: {} },
@@ -9,6 +9,8 @@ import {
useState,
} from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import { cn } from '@/lib/utils';
import type { TFunction } from 'i18next';
import { UseFormReturn } from 'react-hook-form';
import {
@@ -30,6 +32,7 @@ import {
Play,
RefreshCw,
Shield,
Settings2,
Trash2,
UserCheck,
UserMinus,
@@ -495,6 +498,10 @@ function TargetCombobox({
const [open, setOpen] = useState(false);
const pipelineAllowed = isMessageEventPattern(binding.event_pattern);
const targetType = binding.target_type || 'agent';
const selectedTarget =
targetType !== 'discard'
? agentOptions.find((agent) => agent.uuid === binding.target_uuid)
: undefined;
const current =
targetType === 'discard'
@@ -545,63 +552,92 @@ function TargetCombobox({
}
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className="h-8 w-[200px] justify-between text-sm font-normal px-3"
>
<span className="truncate">{currentLabel()}</span>
<ChevronsUpDown className="ml-1 h-3.5 w-3.5 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[240px] p-0" align="start">
<Command>
<CommandInput placeholder={t('bots.searchTarget')} className="h-8" />
<CommandList>
<CommandEmpty>{t('bots.noTargetFound')}</CommandEmpty>
{agents.length > 0 && (
<CommandGroup heading={t('bots.targetAgent')}>
{agents.map((a) => (
<CommandItem
key={a.uuid}
value={`agent:${a.uuid}:${a.name}`}
onSelect={() => select(encodeTarget('agent', a.uuid || ''))}
>
<Bot className="mr-2 size-3.5 shrink-0" />
<span className="truncate">{targetLabel(a)}</span>
{current === encodeTarget('agent', a.uuid || '') && (
<Check className="ml-auto size-3.5 shrink-0" />
)}
</CommandItem>
))}
</CommandGroup>
<div className="relative w-[200px] shrink-0">
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className={cn(
'h-8 w-full justify-between text-sm font-normal px-3',
selectedTarget && 'pr-10',
)}
{pipelines.length > 0 && (
<CommandGroup heading={t('bots.targetPipeline')}>
{pipelines.map((a) => (
<CommandItem
key={a.uuid}
value={`pipeline:${a.uuid}:${a.name}`}
onSelect={() =>
select(encodeTarget('pipeline', a.uuid || ''))
}
>
<Workflow className="mr-2 size-3.5 shrink-0" />
<span className="truncate">{targetLabel(a)}</span>
{current === encodeTarget('pipeline', a.uuid || '') && (
<Check className="ml-auto size-3.5 shrink-0" />
)}
</CommandItem>
))}
</CommandGroup>
)}
</CommandList>
</Command>
</PopoverContent>
</Popover>
>
<span className="truncate">{currentLabel()}</span>
<ChevronsUpDown className="ml-1 h-3.5 w-3.5 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[240px] p-0" align="start">
<Command>
<CommandInput placeholder={t('bots.searchTarget')} className="h-8" />
<CommandList>
<CommandEmpty>{t('bots.noTargetFound')}</CommandEmpty>
{agents.length > 0 && (
<CommandGroup heading={t('bots.targetAgent')}>
{agents.map((a) => (
<CommandItem
key={a.uuid}
value={`agent:${a.uuid}:${a.name}`}
onSelect={() => select(encodeTarget('agent', a.uuid || ''))}
>
<Bot className="mr-2 size-3.5 shrink-0" />
<span className="truncate">{targetLabel(a)}</span>
{current === encodeTarget('agent', a.uuid || '') && (
<Check className="ml-auto size-3.5 shrink-0" />
)}
</CommandItem>
))}
</CommandGroup>
)}
{pipelines.length > 0 && (
<CommandGroup heading={t('bots.targetPipeline')}>
{pipelines.map((a) => (
<CommandItem
key={a.uuid}
value={`pipeline:${a.uuid}:${a.name}`}
onSelect={() =>
select(encodeTarget('pipeline', a.uuid || ''))
}
>
<Workflow className="mr-2 size-3.5 shrink-0" />
<span className="truncate">{targetLabel(a)}</span>
{current === encodeTarget('pipeline', a.uuid || '') && (
<Check className="ml-auto size-3.5 shrink-0" />
)}
</CommandItem>
))}
</CommandGroup>
)}
</CommandList>
</Command>
</PopoverContent>
</Popover>
{selectedTarget && (
<Tooltip>
<TooltipTrigger asChild>
<Button
asChild
variant="ghost"
size="icon"
className="absolute right-1 top-1 size-6"
>
<Link
to={`/home/agents?id=${encodeURIComponent(selectedTarget.uuid || '')}&tab=config`}
target="_blank"
rel="noopener noreferrer"
aria-label={t('bots.pluginSubscriptions.configure')}
>
<Settings2 className="size-3.5" />
</Link>
</Button>
</TooltipTrigger>
<TooltipContent>
{t('bots.pluginSubscriptions.configure')}
</TooltipContent>
</Tooltip>
)}
</div>
);
}
@@ -390,9 +390,10 @@ const BotSessionMonitor = forwardRef<
}, [selectedSessionId, messagePage, loadMessages]);
useEffect(() => {
if (loadingMessages || messageError) return;
if (messages.length === 0 && toolCalls.length === 0) return;
// Wait for DOM to render the new messages before scrolling
requestAnimationFrame(() => {
// The timeline is hidden until all message data has finished loading.
const frame = requestAnimationFrame(() => {
const container = messagesContainerRef.current;
if (container) {
const viewport = container.querySelector(
@@ -402,7 +403,8 @@ const BotSessionMonitor = forwardRef<
scrollTarget.scrollTop = scrollTarget.scrollHeight;
}
});
}, [messages, toolCalls]);
return () => cancelAnimationFrame(frame);
}, [loadingMessages, messageError, messages, toolCalls]);
const parseMessageChain = (content: string): MessageChainComponent[] => {
try {
+4
View File
@@ -1003,6 +1003,8 @@ const enUS = {
eventWildcard: 'All events',
eventNamespaceWildcard: '{{namespace}}.*',
eventNames: {
wecombot_enter_chat: "User entered bot chat",
wecombot_template_card_event: "Template card interaction",
wecomcs_enter_session: "Customer entered conversation",
wecomcs_msg_send_fail: "Customer service message failed",
wecomcs_servicer_status_change: "Representative availability changed",
@@ -1025,6 +1027,8 @@ const enUS = {
platform_specific: 'Platform-specific event',
},
eventDescriptions: {
wecombot_enter_chat: "A user opened the bot chat. Use this event for welcome workflows.",
wecombot_template_card_event: "A user clicked or submitted a template card. Managed form callbacks are handled separately.",
wecomcs_enter_session: "A customer opened a service link; includes entry source and welcome code.",
wecomcs_msg_send_fail: "WeChat reported an outgoing message failure, with message ID and failure type.",
wecomcs_servicer_status_change: "A representative started, stopped or suspended service.",
+4
View File
@@ -1022,6 +1022,8 @@ const esES = {
eventWildcard: 'Todos los eventos',
eventNamespaceWildcard: '{{namespace}}.*',
eventNames: {
wecombot_enter_chat: "Usuario entró al chat del bot",
wecombot_template_card_event: "Interacción con tarjeta",
wecomcs_enter_session: "Cliente entró en la conversación",
wecomcs_msg_send_fail: "Fallo de envío de atención al cliente",
wecomcs_servicer_status_change: "Disponibilidad del agente modificada",
@@ -1044,6 +1046,8 @@ const esES = {
platform_specific: 'Evento específico de la plataforma',
},
eventDescriptions: {
wecombot_enter_chat: "Un usuario abrió el chat del bot. Úsalo para flujos de bienvenida.",
wecombot_template_card_event: "Un usuario pulsó o envió una tarjeta. Las respuestas de formularios gestionados se procesan por separado.",
wecomcs_enter_session: "Un cliente abrió el enlace de atención; incluye origen y código de bienvenida.",
wecomcs_msg_send_fail: "WeChat notificó un fallo de envío con el ID y el tipo de error.",
wecomcs_servicer_status_change: "Un agente inició, detuvo o suspendió la atención.",
+4
View File
@@ -1008,6 +1008,8 @@ const jaJP = {
eventWildcard: 'すべてのイベント',
eventNamespaceWildcard: '{{namespace}}.*',
eventNames: {
wecombot_enter_chat: "ボットとのチャットに参加",
wecombot_template_card_event: "テンプレートカード操作",
wecomcs_enter_session: "顧客が会話に参加",
wecomcs_msg_send_fail: "カスタマーサービスの送信失敗",
wecomcs_servicer_status_change: "担当者の受付状態変更",
@@ -1030,6 +1032,8 @@ const jaJP = {
platform_specific: 'プラットフォーム固有イベント',
},
eventDescriptions: {
wecombot_enter_chat: "ユーザーがボットのチャットを開きました。歓迎や案内のフローに使用できます。",
wecombot_template_card_event: "ユーザーがカードをクリックまたは送信しました。管理対象フォームの応答は別途処理します。",
wecomcs_enter_session: "顧客がリンクから会話を開きました。流入元と歓迎メッセージ用コードを含みます。",
wecomcs_msg_send_fail: "WeChat が送信失敗を通知しました。メッセージ ID と失敗種別を含みます。",
wecomcs_servicer_status_change: "担当者が受付を開始、停止、または一時停止しました。",
+4
View File
@@ -1018,6 +1018,8 @@ const ruRU = {
eventWildcard: 'Все события',
eventNamespaceWildcard: '{{namespace}}.*',
eventNames: {
wecombot_enter_chat: "Пользователь открыл чат бота",
wecombot_template_card_event: "Взаимодействие с карточкой",
wecomcs_enter_session: "Клиент вошёл в диалог",
wecomcs_msg_send_fail: "Ошибка отправки сообщения поддержки",
wecomcs_servicer_status_change: "Статус оператора изменён",
@@ -1040,6 +1042,8 @@ const ruRU = {
platform_specific: 'Событие конкретной платформы',
},
eventDescriptions: {
wecombot_enter_chat: "Пользователь открыл чат бота. Событие подходит для сценариев приветствия.",
wecombot_template_card_event: "Пользователь нажал или отправил карточку. Ответы управляемых форм обрабатываются отдельно.",
wecomcs_enter_session: "Клиент открыл ссылку поддержки; событие содержит источник и код приветствия.",
wecomcs_msg_send_fail: "WeChat сообщил об ошибке отправки с ID сообщения и типом ошибки.",
wecomcs_servicer_status_change: "Оператор начал, остановил или приостановил приём обращений.",
+4
View File
@@ -986,6 +986,8 @@ const thTH = {
eventWildcard: 'ทุกเหตุการณ์',
eventNamespaceWildcard: '{{namespace}}.*',
eventNames: {
wecombot_enter_chat: "ผู้ใช้เข้าสู่แชตบอต",
wecombot_template_card_event: "การโต้ตอบกับการ์ด",
wecomcs_enter_session: "ลูกค้าเข้าสู่การสนทนา",
wecomcs_msg_send_fail: "ส่งข้อความบริการลูกค้าไม่สำเร็จ",
wecomcs_servicer_status_change: "สถานะเจ้าหน้าที่เปลี่ยนแปลง",
@@ -1008,6 +1010,8 @@ const thTH = {
platform_specific: 'เหตุการณ์เฉพาะแพลตฟอร์ม',
},
eventDescriptions: {
wecombot_enter_chat: "ผู้ใช้เปิดแชตบอต ใช้สำหรับขั้นตอนต้อนรับและแนะนำ",
wecombot_template_card_event: "ผู้ใช้คลิกหรือส่งการ์ด การตอบกลับแบบฟอร์มที่ระบบจัดการจะประมวลผลแยกกัน",
wecomcs_enter_session: "ลูกค้าเปิดลิงก์บริการ พร้อมข้อมูลแหล่งที่มาและรหัสต้อนรับ",
wecomcs_msg_send_fail: "WeChat แจ้งการส่งล้มเหลว พร้อม ID ข้อความและประเภทข้อผิดพลาด",
wecomcs_servicer_status_change: "เจ้าหน้าที่เริ่ม หยุด หรือพักการให้บริการ",
+4
View File
@@ -1003,6 +1003,8 @@ const viVN = {
eventWildcard: 'Tất cả sự kiện',
eventNamespaceWildcard: '{{namespace}}.*',
eventNames: {
wecombot_enter_chat: "Người dùng vào cuộc trò chuyện với bot",
wecombot_template_card_event: "Tương tác thẻ mẫu",
wecomcs_enter_session: "Khách hàng vào hội thoại",
wecomcs_msg_send_fail: "Gửi tin nhắn hỗ trợ thất bại",
wecomcs_servicer_status_change: "Trạng thái nhân viên thay đổi",
@@ -1025,6 +1027,8 @@ const viVN = {
platform_specific: 'Sự kiện riêng của nền tảng',
},
eventDescriptions: {
wecombot_enter_chat: "Người dùng mở cuộc trò chuyện với bot. Dùng cho quy trình chào mừng.",
wecombot_template_card_event: "Người dùng nhấp hoặc gửi thẻ. Phản hồi biểu mẫu do hệ thống quản lý được xử lý riêng.",
wecomcs_enter_session: "Khách hàng mở liên kết hỗ trợ; gồm nguồn truy cập và mã chào mừng.",
wecomcs_msg_send_fail: "WeChat thông báo gửi thất bại, gồm ID tin nhắn và loại lỗi.",
wecomcs_servicer_status_change: "Nhân viên bắt đầu, dừng hoặc tạm dừng tiếp nhận.",
+4
View File
@@ -946,6 +946,8 @@ const zhHans = {
eventWildcard: '全部事件',
eventNamespaceWildcard: '{{namespace}}.*',
eventNames: {
wecombot_enter_chat: "用户进入机器人会话",
wecombot_template_card_event: "模板卡片交互",
wecomcs_enter_session: "客户进入会话",
wecomcs_msg_send_fail: "客服消息发送失败",
wecomcs_servicer_status_change: "接待人员状态变更",
@@ -968,6 +970,8 @@ const zhHans = {
platform_specific: '平台特定事件',
},
eventDescriptions: {
wecombot_enter_chat: "用户进入机器人会话,可用于欢迎和引导流程。",
wecombot_template_card_event: "用户点击或提交模板卡片;已有交互表单的回调单独处理。",
wecomcs_enter_session: "客户通过客服链接进入会话,可获取来源和欢迎语凭据。",
wecomcs_msg_send_fail: "微信通知客服消息发送失败,包含消息 ID 和失败类型。",
wecomcs_servicer_status_change: "接待人员开始、停止接待或暂时挂起。",
+4
View File
@@ -945,6 +945,8 @@ const zhHant = {
eventWildcard: '全部事件',
eventNamespaceWildcard: '{{namespace}}.*',
eventNames: {
wecombot_enter_chat: "使用者進入機器人會話",
wecombot_template_card_event: "範本卡片互動",
wecomcs_enter_session: "客戶進入會話",
wecomcs_msg_send_fail: "客服訊息傳送失敗",
wecomcs_servicer_status_change: "接待人員狀態變更",
@@ -967,6 +969,8 @@ const zhHant = {
platform_specific: '平台特定事件',
},
eventDescriptions: {
wecombot_enter_chat: "使用者進入機器人會話,可用於歡迎和引導流程。",
wecombot_template_card_event: "使用者點擊或提交範本卡片;已有互動表單的回呼單獨處理。",
wecomcs_enter_session: "客戶透過客服連結進入會話,可取得來源和歡迎語憑證。",
wecomcs_msg_send_fail: "微信通知客服訊息傳送失敗,包含訊息 ID 和失敗類型。",
wecomcs_servicer_status_change: "接待人員開始、停止接待或暫時掛起。",