fix(agents): align debug event selection

This commit is contained in:
RockChinQ
2026-08-26 14:06:09 +08:00
parent 47f5515fa9
commit 4787799cd8
6 changed files with 184 additions and 97 deletions
+29 -8
View File
@@ -49,6 +49,12 @@ export default function AgentDetailContent({ id }: { id: string }) {
const [runnerStatus, setRunnerStatus] = useState<AgentRunnerStatus | null>( const [runnerStatus, setRunnerStatus] = useState<AgentRunnerStatus | null>(
null, null,
); );
const [availableEventTypes, setAvailableEventTypes] = useState<string[]>([
'message.received',
]);
const [supportedEventPatterns, setSupportedEventPatterns] = useState<
string[]
>(['*']);
const agentFormRef = useRef<AgentFormHandle>(null); const agentFormRef = useRef<AgentFormHandle>(null);
useEffect(() => { useEffect(() => {
@@ -70,10 +76,25 @@ export default function AgentDetailContent({ id }: { id: string }) {
if (isCreateMode) return; if (isCreateMode) return;
let cancelled = false; let cancelled = false;
setLoading(true); setLoading(true);
httpClient Promise.all([
.getAgent(id) httpClient.getAgent(id),
.then((resp) => { httpClient.getAdapters().catch(() => ({ adapters: [] })),
if (!cancelled) setAgent(resp.agent); ])
.then(([resp, adaptersResp]) => {
if (cancelled) return;
const adapterEvents = adaptersResp.adapters.flatMap(
(adapter) => adapter.spec.supported_events ?? [],
);
setAvailableEventTypes(
adapterEvents.length > 0
? Array.from(new Set(adapterEvents)).sort()
: ['message.received'],
);
setSupportedEventPatterns(
resp.agent.supported_event_patterns ??
resp.agent.capability?.supported_event_patterns ?? ['*'],
);
setAgent(resp.agent);
}) })
.finally(() => { .finally(() => {
if (!cancelled) setLoading(false); if (!cancelled) setLoading(false);
@@ -177,6 +198,7 @@ export default function AgentDetailContent({ id }: { id: string }) {
<AgentFormComponent <AgentFormComponent
ref={agentFormRef} ref={agentFormRef}
agentId={id} agentId={id}
availableEventTypes={availableEventTypes}
onFinish={(updatedAgent) => { onFinish={(updatedAgent) => {
if (updatedAgent) { if (updatedAgent) {
setAgent((current) => setAgent((current) =>
@@ -188,6 +210,7 @@ export default function AgentDetailContent({ id }: { id: string }) {
onDirtyChange={setFormDirty} onDirtyChange={setFormDirty}
onSavingChange={setFormSaving} onSavingChange={setFormSaving}
onRunnerStatusChange={setRunnerStatus} onRunnerStatusChange={setRunnerStatus}
onSupportedEventPatternsChange={setSupportedEventPatterns}
/> />
</fieldset> </fieldset>
} }
@@ -201,10 +224,8 @@ export default function AgentDetailContent({ id }: { id: string }) {
onOpenRunnerConfig={() => onOpenRunnerConfig={() =>
agentFormRef.current?.openSection('runner_config') agentFormRef.current?.openSection('runner_config')
} }
supportedEventPatterns={ supportedEventPatterns={supportedEventPatterns}
agent.supported_event_patterns ?? availableEventTypes={availableEventTypes}
agent.capability?.supported_event_patterns ?? ['*']
}
/> />
) : undefined ) : undefined
} }
@@ -17,7 +17,9 @@ import { Label } from '@/components/ui/label';
import { import {
Select, Select,
SelectContent, SelectContent,
SelectGroup,
SelectItem, SelectItem,
SelectLabel,
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from '@/components/ui/select'; } from '@/components/ui/select';
@@ -28,9 +30,16 @@ import {
CollapsibleContent, CollapsibleContent,
CollapsibleTrigger, CollapsibleTrigger,
} from '@/components/ui/collapsible'; } from '@/components/ui/collapsible';
import {
eventGroupLabel,
eventPatternDescription,
eventPatternLabel,
groupEventPatterns,
} from '@/app/home/components/event-patterns/event-pattern-groups';
interface AgentDebugPanelProps { interface AgentDebugPanelProps {
agentId: string; agentId: string;
availableEventTypes: string[];
supportedEventPatterns?: string[]; supportedEventPatterns?: string[];
beforeRun?: () => Promise<boolean>; beforeRun?: () => Promise<boolean>;
hasUnsavedChanges?: boolean; hasUnsavedChanges?: boolean;
@@ -46,16 +55,15 @@ interface DebugEntry {
detail?: string; detail?: string;
} }
const EVENT_PRESETS = [ const EVENT_PRESET_DATA: Record<
{ string,
value: 'message.received', { text: string; data: Record<string, unknown> }
labelKey: 'agents.debugMessageReceived', > = {
'message.received': {
text: '', text: '',
data: {}, data: {},
}, },
{ 'group.member_joined': {
value: 'group.member.joined',
labelKey: 'agents.debugGroupMemberJoined',
text: 'A new member joined the group.', text: 'A new member joined the group.',
data: { data: {
group_id: 'debug-group', group_id: 'debug-group',
@@ -63,9 +71,7 @@ const EVENT_PRESETS = [
member_name: 'Debug User', member_name: 'Debug User',
}, },
}, },
{ 'group.member_left': {
value: 'group.member.left',
labelKey: 'agents.debugGroupMemberLeft',
text: 'A member left the group.', text: 'A member left the group.',
data: { data: {
group_id: 'debug-group', group_id: 'debug-group',
@@ -73,9 +79,7 @@ const EVENT_PRESETS = [
member_name: 'Debug User', member_name: 'Debug User',
}, },
}, },
{ 'friend.request_received': {
value: 'friend.requested',
labelKey: 'agents.debugFriendRequested',
text: 'A user sent a friend request.', text: 'A user sent a friend request.',
data: { data: {
requester_id: 'debug-user', requester_id: 'debug-user',
@@ -83,22 +87,14 @@ const EVENT_PRESETS = [
message: 'Hello', message: 'Hello',
}, },
}, },
{ 'feedback.received': {
value: 'feedback.received',
labelKey: 'agents.debugFeedbackReceived',
text: 'The user submitted feedback.', text: 'The user submitted feedback.',
data: { data: {
rating: 5, rating: 5,
content: 'Debug feedback', content: 'Debug feedback',
}, },
}, },
{ };
value: 'custom',
labelKey: 'agents.debugCustomEvent',
text: '',
data: {},
},
] as const;
function createDebugSessionId(agentId: string) { function createDebugSessionId(agentId: string) {
const nonce = globalThis.crypto?.randomUUID?.() ?? String(Date.now()); const nonce = globalThis.crypto?.randomUUID?.() ?? String(Date.now());
@@ -112,6 +108,7 @@ function matchesEventPattern(pattern: string, eventType: string) {
export default function AgentDebugPanel({ export default function AgentDebugPanel({
agentId, agentId,
availableEventTypes,
supportedEventPatterns = ['*'], supportedEventPatterns = ['*'],
beforeRun, beforeRun,
hasUnsavedChanges = false, hasUnsavedChanges = false,
@@ -132,27 +129,39 @@ export default function AgentDebugPanel({
() => supportedEventPatterns.join(', '), () => supportedEventPatterns.join(', '),
[supportedEventPatterns], [supportedEventPatterns],
); );
const availablePresets = useMemo( const availableEvents = useMemo(() => {
() => const concretePatterns = supportedEventPatterns.filter(
EVENT_PRESETS.filter( (pattern) => pattern !== '*' && !pattern.endsWith('.*'),
(item) => );
item.value === 'custom' || return Array.from(new Set([...availableEventTypes, ...concretePatterns]))
supportedEventPatterns.some((pattern) => .filter((candidate) =>
matchesEventPattern(pattern, item.value), supportedEventPatterns.some((pattern) =>
), matchesEventPattern(pattern, candidate),
), ),
[supportedEventPatterns], )
.sort();
}, [availableEventTypes, supportedEventPatterns]);
const eventGroups = useMemo(
() => groupEventPatterns(availableEvents),
[availableEvents],
);
const supportsCustomEvent = supportedEventPatterns.some(
(pattern) => pattern === '*' || pattern.endsWith('.*'),
); );
useEffect(() => { useEffect(() => {
if (availablePresets.some((item) => item.value === preset)) return; if (
selectPreset(availablePresets[0]?.value ?? 'custom'); availableEvents.includes(preset) ||
}, [availablePresets, preset]); (preset === 'custom' && supportsCustomEvent)
) {
return;
}
selectPreset(availableEvents[0] ?? 'custom');
}, [availableEvents, preset, supportsCustomEvent]);
function selectPreset(value: string) { function selectPreset(value: string) {
setPreset(value); setPreset(value);
const nextPreset = EVENT_PRESETS.find((item) => item.value === value); const nextPreset = EVENT_PRESET_DATA[value] ?? { text: '', data: {} };
if (!nextPreset) return;
setInputText(nextPreset.text); setInputText(nextPreset.text);
setEventDataText(JSON.stringify(nextPreset.data, null, 2)); setEventDataText(JSON.stringify(nextPreset.data, null, 2));
} }
@@ -275,15 +284,42 @@ export default function AgentDebugPanel({
<div className="min-w-0 flex-1 space-y-1.5"> <div className="min-w-0 flex-1 space-y-1.5">
<Label>{t('agents.debugEventType')}</Label> <Label>{t('agents.debugEventType')}</Label>
<Select value={preset} onValueChange={selectPreset}> <Select value={preset} onValueChange={selectPreset}>
<SelectTrigger className="w-full"> <SelectTrigger
className="w-full"
aria-label={t('agents.debugEventType')}
>
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent className="w-[var(--radix-select-trigger-width)] max-w-[calc(100vw-2rem)]">
{availablePresets.map((item) => ( {eventGroups.map((group) => (
<SelectItem key={item.value} value={item.value}> <SelectGroup key={group.namespace}>
{t(item.labelKey)} <SelectLabel>
</SelectItem> {eventGroupLabel(group.namespace, t)}
</SelectLabel>
{group.patterns.map((event) => (
<SelectItem
key={event}
value={event}
description={eventPatternDescription(event, t)}
className="py-2"
>
{eventPatternLabel(event, t)}
</SelectItem>
))}
</SelectGroup>
))} ))}
{supportsCustomEvent && (
<SelectGroup>
<SelectLabel>{t('agents.debugCustomEvent')}</SelectLabel>
<SelectItem
value="custom"
description={t('bots.eventDescriptions.custom')}
className="py-2"
>
{t('agents.debugCustomEvent')}
</SelectItem>
</SelectGroup>
)}
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
@@ -20,6 +20,8 @@ import { cn } from '@/lib/utils';
import { import {
eventGroupLabel, eventGroupLabel,
eventNamespaces, eventNamespaces,
eventPatternDescription,
eventPatternLabel,
groupEventPatterns, groupEventPatterns,
} from '@/app/home/components/event-patterns/event-pattern-groups'; } from '@/app/home/components/event-patterns/event-pattern-groups';
@@ -61,30 +63,6 @@ export default function AgentEventPatternPicker({
}, [events, selectedPatterns]); }, [events, selectedPatterns]);
const optionGroups = useMemo(() => groupEventPatterns(options), [options]); const optionGroups = useMemo(() => groupEventPatterns(options), [options]);
function eventLabel(pattern: string) {
if (pattern === '*') return t('bots.eventWildcard');
if (pattern.endsWith('.*')) {
return t('bots.eventNamespaceWildcard', {
namespace: pattern.replace('.*', ''),
});
}
const key = `bots.eventNames.${pattern.replace(/\./g, '_')}`;
const label = t(key);
return label === key ? pattern : label;
}
function eventDescription(pattern: string) {
if (pattern === '*') return t('bots.eventDescriptions.all');
if (pattern.endsWith('.*')) {
return t('bots.eventDescriptions.namespace');
}
const key = `bots.eventDescriptions.${pattern.replace(/\./g, '_')}`;
const description = t(key);
return description === key
? t('bots.eventDescriptions.custom')
: description;
}
function togglePattern(pattern: string) { function togglePattern(pattern: string) {
if (pattern === '*') { if (pattern === '*') {
onChange(['*']); onChange(['*']);
@@ -127,7 +105,9 @@ export default function AgentEventPatternPicker({
variant="secondary" variant="secondary"
className="max-w-full rounded-md font-normal" className="max-w-full rounded-md font-normal"
> >
<span className="truncate">{eventLabel(pattern)}</span> <span className="truncate">
{eventPatternLabel(pattern, t)}
</span>
</Badge> </Badge>
))} ))}
{selectedPatterns.length > 3 && ( {selectedPatterns.length > 3 && (
@@ -157,7 +137,7 @@ export default function AgentEventPatternPicker({
return ( return (
<CommandItem <CommandItem
key={pattern} key={pattern}
value={`${eventLabel(pattern)} ${pattern}`} value={`${eventPatternLabel(pattern, t)} ${pattern}`}
onSelect={() => togglePattern(pattern)} onSelect={() => togglePattern(pattern)}
className="items-start gap-2 py-2" className="items-start gap-2 py-2"
> >
@@ -170,14 +150,14 @@ export default function AgentEventPatternPicker({
<span className="min-w-0 flex-1"> <span className="min-w-0 flex-1">
<span className="flex min-w-0 items-center gap-2"> <span className="flex min-w-0 items-center gap-2">
<span className="truncate font-medium"> <span className="truncate font-medium">
{eventLabel(pattern)} {eventPatternLabel(pattern, t)}
</span> </span>
<code className="shrink-0 text-[10px] text-muted-foreground"> <code className="shrink-0 text-[10px] text-muted-foreground">
{pattern} {pattern}
</code> </code>
</span> </span>
<span className="mt-0.5 block text-xs text-muted-foreground"> <span className="mt-0.5 block text-xs text-muted-foreground">
{eventDescription(pattern)} {eventPatternDescription(pattern, t)}
</span> </span>
</span> </span>
</CommandItem> </CommandItem>
@@ -47,10 +47,12 @@ export interface AgentRunnerStatus {
interface AgentFormComponentProps { interface AgentFormComponentProps {
agentId: string; agentId: string;
availableEventTypes: string[];
onFinish: (agent?: Partial<Agent>) => void; onFinish: (agent?: Partial<Agent>) => void;
onDirtyChange?: (dirty: boolean) => void; onDirtyChange?: (dirty: boolean) => void;
onSavingChange?: (saving: boolean) => void; onSavingChange?: (saving: boolean) => void;
onRunnerStatusChange?: (status: AgentRunnerStatus) => void; onRunnerStatusChange?: (status: AgentRunnerStatus) => void;
onSupportedEventPatternsChange?: (patterns: string[]) => void;
} }
export type AgentConfigSection = 'events' | 'runner' | 'runner_config'; export type AgentConfigSection = 'events' | 'runner' | 'runner_config';
@@ -98,10 +100,12 @@ function isRunnerFieldVisible(
function AgentFormComponent( function AgentFormComponent(
{ {
agentId, agentId,
availableEventTypes,
onFinish, onFinish,
onDirtyChange, onDirtyChange,
onSavingChange, onSavingChange,
onRunnerStatusChange, onRunnerStatusChange,
onSupportedEventPatternsChange,
}: AgentFormComponentProps, }: AgentFormComponentProps,
ref: ForwardedRef<AgentFormHandle>, ref: ForwardedRef<AgentFormHandle>,
) { ) {
@@ -112,9 +116,6 @@ function AgentFormComponent(
useState<ApiRespPluginSystemStatus | null>(null); useState<ApiRespPluginSystemStatus | null>(null);
const [pluginStatusLoading, setPluginStatusLoading] = useState(true); const [pluginStatusLoading, setPluginStatusLoading] = useState(true);
const [pluginStatusError, setPluginStatusError] = useState(false); const [pluginStatusError, setPluginStatusError] = useState(false);
const [availableEventTypes, setAvailableEventTypes] = useState<string[]>([
'message.received',
]);
const [activeSection, setActiveSection] = const [activeSection, setActiveSection] =
useState<AgentConfigSection>('runner'); useState<AgentConfigSection>('runner');
const isSavingRef = useRef(false); const isSavingRef = useRef(false);
@@ -159,24 +160,17 @@ function AgentFormComponent(
onDirtyChange?.(hasUnsavedChanges); onDirtyChange?.(hasUnsavedChanges);
}, [hasUnsavedChanges, onDirtyChange]); }, [hasUnsavedChanges, onDirtyChange]);
const supportedEventPatterns = form.watch('supported_event_patterns');
useEffect(() => {
onSupportedEventPatternsChange?.(supportedEventPatterns);
}, [onSupportedEventPatternsChange, supportedEventPatterns]);
useEffect(() => { useEffect(() => {
let cancelled = false; let cancelled = false;
Promise.all([ Promise.all([httpClient.getAgentMetadata(), httpClient.getAgent(agentId)])
httpClient.getAgentMetadata(), .then(([metadata, resp]) => {
httpClient.getAgent(agentId),
httpClient.getAdapters().catch(() => ({ adapters: [] })),
])
.then(([metadata, resp, adaptersResp]) => {
if (cancelled) return; if (cancelled) return;
setRunnerConfigSchema(metadata.runner_config ?? null); setRunnerConfigSchema(metadata.runner_config ?? null);
const adapterEvents = adaptersResp.adapters.flatMap(
(adapter) => adapter.spec.supported_events ?? [],
);
setAvailableEventTypes(
adapterEvents.length > 0
? Array.from(new Set(adapterEvents)).sort()
: ['message.received'],
);
const agent = resp.agent; const agent = resp.agent;
const config = (agent.config ?? {}) as Record<string, any>; const config = (agent.config ?? {}) as Record<string, any>;
const loadedValues: FormValues = { const loadedValues: FormValues = {
@@ -55,3 +55,25 @@ export function eventGroupLabel(namespace: string, t: TFunction) {
const label = t(key); const label = t(key);
return label === key ? namespace : label; return label === key ? namespace : label;
} }
export function eventPatternLabel(pattern: string, t: TFunction) {
if (pattern === '*') return t('bots.eventWildcard');
if (pattern.endsWith('.*')) {
return t('bots.eventNamespaceWildcard', {
namespace: pattern.replace('.*', ''),
});
}
const key = `bots.eventNames.${pattern.replace(/\./g, '_')}`;
const label = t(key);
return label === key ? pattern : label;
}
export function eventPatternDescription(pattern: string, t: TFunction) {
if (pattern === '*') return t('bots.eventDescriptions.all');
if (pattern.endsWith('.*')) {
return t('bots.eventDescriptions.namespace');
}
const key = `bots.eventDescriptions.${pattern.replace(/\./g, '_')}`;
const description = t(key);
return description === key ? t('bots.eventDescriptions.custom') : description;
}
@@ -27,6 +27,21 @@ test.describe('processor detail workbench', () => {
expect(debugBox!.x).toBeLessThan(configBox!.x); expect(debugBox!.x).toBeLessThan(configBox!.x);
expect(configBox!.width).toBeGreaterThan(debugBox!.width); expect(configBox!.width).toBeGreaterThan(debugBox!.width);
const debugEventPicker = debugPanel.getByRole('combobox', {
name: 'Event type',
});
await debugEventPicker.click();
await expect(page.getByRole('group', { name: 'Messages' })).toBeVisible();
await expect(page.getByRole('group', { name: 'Groups' })).toBeVisible();
await expect(
page.getByRole('option').filter({ hasText: 'Member joined group' }),
).toContainText('A member joins a group where the bot is present.');
await expect(
page.getByRole('option').filter({ hasText: 'Message edited' }),
).toContainText('The platform reports that an existing message changed.');
await expect(page.getByRole('option')).toHaveCount(6);
await page.keyboard.press('Escape');
const appShell = page.locator('[class*="group/sidebar-wrapper"]'); const appShell = page.locator('[class*="group/sidebar-wrapper"]');
const sidebarInset = page.locator('[data-slot="sidebar-inset"]'); const sidebarInset = page.locator('[data-slot="sidebar-inset"]');
await expect(appShell).toHaveCSS('overflow', 'clip'); await expect(appShell).toHaveCSS('overflow', 'clip');
@@ -108,6 +123,25 @@ test.describe('processor detail workbench', () => {
).toBeVisible(); ).toBeVisible();
await expect(page.getByRole('group', { name: 'Messages' })).toHaveCount(1); await expect(page.getByRole('group', { name: 'Messages' })).toHaveCount(1);
await expect(page.getByRole('group', { name: 'Groups' })).toHaveCount(1); await expect(page.getByRole('group', { name: 'Groups' })).toHaveCount(1);
await page
.getByRole('option')
.filter({ hasText: 'message.*' })
.first()
.click();
await page.keyboard.press('Escape');
await debugEventPicker.click();
await expect(
page.getByRole('option').filter({ hasText: 'Message edited' }),
).toBeVisible();
await expect(
page.getByRole('option').filter({ hasText: 'Member joined group' }),
).toHaveCount(0);
await expect(page.getByRole('option')).toHaveCount(3);
await page.keyboard.press('Escape');
await eventPicker.click();
await page.getByRole('option').filter({ hasText: 'All events' }).click();
await page.keyboard.press('Escape'); await page.keyboard.press('Escape');
await flow.getByRole('tab').nth(1).click(); await flow.getByRole('tab').nth(1).click();