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>(
null,
);
const [availableEventTypes, setAvailableEventTypes] = useState<string[]>([
'message.received',
]);
const [supportedEventPatterns, setSupportedEventPatterns] = useState<
string[]
>(['*']);
const agentFormRef = useRef<AgentFormHandle>(null);
useEffect(() => {
@@ -70,10 +76,25 @@ export default function AgentDetailContent({ id }: { id: string }) {
if (isCreateMode) return;
let cancelled = false;
setLoading(true);
httpClient
.getAgent(id)
.then((resp) => {
if (!cancelled) setAgent(resp.agent);
Promise.all([
httpClient.getAgent(id),
httpClient.getAdapters().catch(() => ({ adapters: [] })),
])
.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(() => {
if (!cancelled) setLoading(false);
@@ -177,6 +198,7 @@ export default function AgentDetailContent({ id }: { id: string }) {
<AgentFormComponent
ref={agentFormRef}
agentId={id}
availableEventTypes={availableEventTypes}
onFinish={(updatedAgent) => {
if (updatedAgent) {
setAgent((current) =>
@@ -188,6 +210,7 @@ export default function AgentDetailContent({ id }: { id: string }) {
onDirtyChange={setFormDirty}
onSavingChange={setFormSaving}
onRunnerStatusChange={setRunnerStatus}
onSupportedEventPatternsChange={setSupportedEventPatterns}
/>
</fieldset>
}
@@ -201,10 +224,8 @@ export default function AgentDetailContent({ id }: { id: string }) {
onOpenRunnerConfig={() =>
agentFormRef.current?.openSection('runner_config')
}
supportedEventPatterns={
agent.supported_event_patterns ??
agent.capability?.supported_event_patterns ?? ['*']
}
supportedEventPatterns={supportedEventPatterns}
availableEventTypes={availableEventTypes}
/>
) : undefined
}
@@ -17,7 +17,9 @@ import { Label } from '@/components/ui/label';
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
@@ -28,9 +30,16 @@ import {
CollapsibleContent,
CollapsibleTrigger,
} from '@/components/ui/collapsible';
import {
eventGroupLabel,
eventPatternDescription,
eventPatternLabel,
groupEventPatterns,
} from '@/app/home/components/event-patterns/event-pattern-groups';
interface AgentDebugPanelProps {
agentId: string;
availableEventTypes: string[];
supportedEventPatterns?: string[];
beforeRun?: () => Promise<boolean>;
hasUnsavedChanges?: boolean;
@@ -46,16 +55,15 @@ interface DebugEntry {
detail?: string;
}
const EVENT_PRESETS = [
{
value: 'message.received',
labelKey: 'agents.debugMessageReceived',
const EVENT_PRESET_DATA: Record<
string,
{ text: string; data: Record<string, unknown> }
> = {
'message.received': {
text: '',
data: {},
},
{
value: 'group.member.joined',
labelKey: 'agents.debugGroupMemberJoined',
'group.member_joined': {
text: 'A new member joined the group.',
data: {
group_id: 'debug-group',
@@ -63,9 +71,7 @@ const EVENT_PRESETS = [
member_name: 'Debug User',
},
},
{
value: 'group.member.left',
labelKey: 'agents.debugGroupMemberLeft',
'group.member_left': {
text: 'A member left the group.',
data: {
group_id: 'debug-group',
@@ -73,9 +79,7 @@ const EVENT_PRESETS = [
member_name: 'Debug User',
},
},
{
value: 'friend.requested',
labelKey: 'agents.debugFriendRequested',
'friend.request_received': {
text: 'A user sent a friend request.',
data: {
requester_id: 'debug-user',
@@ -83,22 +87,14 @@ const EVENT_PRESETS = [
message: 'Hello',
},
},
{
value: 'feedback.received',
labelKey: 'agents.debugFeedbackReceived',
'feedback.received': {
text: 'The user submitted feedback.',
data: {
rating: 5,
content: 'Debug feedback',
},
},
{
value: 'custom',
labelKey: 'agents.debugCustomEvent',
text: '',
data: {},
},
] as const;
};
function createDebugSessionId(agentId: string) {
const nonce = globalThis.crypto?.randomUUID?.() ?? String(Date.now());
@@ -112,6 +108,7 @@ function matchesEventPattern(pattern: string, eventType: string) {
export default function AgentDebugPanel({
agentId,
availableEventTypes,
supportedEventPatterns = ['*'],
beforeRun,
hasUnsavedChanges = false,
@@ -132,27 +129,39 @@ export default function AgentDebugPanel({
() => supportedEventPatterns.join(', '),
[supportedEventPatterns],
);
const availablePresets = useMemo(
() =>
EVENT_PRESETS.filter(
(item) =>
item.value === 'custom' ||
supportedEventPatterns.some((pattern) =>
matchesEventPattern(pattern, item.value),
),
),
[supportedEventPatterns],
const availableEvents = useMemo(() => {
const concretePatterns = supportedEventPatterns.filter(
(pattern) => pattern !== '*' && !pattern.endsWith('.*'),
);
return Array.from(new Set([...availableEventTypes, ...concretePatterns]))
.filter((candidate) =>
supportedEventPatterns.some((pattern) =>
matchesEventPattern(pattern, candidate),
),
)
.sort();
}, [availableEventTypes, supportedEventPatterns]);
const eventGroups = useMemo(
() => groupEventPatterns(availableEvents),
[availableEvents],
);
const supportsCustomEvent = supportedEventPatterns.some(
(pattern) => pattern === '*' || pattern.endsWith('.*'),
);
useEffect(() => {
if (availablePresets.some((item) => item.value === preset)) return;
selectPreset(availablePresets[0]?.value ?? 'custom');
}, [availablePresets, preset]);
if (
availableEvents.includes(preset) ||
(preset === 'custom' && supportsCustomEvent)
) {
return;
}
selectPreset(availableEvents[0] ?? 'custom');
}, [availableEvents, preset, supportsCustomEvent]);
function selectPreset(value: string) {
setPreset(value);
const nextPreset = EVENT_PRESETS.find((item) => item.value === value);
if (!nextPreset) return;
const nextPreset = EVENT_PRESET_DATA[value] ?? { text: '', data: {} };
setInputText(nextPreset.text);
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">
<Label>{t('agents.debugEventType')}</Label>
<Select value={preset} onValueChange={selectPreset}>
<SelectTrigger className="w-full">
<SelectTrigger
className="w-full"
aria-label={t('agents.debugEventType')}
>
<SelectValue />
</SelectTrigger>
<SelectContent>
{availablePresets.map((item) => (
<SelectItem key={item.value} value={item.value}>
{t(item.labelKey)}
</SelectItem>
<SelectContent className="w-[var(--radix-select-trigger-width)] max-w-[calc(100vw-2rem)]">
{eventGroups.map((group) => (
<SelectGroup key={group.namespace}>
<SelectLabel>
{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>
</Select>
</div>
@@ -20,6 +20,8 @@ import { cn } from '@/lib/utils';
import {
eventGroupLabel,
eventNamespaces,
eventPatternDescription,
eventPatternLabel,
groupEventPatterns,
} from '@/app/home/components/event-patterns/event-pattern-groups';
@@ -61,30 +63,6 @@ export default function AgentEventPatternPicker({
}, [events, selectedPatterns]);
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) {
if (pattern === '*') {
onChange(['*']);
@@ -127,7 +105,9 @@ export default function AgentEventPatternPicker({
variant="secondary"
className="max-w-full rounded-md font-normal"
>
<span className="truncate">{eventLabel(pattern)}</span>
<span className="truncate">
{eventPatternLabel(pattern, t)}
</span>
</Badge>
))}
{selectedPatterns.length > 3 && (
@@ -157,7 +137,7 @@ export default function AgentEventPatternPicker({
return (
<CommandItem
key={pattern}
value={`${eventLabel(pattern)} ${pattern}`}
value={`${eventPatternLabel(pattern, t)} ${pattern}`}
onSelect={() => togglePattern(pattern)}
className="items-start gap-2 py-2"
>
@@ -170,14 +150,14 @@ export default function AgentEventPatternPicker({
<span className="min-w-0 flex-1">
<span className="flex min-w-0 items-center gap-2">
<span className="truncate font-medium">
{eventLabel(pattern)}
{eventPatternLabel(pattern, t)}
</span>
<code className="shrink-0 text-[10px] text-muted-foreground">
{pattern}
</code>
</span>
<span className="mt-0.5 block text-xs text-muted-foreground">
{eventDescription(pattern)}
{eventPatternDescription(pattern, t)}
</span>
</span>
</CommandItem>
@@ -47,10 +47,12 @@ export interface AgentRunnerStatus {
interface AgentFormComponentProps {
agentId: string;
availableEventTypes: string[];
onFinish: (agent?: Partial<Agent>) => void;
onDirtyChange?: (dirty: boolean) => void;
onSavingChange?: (saving: boolean) => void;
onRunnerStatusChange?: (status: AgentRunnerStatus) => void;
onSupportedEventPatternsChange?: (patterns: string[]) => void;
}
export type AgentConfigSection = 'events' | 'runner' | 'runner_config';
@@ -98,10 +100,12 @@ function isRunnerFieldVisible(
function AgentFormComponent(
{
agentId,
availableEventTypes,
onFinish,
onDirtyChange,
onSavingChange,
onRunnerStatusChange,
onSupportedEventPatternsChange,
}: AgentFormComponentProps,
ref: ForwardedRef<AgentFormHandle>,
) {
@@ -112,9 +116,6 @@ function AgentFormComponent(
useState<ApiRespPluginSystemStatus | null>(null);
const [pluginStatusLoading, setPluginStatusLoading] = useState(true);
const [pluginStatusError, setPluginStatusError] = useState(false);
const [availableEventTypes, setAvailableEventTypes] = useState<string[]>([
'message.received',
]);
const [activeSection, setActiveSection] =
useState<AgentConfigSection>('runner');
const isSavingRef = useRef(false);
@@ -159,24 +160,17 @@ function AgentFormComponent(
onDirtyChange?.(hasUnsavedChanges);
}, [hasUnsavedChanges, onDirtyChange]);
const supportedEventPatterns = form.watch('supported_event_patterns');
useEffect(() => {
onSupportedEventPatternsChange?.(supportedEventPatterns);
}, [onSupportedEventPatternsChange, supportedEventPatterns]);
useEffect(() => {
let cancelled = false;
Promise.all([
httpClient.getAgentMetadata(),
httpClient.getAgent(agentId),
httpClient.getAdapters().catch(() => ({ adapters: [] })),
])
.then(([metadata, resp, adaptersResp]) => {
Promise.all([httpClient.getAgentMetadata(), httpClient.getAgent(agentId)])
.then(([metadata, resp]) => {
if (cancelled) return;
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 config = (agent.config ?? {}) as Record<string, any>;
const loadedValues: FormValues = {
@@ -55,3 +55,25 @@ export function eventGroupLabel(namespace: string, t: TFunction) {
const label = t(key);
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(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 sidebarInset = page.locator('[data-slot="sidebar-inset"]');
await expect(appShell).toHaveCSS('overflow', 'clip');
@@ -108,6 +123,25 @@ test.describe('processor detail workbench', () => {
).toBeVisible();
await expect(page.getByRole('group', { name: 'Messages' })).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 flow.getByRole('tab').nth(1).click();