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 {