feat(agent-debug): edit event data with compact forms

This commit is contained in:
RockChinQ
2026-09-06 01:57:47 +08:00
parent 2a46325232
commit 5d0657b1db
14 changed files with 916 additions and 245 deletions
@@ -1,4 +1,11 @@
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import {
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { import {
@@ -40,6 +47,13 @@ import {
} from '@/app/home/components/event-patterns/event-pattern-groups'; } from '@/app/home/components/event-patterns/event-pattern-groups';
import EventSelectOptionContent from '@/app/home/components/event-patterns/EventSelectOptionContent'; import EventSelectOptionContent from '@/app/home/components/event-patterns/EventSelectOptionContent';
import AgentExecutionTrace from './AgentExecutionTrace'; import AgentExecutionTrace from './AgentExecutionTrace';
import AgentEventDataEditor from './AgentEventDataEditor';
import {
createDebugEventData,
debugEventInputText,
invalidDebugEventField,
parseDebugEventData,
} from './debug-event-data';
import { executionSteps, type DebugExecutionEvent } from './debug-execution'; import { executionSteps, type DebugExecutionEvent } from './debug-execution';
interface AgentDebugPanelProps { interface AgentDebugPanelProps {
@@ -63,104 +77,6 @@ interface DebugEntry {
finished?: boolean; finished?: boolean;
} }
const EVENT_PRESET_DATA: Record<
string,
{ text: string; data: Record<string, unknown> }
> = {
'message.received': {
text: '',
data: {},
},
'group.member_joined': {
text: 'A new member joined the group.',
data: {
group_id: 'debug-group',
member_id: 'debug-user',
member_name: 'Debug User',
},
},
'group.member_left': {
text: 'A member left the group.',
data: {
group_id: 'debug-group',
member_id: 'debug-user',
member_name: 'Debug User',
},
},
'friend.request_received': {
text: 'A user sent a friend request.',
data: {
requester_id: 'debug-user',
requester_name: 'Debug User',
request_id: 'debug-friend-request',
message: 'Hello',
},
},
'feedback.received': {
text: 'The user submitted feedback.',
data: {
rating: 5,
content: 'Debug feedback',
},
},
'friend.added': {
text: 'A friend was added.',
data: { user_id: 'debug-user', user_name: 'Debug User' },
},
'group.member_banned': {
text: 'A member was banned.',
data: {
group_id: 'debug-group',
member_id: 'debug-user',
member_name: 'Debug User',
},
},
'bot.invited_to_group': {
text: 'The bot was invited to a group.',
data: {
group_id: 'debug-group',
request_id: 'debug-group-request',
requester_id: 'debug-user',
},
},
'bot.muted': {
text: 'The bot was muted.',
data: { group_id: 'debug-group', duration: 60 },
},
'bot.unmuted': {
text: 'The bot was unmuted.',
data: { group_id: 'debug-group' },
},
'bot.removed_from_group': {
text: 'The bot was removed from the group.',
data: { group_id: 'debug-group' },
},
'message.edited': {
text: 'A message was edited.',
data: {
group_id: 'debug-group',
message_id: 'debug-message',
text: 'Edited message',
},
},
'message.deleted': {
text: 'A message was deleted.',
data: { group_id: 'debug-group', message_id: 'debug-message' },
},
'message.reaction': {
text: 'A reaction was added.',
data: {
group_id: 'debug-group',
message_id: 'debug-message',
reaction: '👍',
},
},
'platform.specific': {
text: 'A platform-specific event occurred.',
data: { event_name: 'debug-platform-event' },
},
};
function createDebugSessionId(agentId: string) { function createDebugSessionId(agentId: string) {
const nonce = globalThis.crypto?.randomUUID?.() ?? String(Date.now()); const nonce = globalThis.crypto?.randomUUID?.() ?? String(Date.now());
return `webui:${agentId}:${nonce}`; return `webui:${agentId}:${nonce}`;
@@ -186,8 +102,22 @@ export default function AgentDebugPanel({
); );
const [preset, setPreset] = useState('message.received'); const [preset, setPreset] = useState('message.received');
const [customEventType, setCustomEventType] = useState('custom.event'); const [customEventType, setCustomEventType] = useState('custom.event');
const [inputText, setInputText] = useState(''); const newEventData = useCallback(
const [eventDataText, setEventDataText] = useState('{}'); (type: string) =>
JSON.stringify(
createDebugEventData(type, {
user: t('agents.debugData.sampleUser'),
message: t('agents.debugData.sampleMessage'),
feedback: t('agents.debugData.sampleFeedback'),
}),
null,
2,
),
[t],
);
const [eventDataText, setEventDataText] = useState(() =>
newEventData('message.received'),
);
const [mockOptionsText, setMockOptionsText] = useState('{}'); const [mockOptionsText, setMockOptionsText] = useState('{}');
const [running, setRunning] = useState(false); const [running, setRunning] = useState(false);
const [entries, setEntries] = useState<DebugEntry[]>([]); const [entries, setEntries] = useState<DebugEntry[]>([]);
@@ -205,11 +135,7 @@ export default function AgentDebugPanel({
}, [entries]); }, [entries]);
const eventType = preset === 'custom' ? customEventType.trim() : preset; const eventType = preset === 'custom' ? customEventType.trim() : preset;
const isMessageEvent = eventType.startsWith('message.'); const eventDataValid = parseDebugEventData(eventDataText) !== null;
const supportedLabel = useMemo(
() => supportedEventPatterns.join(', '),
[supportedEventPatterns],
);
const availableEvents = useMemo(() => { const availableEvents = useMemo(() => {
const concretePatterns = supportedEventPatterns.filter( const concretePatterns = supportedEventPatterns.filter(
(pattern) => pattern !== '*' && !pattern.endsWith('.*'), (pattern) => pattern !== '*' && !pattern.endsWith('.*'),
@@ -230,6 +156,14 @@ export default function AgentDebugPanel({
(pattern) => pattern === '*' || pattern.endsWith('.*'), (pattern) => pattern === '*' || pattern.endsWith('.*'),
); );
const selectPreset = useCallback(
(value: string) => {
setPreset(value);
setEventDataText(newEventData(value));
},
[newEventData],
);
useEffect(() => { useEffect(() => {
if ( if (
availableEvents.includes(preset) || availableEvents.includes(preset) ||
@@ -238,24 +172,13 @@ export default function AgentDebugPanel({
return; return;
} }
selectPreset(availableEvents[0] ?? 'custom'); selectPreset(availableEvents[0] ?? 'custom');
}, [availableEvents, preset, supportsCustomEvent]); }, [availableEvents, preset, supportsCustomEvent, selectPreset]);
function selectPreset(value: string) {
setPreset(value);
const nextPreset = EVENT_PRESET_DATA[value] ?? { text: '', data: {} };
setInputText(nextPreset.text);
setEventDataText(JSON.stringify(nextPreset.data, null, 2));
}
async function runDebugEvent() { async function runDebugEvent() {
if (!eventType) { if (!eventType) {
toast.error(t('agents.debugEventTypeRequired')); toast.error(t('agents.debugEventTypeRequired'));
return; return;
} }
if (isMessageEvent && !inputText.trim()) {
toast.error(t('agents.debugInputRequired'));
return;
}
if ( if (
!supportedEventPatterns.some((pattern) => !supportedEventPatterns.some((pattern) =>
matchesEventPattern(pattern, eventType), matchesEventPattern(pattern, eventType),
@@ -265,18 +188,22 @@ export default function AgentDebugPanel({
return; return;
} }
let eventData: Record<string, unknown>; const eventData = parseDebugEventData(eventDataText);
let mockOptions: Record<string, unknown>; if (!eventData) {
try {
const parsed = JSON.parse(eventDataText || '{}');
if (!parsed || Array.isArray(parsed) || typeof parsed !== 'object') {
throw new Error('payload must be an object');
}
eventData = parsed as Record<string, unknown>;
} catch {
toast.error(t('agents.debugInvalidPayload')); toast.error(t('agents.debugInvalidPayload'));
return; return;
} }
const invalidField = invalidDebugEventField(eventType, eventData);
if (invalidField) {
toast.error(
t('agents.debugData.invalidField', {
field: t(`agents.debugData.${invalidField.label}`),
}),
);
return;
}
const inputText = debugEventInputText(eventType, eventData);
let mockOptions: Record<string, unknown>;
try { try {
const parsed = JSON.parse(mockOptionsText || '{}'); const parsed = JSON.parse(mockOptionsText || '{}');
if (!parsed || Array.isArray(parsed) || typeof parsed !== 'object') if (!parsed || Array.isArray(parsed) || typeof parsed !== 'object')
@@ -367,8 +294,6 @@ export default function AgentDebugPanel({
}, },
], ],
); );
if (isMessageEvent)
setInputText((current) => (current === inputText ? '' : current));
} catch (error) { } catch (error) {
if (controller.signal.aborted) { if (controller.signal.aborted) {
if (controller.signal.reason === 'user') { if (controller.signal.reason === 'user') {
@@ -541,131 +466,115 @@ export default function AgentDebugPanel({
)} )}
</div> </div>
<div className="shrink-0 space-y-3 border-t p-3"> <div className="flex max-h-[60%] min-h-0 shrink-0 flex-col border-t">
{supportedEventPatterns.length === 0 ? ( <div className="min-h-0 space-y-3 overflow-y-auto p-3">
<Alert className="bg-amber-500/5 text-amber-800 dark:text-amber-200"> {supportedEventPatterns.length === 0 ? (
<AlertTriangle className="size-4" /> <Alert className="bg-amber-500/5 text-amber-800 dark:text-amber-200">
<AlertTitle>{t('agents.debugNoEventsTitle')}</AlertTitle> <AlertTriangle className="size-4" />
<AlertDescription> <AlertTitle>{t('agents.debugNoEventsTitle')}</AlertTitle>
{t('agents.debugNoEventsDescription')} <AlertDescription>
</AlertDescription> {t('agents.debugNoEventsDescription')}
</Alert> </AlertDescription>
) : ( </Alert>
<> ) : (
<div className="space-y-1.5"> <>
<Label>{t('agents.debugEventType')}</Label> <div className="space-y-1.5">
<Select value={preset} onValueChange={selectPreset}> <Label>{t('agents.debugEventType')}</Label>
<SelectTrigger <Select value={preset} onValueChange={selectPreset}>
className="w-full" <SelectTrigger
aria-label={t('agents.debugEventType')} className="w-full"
> aria-label={t('agents.debugEventType')}
<SelectValue /> >
</SelectTrigger> <SelectValue />
<SelectContent className="w-[var(--radix-select-trigger-width)] max-w-[calc(100vw-2rem)]"> </SelectTrigger>
{eventGroups.map((group) => ( <SelectContent className="w-[var(--radix-select-trigger-width)] max-w-[calc(100vw-2rem)]">
<SelectGroup key={group.namespace}> {eventGroups.map((group) => (
<SelectLabel> <SelectGroup key={group.namespace}>
{eventGroupLabel(group.namespace, t)} <SelectLabel>
</SelectLabel> {eventGroupLabel(group.namespace, t)}
{group.patterns.map((event) => ( </SelectLabel>
{group.patterns.map((event) => (
<SelectItem
key={event}
value={event}
description={eventPatternDescription(event, t)}
className="py-2"
>
<EventSelectOptionContent
event={event}
label={eventPatternLabel(event, t)}
/>
</SelectItem>
))}
</SelectGroup>
))}
{supportsCustomEvent && (
<SelectGroup>
<SelectLabel>
{t('agents.debugCustomEvent')}
</SelectLabel>
<SelectItem <SelectItem
key={event} value="custom"
value={event} description={t('bots.eventDescriptions.custom')}
description={eventPatternDescription(event, t)}
className="py-2" className="py-2"
> >
<EventSelectOptionContent <EventSelectOptionContent
event={event} event="custom.event"
label={eventPatternLabel(event, t)} label={t('agents.debugCustomEvent')}
/> />
</SelectItem> </SelectItem>
))} </SelectGroup>
</SelectGroup> )}
))} </SelectContent>
{supportsCustomEvent && ( </Select>
<SelectGroup>
<SelectLabel>{t('agents.debugCustomEvent')}</SelectLabel>
<SelectItem
value="custom"
description={t('bots.eventDescriptions.custom')}
className="py-2"
>
<EventSelectOptionContent
event="custom.event"
label={t('agents.debugCustomEvent')}
/>
</SelectItem>
</SelectGroup>
)}
</SelectContent>
</Select>
</div>
{preset === 'custom' && (
<div className="space-y-1.5">
<Label htmlFor="agent-debug-custom-event">
{t('agents.debugCustomEventType')}
</Label>
<Input
id="agent-debug-custom-event"
value={customEventType}
onChange={(event) => setCustomEventType(event.target.value)}
placeholder="custom.event"
/>
</div> </div>
)}
<div className="space-y-1.5"> {preset === 'custom' && (
<Label htmlFor="agent-debug-input"> <div className="space-y-1.5">
{isMessageEvent <Label htmlFor="agent-debug-custom-event">
? t('agents.debugMessageInput') {t('agents.debugCustomEventType')}
: t('agents.debugEventSummary')} </Label>
</Label> <Input
<Textarea id="agent-debug-custom-event"
id="agent-debug-input" value={customEventType}
value={inputText} onChange={(event) => setCustomEventType(event.target.value)}
onChange={(event) => setInputText(event.target.value)} placeholder="custom.event"
className="min-h-20 resize-y" />
placeholder={t('agents.debugInputPlaceholder')} </div>
)}
<AgentEventDataEditor
key={eventType}
eventType={eventType}
custom={preset === 'custom'}
value={eventDataText}
onChange={setEventDataText}
/> />
</div>
<details className="rounded-md border bg-muted/20 px-3 py-2"> <details className="text-muted-foreground">
<summary className="cursor-pointer text-xs font-medium"> <summary className="cursor-pointer text-xs font-medium">
{t('agents.debugEventPayload')} {t('agents.debugMockOptions')}
</summary> </summary>
<div className="mt-2 space-y-2"> <p className="my-2 text-xs text-muted-foreground">
<p className="text-xs text-muted-foreground"> {t('agents.debugMockOptionsHelp')}
{t('agents.debugSupportedEvents')}: {supportedLabel}
</p> </p>
<Textarea <Textarea
id="agent-debug-payload" aria-label={t('agents.debugMockOptions')}
value={eventDataText} value={mockOptionsText}
onChange={(event) => setEventDataText(event.target.value)} onChange={(event) => setMockOptionsText(event.target.value)}
className="min-h-28 resize-y font-mono text-xs" className="min-h-24 font-mono text-xs"
spellCheck={false} spellCheck={false}
/> />
</div> </details>
</details> </>
)}
<details className="rounded-md border bg-muted/20 px-3 py-2"> </div>
<summary className="cursor-pointer text-xs font-medium"> {supportedEventPatterns.length > 0 && (
{t('agents.debugMockOptions')} <div className="shrink-0 px-3 pb-3">
</summary>
<p className="my-2 text-xs text-muted-foreground">
{t('agents.debugMockOptionsHelp')}
</p>
<Textarea
aria-label={t('agents.debugMockOptions')}
value={mockOptionsText}
onChange={(event) => setMockOptionsText(event.target.value)}
className="min-h-24 font-mono text-xs"
spellCheck={false}
/>
</details>
<Button <Button
type="button" type="button"
className="w-full" className="w-full"
disabled={!running && !eventDataValid}
onClick={() => onClick={() =>
running ? requestRef.current?.abort('user') : runDebugEvent() running ? requestRef.current?.abort('user') : runDebugEvent()
} }
@@ -681,7 +590,7 @@ export default function AgentDebugPanel({
? t('agents.debugSaveAndRun') ? t('agents.debugSaveAndRun')
: t('agents.debugRun')} : t('agents.debugRun')}
</Button> </Button>
</> </div>
)} )}
</div> </div>
</div> </div>
@@ -0,0 +1,129 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ArrowLeftRight } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { debugEventDefinition, parseDebugEventData } from './debug-event-data';
export default function AgentEventDataEditor({
eventType,
value,
onChange,
custom = false,
}: {
eventType: string;
custom?: boolean;
value: string;
onChange: (value: string) => void;
}) {
const { t } = useTranslation();
const [showJson, setShowJson] = useState(false);
const fields = custom ? [] : (debugEventDefinition(eventType)?.fields ?? []);
const data = parseDebugEventData(value);
const jsonMode = showJson || !fields.length;
function updateField(key: string, next: string | number | undefined) {
if (data) onChange(JSON.stringify({ ...data, [key]: next }, null, 2));
}
return (
<div className="space-y-2">
<div className="flex items-center justify-between gap-2">
<span className="text-xs font-medium">
{t('agents.debugData.title')}
</span>
{fields.length > 0 && (
<Button
type="button"
variant="ghost"
size="sm"
className="h-6 gap-1.5 px-1.5 text-xs text-muted-foreground"
disabled={jsonMode && !data}
onClick={() => setShowJson(!showJson)}
>
<ArrowLeftRight className="size-3.5" aria-hidden="true" />
{t(jsonMode ? 'agents.debugData.form' : 'agents.debugData.json')}
</Button>
)}
</div>
{jsonMode ? (
<>
<Textarea
aria-label={t('agents.debugData.json')}
value={value}
onChange={(event) => onChange(event.target.value)}
rows={6}
className="min-h-28 font-mono text-xs"
spellCheck={false}
aria-invalid={!data}
/>
{!data && (
<p role="alert" className="text-xs text-destructive">
{t('agents.debugInvalidPayload')}
</p>
)}
</>
) : (
<div className="grid grid-cols-2 gap-x-3 gap-y-2">
{fields.map((field) => {
const id = `agent-debug-data-${field.key}`;
const current = data?.[field.key];
const text =
typeof current === 'string' || typeof current === 'number'
? current
: '';
return (
<div
key={field.key}
className={`min-w-0 space-y-1 ${field.multiline ? 'col-span-2' : ''}`}
>
<Label htmlFor={id} className="text-xs text-muted-foreground">
{t(`agents.debugData.${field.label}`)}
</Label>
{field.multiline ? (
<Textarea
id={id}
value={text}
rows={2}
required={field.required}
onChange={(event) =>
updateField(field.key, event.target.value)
}
className="min-h-16 resize-y text-sm"
/>
) : (
<Input
id={id}
value={text}
type={typeof field.value === 'number' ? 'number' : 'text'}
min={field.min}
max={field.max}
step={1}
className="h-8 text-sm"
placeholder={
field.placeholder
? t(`agents.debugData.${field.placeholder}`)
: undefined
}
onChange={(event) =>
updateField(
field.key,
typeof field.value === 'number'
? event.target.value === ''
? undefined
: Number(event.target.value)
: event.target.value,
)
}
/>
)}
</div>
);
})}
</div>
)}
</div>
);
}
@@ -0,0 +1,229 @@
export interface DebugEventField {
key: string;
label: string;
value: string | number;
sample?: 'user' | 'message' | 'feedback';
multiline?: boolean;
required?: boolean;
min?: number;
max?: number;
placeholder?: string;
}
interface DebugEventDefinition {
fields: DebugEventField[];
defaults?: Record<string, unknown>;
messageField?: string;
}
const group: DebugEventField = {
key: 'group_id',
label: 'groupId',
value: 'debug-group',
};
const member: DebugEventField = {
key: 'member_id',
label: 'memberId',
value: 'debug-user',
};
const memberName: DebugEventField = {
key: 'member_name',
label: 'memberName',
value: '',
sample: 'user',
};
const user: DebugEventField = {
key: 'user_id',
label: 'userId',
value: 'debug-user',
};
const userName: DebugEventField = {
key: 'user_name',
label: 'userName',
value: '',
sample: 'user',
};
const requester: DebugEventField = {
key: 'requester_id',
label: 'requesterId',
value: 'debug-user',
};
const messageId: DebugEventField = {
key: 'message_id',
label: 'messageId',
value: 'debug-message',
};
const duration: DebugEventField = {
key: 'duration',
label: 'duration',
value: 60,
min: 0,
};
const message: DebugEventField = {
key: 'text',
label: 'message',
value: '',
sample: 'message',
multiline: true,
required: true,
};
const membership: DebugEventDefinition = {
fields: [memberName, member, group],
};
const friendship: DebugEventDefinition = { fields: [userName, user] };
const deletion: DebugEventDefinition = { fields: [messageId, group] };
// The compact form projects the existing debug API data fields. Fields omitted
// from the form, including custom fields, remain editable in the same JSON data.
const DEBUG_EVENT_DEFINITIONS: Record<string, DebugEventDefinition> = {
'message.received': {
fields: [
message,
userName,
{ ...group, value: '', placeholder: 'privateChat' },
],
defaults: { user_id: 'debug-user' },
messageField: 'text',
},
'message.edited': {
fields: [{ ...message, label: 'newMessage' }, messageId, group],
messageField: 'text',
},
'message.deleted': deletion,
'message.recalled': deletion,
'message.reaction': {
fields: [
{ key: 'reaction', label: 'reaction', value: '👍' },
messageId,
group,
],
defaults: { is_add: true },
},
'group.member_joined': membership,
'group.member_left': membership,
'group.member_banned': { fields: [member, group, duration] },
'group.info_updated': {
fields: [
{ key: 'group_name', label: 'groupName', value: 'debug-group' },
group,
],
},
'friend.request_received': {
fields: [
{
key: 'message',
label: 'verificationMessage',
value: '',
sample: 'message',
multiline: true,
},
{
key: 'requester_name',
label: 'requesterName',
value: '',
sample: 'user',
},
requester,
],
defaults: { request_id: 'debug-friend-request' },
},
'friend.added': friendship,
'friend.removed': friendship,
'bot.invited_to_group': {
fields: [group, requester],
defaults: { request_id: 'debug-group-request' },
},
'bot.muted': { fields: [group, duration] },
'bot.unmuted': { fields: [group] },
'bot.removed_from_group': { fields: [group] },
'feedback.received': {
fields: [
{
key: 'content',
label: 'feedback',
value: '',
sample: 'feedback',
multiline: true,
},
{ key: 'rating', label: 'rating', value: 5, min: 1, max: 5 },
],
},
'platform.specific': {
fields: [
{ key: 'event_name', label: 'eventName', value: 'debug-platform-event' },
],
},
};
export function debugEventDefinition(
eventType: string,
): DebugEventDefinition | undefined {
return Object.hasOwn(DEBUG_EVENT_DEFINITIONS, eventType)
? DEBUG_EVENT_DEFINITIONS[eventType]
: undefined;
}
export function createDebugEventData(
eventType: string,
samples: Record<'user' | 'message' | 'feedback', string>,
) {
const definition = debugEventDefinition(eventType);
return {
...definition?.defaults,
...Object.fromEntries(
(definition?.fields ?? []).map((field) => [
field.key,
field.sample ? samples[field.sample] : field.value,
]),
),
};
}
export function parseDebugEventData(
text: string,
): Record<string, unknown> | null {
try {
const value = JSON.parse(text);
return value && !Array.isArray(value) && typeof value === 'object'
? value
: null;
} catch {
return null;
}
}
export function invalidDebugEventField(
eventType: string,
data: Record<string, unknown>,
) {
return debugEventDefinition(eventType)?.fields.find((field) => {
const value = data[field.key];
if (
value === undefined ||
value === null ||
(typeof value === 'string' && !value.trim())
)
return field.required;
if (typeof field.value === 'number') {
return (
typeof value !== 'number' ||
!Number.isFinite(value) ||
!Number.isInteger(value) ||
(field.min !== undefined && value < field.min) ||
(field.max !== undefined && value > field.max)
);
}
return (
typeof value !== 'string' &&
!(field.key.endsWith('_id') && typeof value === 'number')
);
});
}
export function debugEventInputText(
eventType: string,
data: Record<string, unknown>,
) {
const key = debugEventDefinition(eventType)?.messageField;
return key && typeof data[key] === 'string' ? data[key].trim() : '';
}
+27
View File
@@ -704,6 +704,33 @@ const enUS = {
}, },
}, },
agents: { agents: {
debugData: {
title: 'Event data',
form: 'Common fields',
json: 'Full JSON',
groupId: 'Group ID',
memberId: 'Member ID',
memberName: 'Member name',
userId: 'User ID',
userName: 'User name',
requesterId: 'Requester ID',
requesterName: 'Requester name',
messageId: 'Message ID',
duration: 'Duration (seconds)',
message: 'Message content',
newMessage: 'New message content',
verificationMessage: 'Verification message',
reaction: 'Reaction',
groupName: 'Group name',
feedback: 'Feedback',
rating: 'Rating',
eventName: 'Event name',
privateChat: 'Leave empty for private chat',
sampleUser: 'Test User',
sampleMessage: 'Hello',
sampleFeedback: 'Very helpful',
invalidField: 'Check {{field}}',
},
title: 'Processors', title: 'Processors',
description: 'Create reusable processors and use them in bot event routing', description: 'Create reusable processors and use them in bot event routing',
create: 'Create Processor', create: 'Create Processor',
+27
View File
@@ -506,6 +506,33 @@ const esES = {
}, },
}, },
agents: { agents: {
debugData: {
title: 'Datos del evento',
form: 'Campos comunes',
json: 'JSON completo',
groupId: 'ID del grupo',
memberId: 'ID del miembro',
memberName: 'Nombre del miembro',
userId: 'ID del usuario',
userName: 'Nombre del usuario',
requesterId: 'ID del solicitante',
requesterName: 'Nombre del solicitante',
messageId: 'ID del mensaje',
duration: 'Duración (segundos)',
message: 'Contenido del mensaje',
newMessage: 'Contenido editado',
verificationMessage: 'Mensaje de verificación',
reaction: 'Reacción',
groupName: 'Nombre del grupo',
feedback: 'Comentarios',
rating: 'Puntuación',
eventName: 'Nombre del evento',
privateChat: 'Vacío para chat privado',
sampleUser: 'Usuario de prueba',
sampleMessage: 'Hola',
sampleFeedback: 'Muy útil',
invalidField: 'Revisa {{field}}',
},
title: 'Procesadores', title: 'Procesadores',
description: description:
'Crea procesadores reutilizables y úsalos en el enrutamiento de eventos del bot', 'Crea procesadores reutilizables y úsalos en el enrutamiento de eventos del bot',
+27
View File
@@ -716,6 +716,33 @@ const jaJP = {
}, },
}, },
agents: { agents: {
debugData: {
title: 'イベントデータ',
form: '基本項目',
json: '完全な JSON',
groupId: 'グループ ID',
memberId: 'メンバー ID',
memberName: 'メンバー名',
userId: 'ユーザー ID',
userName: 'ユーザー名',
requesterId: '申請者 ID',
requesterName: '申請者名',
messageId: 'メッセージ ID',
duration: '時間(秒)',
message: 'メッセージ内容',
newMessage: '編集後の内容',
verificationMessage: '申請メッセージ',
reaction: 'リアクション',
groupName: 'グループ名',
feedback: 'フィードバック',
rating: '評価',
eventName: 'イベント名',
privateChat: '空欄で個別チャット',
sampleUser: 'テストユーザー',
sampleMessage: 'こんにちは',
sampleFeedback: 'とても役立ちました',
invalidField: '{{field}}を確認してください',
},
debugReasoning: '思考内容', debugReasoning: '思考内容',
debugTextOutput: 'テキスト出力', debugTextOutput: 'テキスト出力',
debugPlatformNotice: debugPlatformNotice:
+27
View File
@@ -503,6 +503,33 @@ const ruRU = {
}, },
}, },
agents: { agents: {
debugData: {
title: 'Данные события',
form: 'Основные поля',
json: 'Полный JSON',
groupId: 'ID группы',
memberId: 'ID участника',
memberName: 'Имя участника',
userId: 'ID пользователя',
userName: 'Имя пользователя',
requesterId: 'ID заявителя',
requesterName: 'Имя заявителя',
messageId: 'ID сообщения',
duration: 'Длительность (секунды)',
message: 'Текст сообщения',
newMessage: 'Новый текст',
verificationMessage: 'Текст заявки',
reaction: 'Реакция',
groupName: 'Название группы',
feedback: 'Отзыв',
rating: 'Оценка',
eventName: 'Название события',
privateChat: 'Пусто для личного чата',
sampleUser: 'Тестовый пользователь',
sampleMessage: 'Привет',
sampleFeedback: 'Очень полезно',
invalidField: 'Проверьте {{field}}',
},
title: 'Обработчики', title: 'Обработчики',
description: description:
'Создавайте переиспользуемые обработчики и используйте их в маршрутизации событий бота', 'Создавайте переиспользуемые обработчики и используйте их в маршрутизации событий бота',
+27
View File
@@ -490,6 +490,33 @@ const thTH = {
}, },
}, },
agents: { agents: {
debugData: {
title: 'ข้อมูลเหตุการณ์',
form: 'ฟิลด์ทั่วไป',
json: 'JSON ทั้งหมด',
groupId: 'ID กลุ่ม',
memberId: 'ID สมาชิก',
memberName: 'ชื่อสมาชิก',
userId: 'ID ผู้ใช้',
userName: 'ชื่อผู้ใช้',
requesterId: 'ID ผู้ส่งคำขอ',
requesterName: 'ชื่อผู้ส่งคำขอ',
messageId: 'ID ข้อความ',
duration: 'ระยะเวลา (วินาที)',
message: 'เนื้อหาข้อความ',
newMessage: 'เนื้อหาที่แก้ไข',
verificationMessage: 'ข้อความยืนยัน',
reaction: 'อีโมจิ',
groupName: 'ชื่อกลุ่ม',
feedback: 'ความคิดเห็น',
rating: 'คะแนน',
eventName: 'ชื่อเหตุการณ์',
privateChat: 'เว้นว่างสำหรับแชทส่วนตัว',
sampleUser: 'ผู้ใช้ทดสอบ',
sampleMessage: 'สวัสดี',
sampleFeedback: 'มีประโยชน์มาก',
invalidField: 'โปรดตรวจสอบ {{field}}',
},
title: 'ตัวประมวลผล', title: 'ตัวประมวลผล',
description: 'สร้างตัวประมวลผลที่ใช้ซ้ำได้และใช้ในเส้นทางเหตุการณ์ของบอท', description: 'สร้างตัวประมวลผลที่ใช้ซ้ำได้และใช้ในเส้นทางเหตุการณ์ของบอท',
create: 'สร้างตัวประมวลผล', create: 'สร้างตัวประมวลผล',
+27
View File
@@ -499,6 +499,33 @@ const viVN = {
}, },
}, },
agents: { agents: {
debugData: {
title: 'Dữ liệu sự kiện',
form: 'Trường thường dùng',
json: 'JSON đầy đủ',
groupId: 'ID nhóm',
memberId: 'ID thành viên',
memberName: 'Tên thành viên',
userId: 'ID người dùng',
userName: 'Tên người dùng',
requesterId: 'ID người yêu cầu',
requesterName: 'Tên người yêu cầu',
messageId: 'ID tin nhắn',
duration: 'Thời lượng (giây)',
message: 'Nội dung tin nhắn',
newMessage: 'Nội dung đã sửa',
verificationMessage: 'Tin nhắn xác minh',
reaction: 'Biểu cảm',
groupName: 'Tên nhóm',
feedback: 'Phản hồi',
rating: 'Đánh giá',
eventName: 'Tên sự kiện',
privateChat: 'Để trống cho trò chuyện riêng',
sampleUser: 'Người dùng thử',
sampleMessage: 'Xin chào',
sampleFeedback: 'Rất hữu ích',
invalidField: 'Kiểm tra {{field}}',
},
title: 'Bộ xử lý', title: 'Bộ xử lý',
description: description:
'Tạo bộ xử lý có thể tái sử dụng và dùng chúng trong định tuyến sự kiện của bot', 'Tạo bộ xử lý có thể tái sử dụng và dùng chúng trong định tuyến sự kiện của bot',
+27
View File
@@ -670,6 +670,33 @@ const zhHans = {
}, },
}, },
agents: { agents: {
debugData: {
title: '事件数据',
form: '常用字段',
json: '完整 JSON',
groupId: '群组 ID',
memberId: '成员 ID',
memberName: '成员昵称',
userId: '用户 ID',
userName: '用户昵称',
requesterId: '申请者 ID',
requesterName: '申请者昵称',
messageId: '消息 ID',
duration: '时长(秒)',
message: '消息内容',
newMessage: '修改后的内容',
verificationMessage: '验证消息',
reaction: '表情',
groupName: '群组名称',
feedback: '反馈内容',
rating: '评分',
eventName: '事件名称',
privateChat: '留空为私聊',
sampleUser: '测试用户',
sampleMessage: '你好',
sampleFeedback: '很有帮助',
invalidField: '请检查{{field}}',
},
title: '处理器', title: '处理器',
description: '创建可复用的处理器,并在机器人事件路由中使用', description: '创建可复用的处理器,并在机器人事件路由中使用',
create: '创建处理器', create: '创建处理器',
+27
View File
@@ -474,6 +474,33 @@ const zhHant = {
}, },
}, },
agents: { agents: {
debugData: {
title: '事件資料',
form: '常用欄位',
json: '完整 JSON',
groupId: '群組 ID',
memberId: '成員 ID',
memberName: '成員暱稱',
userId: '使用者 ID',
userName: '使用者暱稱',
requesterId: '申請者 ID',
requesterName: '申請者暱稱',
messageId: '訊息 ID',
duration: '時長(秒)',
message: '訊息內容',
newMessage: '修改後的內容',
verificationMessage: '驗證訊息',
reaction: '表情',
groupName: '群組名稱',
feedback: '回饋內容',
rating: '評分',
eventName: '事件名稱',
privateChat: '留空為私聊',
sampleUser: '測試使用者',
sampleMessage: '你好',
sampleFeedback: '很有幫助',
invalidField: '請檢查{{field}}',
},
title: '處理器', title: '處理器',
description: '建立可重用的處理器,並在機器人事件路由中使用', description: '建立可重用的處理器,並在機器人事件路由中使用',
create: '建立處理器', create: '建立處理器',
+92
View File
@@ -0,0 +1,92 @@
import { expect, test } from '@playwright/test';
import { installLangBotApiMocks } from './fixtures/langbot-api';
test('event data stays in sync across the compact form, JSON and the request', async ({
page,
}) => {
await installLangBotApiMocks(page, {
authenticated: true,
withAdapterEvents: true,
});
await page.route(
'**/api/v1/agents/agent-workbench/debug/stream',
async (route) => {
await route.fulfill({
contentType: 'application/x-ndjson',
body:
JSON.stringify({ kind: 'completed', data: { final_text: 'Done' } }) +
'\n',
});
},
);
await page.goto('/home/agents?id=agent-workbench');
const panel = page.getByRole('region', { name: 'Event Debug' });
await panel
.getByRole('textbox', { name: 'Message content', exact: true })
.fill('Hello Alice');
await panel.getByRole('button', { name: 'Full JSON' }).click();
const json = panel.getByRole('textbox', { name: 'Full JSON' });
expect(JSON.parse(await json.inputValue()).text).toBe('Hello Alice');
await json.fill('{');
await expect(panel.getByRole('button', { name: 'Run test' })).toBeDisabled();
await expect(panel.getByRole('alert').last()).toContainText('valid JSON');
await json.fill(
JSON.stringify({
text: 'Edited in JSON',
user_name: 'Alice',
extra: { keep: true },
}),
);
await panel.getByRole('button', { name: 'Common fields' }).click();
await expect(
panel.getByRole('textbox', { name: 'Message content', exact: true }),
).toHaveValue('Edited in JSON');
await panel.getByRole('textbox', { name: 'User name' }).fill('Bob');
await panel.getByRole('button', { name: 'Full JSON' }).click();
expect(JSON.parse(await json.inputValue())).toMatchObject({
user_name: 'Bob',
extra: { keep: true },
});
await panel.getByRole('combobox', { name: 'Event type' }).click();
await page
.getByRole('option')
.filter({ hasText: 'group.member_joined' })
.click();
await expect(
panel.getByRole('textbox', { name: 'Message content', exact: true }),
).toHaveCount(0);
await expect(
panel.getByRole('textbox', { name: 'Event summary' }),
).toHaveCount(0);
await panel.getByRole('textbox', { name: 'Member name' }).fill('Carol');
await panel.getByRole('textbox', { name: 'Member ID' }).fill('member-42');
await panel.getByRole('textbox', { name: 'Group ID' }).fill('group-42');
const request = page.waitForRequest(
(req) => req.method() === 'POST' && req.url().endsWith('/debug/stream'),
);
await panel.getByRole('button', { name: 'Run test' }).click();
expect((await request).postDataJSON()).toMatchObject({
event_type: 'group.member_joined',
text: '',
data: {
member_name: 'Carol',
member_id: 'member-42',
group_id: 'group-42',
},
});
await expect(panel.getByText('Done', { exact: true })).toBeVisible();
await page.setViewportSize({ width: 1280, height: 650 });
await panel.getByRole('button', { name: 'Full JSON' }).click();
await panel.locator('summary').filter({ hasText: 'Mock scenario' }).click();
await expect(
panel.getByRole('button', { name: 'Run test' }),
).toBeInViewport();
await panel.getByRole('combobox', { name: 'Event type' }).click();
await page.getByRole('option').filter({ hasText: 'Custom event' }).click();
await expect(panel.getByRole('textbox', { name: 'Full JSON' })).toBeVisible();
await expect(
panel.getByRole('button', { name: 'Common fields' }),
).toHaveCount(0);
});
@@ -36,7 +36,7 @@ test.describe('processor detail workbench', () => {
.boundingBox(); .boundingBox();
const eventPickerBox = await debugEventPicker.boundingBox(); const eventPickerBox = await debugEventPicker.boundingBox();
const conversationInputBox = await debugPanel const conversationInputBox = await debugPanel
.getByRole('textbox', { name: 'Conversation input' }) .getByRole('textbox', { name: 'Message content' })
.boundingBox(); .boundingBox();
expect(transcriptBox).not.toBeNull(); expect(transcriptBox).not.toBeNull();
expect(eventPickerBox).not.toBeNull(); expect(eventPickerBox).not.toBeNull();
@@ -204,9 +204,7 @@ test.describe('processor detail workbench', () => {
.fill('Updated before debugging'); .fill('Updated before debugging');
await basicInfoDialog.getByRole('button', { name: 'Save' }).click(); await basicInfoDialog.getByRole('button', { name: 'Save' }).click();
await expect(basicInfoDialog).toHaveCount(0); await expect(basicInfoDialog).toHaveCount(0);
await page await page.getByRole('textbox', { name: 'Message content' }).fill('Hello');
.getByRole('textbox', { name: 'Conversation input' })
.fill('Hello');
await page.getByRole('button', { name: 'Run test' }).click(); await page.getByRole('button', { name: 'Run test' }).click();
await expect(page.getByText('Mock Agent response')).toBeVisible(); await expect(page.getByText('Mock Agent response')).toBeVisible();
@@ -253,9 +251,7 @@ test.describe('processor detail workbench', () => {
); );
await page.goto('/home/agents?id=agent-workbench'); await page.goto('/home/agents?id=agent-workbench');
await page await page.getByRole('textbox', { name: 'Message content' }).fill('Hello');
.getByRole('textbox', { name: 'Conversation input' })
.fill('Hello');
await page.getByRole('button', { name: 'Run test' }).click(); await page.getByRole('button', { name: 'Run test' }).click();
await expect( await expect(
@@ -0,0 +1,100 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import test from 'node:test';
import ts from 'typescript';
const source = fs.readFileSync(
new URL(
'../../src/app/home/agents/components/debug-event-data.ts',
import.meta.url,
),
'utf8',
);
const module = { exports: {} };
new Function(
'exports',
ts.transpileModule(source, {
compilerOptions: {
module: ts.ModuleKind.CommonJS,
target: ts.ScriptTarget.ES2022,
},
}).outputText,
)(module.exports);
const {
createDebugEventData,
parseDebugEventData,
debugEventInputText,
invalidDebugEventField,
} = module.exports;
const samples = { user: '测试用户', message: '你好', feedback: '很有帮助' };
test('message text and common fields belong to the same event data', () => {
const data = createDebugEventData('message.received', samples);
data.text = 'A different message';
data.user_name = 'Alice';
data.custom = { nested: true };
const restored = parseDebugEventData(JSON.stringify(data));
assert.deepEqual(restored, data);
assert.equal(
debugEventInputText('message.received', restored),
'A different message',
);
assert.equal(restored.user_name, 'Alice');
assert.equal(createDebugEventData('message.received', samples).text, '你好');
});
test('non-message events and deleted messages do not require fabricated conversation text', () => {
for (const event of [
'group.member_left',
'friend.request_received',
'message.deleted',
'message.reaction',
]) {
const data = createDebugEventData(event, samples);
assert.equal(invalidDebugEventField(event, data), undefined);
assert.equal(debugEventInputText(event, data), '');
assert.equal(data.text, undefined);
}
});
test('edited message uses the edited content, not an event description', () => {
const data = createDebugEventData('message.edited', samples);
data.text = 'Corrected message';
assert.equal(
debugEventInputText('message.edited', data),
'Corrected message',
);
assert.equal(
invalidDebugEventField('message.edited', { ...data, text: ' ' }).key,
'text',
);
});
test('invalid JSON cannot silently become an empty event', () => {
for (const text of ['', '{', 'null', '[]', '"hello"', '12']) {
assert.equal(parseDebugEventData(text), null);
}
const data = { arbitrary: { list: [1, true] } };
assert.deepEqual(parseDebugEventData(JSON.stringify(data)), data);
assert.equal(invalidDebugEventField('custom.example', data), undefined);
assert.equal(invalidDebugEventField('constructor', data), undefined);
});
test('duration and rating remain numbers and reject invalid values', () => {
const data = createDebugEventData('bot.muted', samples);
assert.equal(data.duration, 60);
for (const duration of [-1, 0.5, '60']) {
assert.equal(
invalidDebugEventField('bot.muted', { ...data, duration }).key,
'duration',
);
}
assert.equal(
invalidDebugEventField('bot.muted', { ...data, duration: 0 }),
undefined,
);
assert.equal(
invalidDebugEventField('feedback.received', { rating: 6 }).key,
'rating',
);
});