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 { toast } from 'sonner';
import {
@@ -40,6 +47,13 @@ import {
} from '@/app/home/components/event-patterns/event-pattern-groups';
import EventSelectOptionContent from '@/app/home/components/event-patterns/EventSelectOptionContent';
import AgentExecutionTrace from './AgentExecutionTrace';
import AgentEventDataEditor from './AgentEventDataEditor';
import {
createDebugEventData,
debugEventInputText,
invalidDebugEventField,
parseDebugEventData,
} from './debug-event-data';
import { executionSteps, type DebugExecutionEvent } from './debug-execution';
interface AgentDebugPanelProps {
@@ -63,104 +77,6 @@ interface DebugEntry {
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) {
const nonce = globalThis.crypto?.randomUUID?.() ?? String(Date.now());
return `webui:${agentId}:${nonce}`;
@@ -186,8 +102,22 @@ export default function AgentDebugPanel({
);
const [preset, setPreset] = useState('message.received');
const [customEventType, setCustomEventType] = useState('custom.event');
const [inputText, setInputText] = useState('');
const [eventDataText, setEventDataText] = useState('{}');
const newEventData = useCallback(
(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 [running, setRunning] = useState(false);
const [entries, setEntries] = useState<DebugEntry[]>([]);
@@ -205,11 +135,7 @@ export default function AgentDebugPanel({
}, [entries]);
const eventType = preset === 'custom' ? customEventType.trim() : preset;
const isMessageEvent = eventType.startsWith('message.');
const supportedLabel = useMemo(
() => supportedEventPatterns.join(', '),
[supportedEventPatterns],
);
const eventDataValid = parseDebugEventData(eventDataText) !== null;
const availableEvents = useMemo(() => {
const concretePatterns = supportedEventPatterns.filter(
(pattern) => pattern !== '*' && !pattern.endsWith('.*'),
@@ -230,6 +156,14 @@ export default function AgentDebugPanel({
(pattern) => pattern === '*' || pattern.endsWith('.*'),
);
const selectPreset = useCallback(
(value: string) => {
setPreset(value);
setEventDataText(newEventData(value));
},
[newEventData],
);
useEffect(() => {
if (
availableEvents.includes(preset) ||
@@ -238,24 +172,13 @@ export default function AgentDebugPanel({
return;
}
selectPreset(availableEvents[0] ?? 'custom');
}, [availableEvents, preset, supportsCustomEvent]);
function selectPreset(value: string) {
setPreset(value);
const nextPreset = EVENT_PRESET_DATA[value] ?? { text: '', data: {} };
setInputText(nextPreset.text);
setEventDataText(JSON.stringify(nextPreset.data, null, 2));
}
}, [availableEvents, preset, supportsCustomEvent, selectPreset]);
async function runDebugEvent() {
if (!eventType) {
toast.error(t('agents.debugEventTypeRequired'));
return;
}
if (isMessageEvent && !inputText.trim()) {
toast.error(t('agents.debugInputRequired'));
return;
}
if (
!supportedEventPatterns.some((pattern) =>
matchesEventPattern(pattern, eventType),
@@ -265,18 +188,22 @@ export default function AgentDebugPanel({
return;
}
let eventData: Record<string, unknown>;
let mockOptions: Record<string, unknown>;
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 {
const eventData = parseDebugEventData(eventDataText);
if (!eventData) {
toast.error(t('agents.debugInvalidPayload'));
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 {
const parsed = JSON.parse(mockOptionsText || '{}');
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) {
if (controller.signal.aborted) {
if (controller.signal.reason === 'user') {
@@ -541,131 +466,115 @@ export default function AgentDebugPanel({
)}
</div>
<div className="shrink-0 space-y-3 border-t p-3">
{supportedEventPatterns.length === 0 ? (
<Alert className="bg-amber-500/5 text-amber-800 dark:text-amber-200">
<AlertTriangle className="size-4" />
<AlertTitle>{t('agents.debugNoEventsTitle')}</AlertTitle>
<AlertDescription>
{t('agents.debugNoEventsDescription')}
</AlertDescription>
</Alert>
) : (
<>
<div className="space-y-1.5">
<Label>{t('agents.debugEventType')}</Label>
<Select value={preset} onValueChange={selectPreset}>
<SelectTrigger
className="w-full"
aria-label={t('agents.debugEventType')}
>
<SelectValue />
</SelectTrigger>
<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) => (
<div className="flex max-h-[60%] min-h-0 shrink-0 flex-col border-t">
<div className="min-h-0 space-y-3 overflow-y-auto p-3">
{supportedEventPatterns.length === 0 ? (
<Alert className="bg-amber-500/5 text-amber-800 dark:text-amber-200">
<AlertTriangle className="size-4" />
<AlertTitle>{t('agents.debugNoEventsTitle')}</AlertTitle>
<AlertDescription>
{t('agents.debugNoEventsDescription')}
</AlertDescription>
</Alert>
) : (
<>
<div className="space-y-1.5">
<Label>{t('agents.debugEventType')}</Label>
<Select value={preset} onValueChange={selectPreset}>
<SelectTrigger
className="w-full"
aria-label={t('agents.debugEventType')}
>
<SelectValue />
</SelectTrigger>
<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"
>
<EventSelectOptionContent
event={event}
label={eventPatternLabel(event, t)}
/>
</SelectItem>
))}
</SelectGroup>
))}
{supportsCustomEvent && (
<SelectGroup>
<SelectLabel>
{t('agents.debugCustomEvent')}
</SelectLabel>
<SelectItem
key={event}
value={event}
description={eventPatternDescription(event, t)}
value="custom"
description={t('bots.eventDescriptions.custom')}
className="py-2"
>
<EventSelectOptionContent
event={event}
label={eventPatternLabel(event, t)}
event="custom.event"
label={t('agents.debugCustomEvent')}
/>
</SelectItem>
))}
</SelectGroup>
))}
{supportsCustomEvent && (
<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"
/>
</SelectGroup>
)}
</SelectContent>
</Select>
</div>
)}
<div className="space-y-1.5">
<Label htmlFor="agent-debug-input">
{isMessageEvent
? t('agents.debugMessageInput')
: t('agents.debugEventSummary')}
</Label>
<Textarea
id="agent-debug-input"
value={inputText}
onChange={(event) => setInputText(event.target.value)}
className="min-h-20 resize-y"
placeholder={t('agents.debugInputPlaceholder')}
{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>
)}
<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">
<summary className="cursor-pointer text-xs font-medium">
{t('agents.debugEventPayload')}
</summary>
<div className="mt-2 space-y-2">
<p className="text-xs text-muted-foreground">
{t('agents.debugSupportedEvents')}: {supportedLabel}
<details className="text-muted-foreground">
<summary className="cursor-pointer text-xs font-medium">
{t('agents.debugMockOptions')}
</summary>
<p className="my-2 text-xs text-muted-foreground">
{t('agents.debugMockOptionsHelp')}
</p>
<Textarea
id="agent-debug-payload"
value={eventDataText}
onChange={(event) => setEventDataText(event.target.value)}
className="min-h-28 resize-y font-mono text-xs"
aria-label={t('agents.debugMockOptions')}
value={mockOptionsText}
onChange={(event) => setMockOptionsText(event.target.value)}
className="min-h-24 font-mono text-xs"
spellCheck={false}
/>
</div>
</details>
<details className="rounded-md border bg-muted/20 px-3 py-2">
<summary className="cursor-pointer text-xs font-medium">
{t('agents.debugMockOptions')}
</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>
</details>
</>
)}
</div>
{supportedEventPatterns.length > 0 && (
<div className="shrink-0 px-3 pb-3">
<Button
type="button"
className="w-full"
disabled={!running && !eventDataValid}
onClick={() =>
running ? requestRef.current?.abort('user') : runDebugEvent()
}
@@ -681,7 +590,7 @@ export default function AgentDebugPanel({
? t('agents.debugSaveAndRun')
: t('agents.debugRun')}
</Button>
</>
</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: {
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',
description: 'Create reusable processors and use them in bot event routing',
create: 'Create Processor',
+27
View File
@@ -506,6 +506,33 @@ const esES = {
},
},
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',
description:
'Crea procesadores reutilizables y úsalos en el enrutamiento de eventos del bot',
+27
View File
@@ -716,6 +716,33 @@ const jaJP = {
},
},
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: '思考内容',
debugTextOutput: 'テキスト出力',
debugPlatformNotice:
+27
View File
@@ -503,6 +503,33 @@ const ruRU = {
},
},
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: 'Обработчики',
description:
'Создавайте переиспользуемые обработчики и используйте их в маршрутизации событий бота',
+27
View File
@@ -490,6 +490,33 @@ const thTH = {
},
},
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: 'ตัวประมวลผล',
description: 'สร้างตัวประมวลผลที่ใช้ซ้ำได้และใช้ในเส้นทางเหตุการณ์ของบอท',
create: 'สร้างตัวประมวลผล',
+27
View File
@@ -499,6 +499,33 @@ const viVN = {
},
},
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ý',
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',
+27
View File
@@ -670,6 +670,33 @@ const zhHans = {
},
},
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: '处理器',
description: '创建可复用的处理器,并在机器人事件路由中使用',
create: '创建处理器',
+27
View File
@@ -474,6 +474,33 @@ const zhHant = {
},
},
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: '處理器',
description: '建立可重用的處理器,並在機器人事件路由中使用',
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();
const eventPickerBox = await debugEventPicker.boundingBox();
const conversationInputBox = await debugPanel
.getByRole('textbox', { name: 'Conversation input' })
.getByRole('textbox', { name: 'Message content' })
.boundingBox();
expect(transcriptBox).not.toBeNull();
expect(eventPickerBox).not.toBeNull();
@@ -204,9 +204,7 @@ test.describe('processor detail workbench', () => {
.fill('Updated before debugging');
await basicInfoDialog.getByRole('button', { name: 'Save' }).click();
await expect(basicInfoDialog).toHaveCount(0);
await page
.getByRole('textbox', { name: 'Conversation input' })
.fill('Hello');
await page.getByRole('textbox', { name: 'Message content' }).fill('Hello');
await page.getByRole('button', { name: 'Run test' }).click();
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
.getByRole('textbox', { name: 'Conversation input' })
.fill('Hello');
await page.getByRole('textbox', { name: 'Message content' }).fill('Hello');
await page.getByRole('button', { name: 'Run test' }).click();
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',
);
});