mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-16 14:57:15 +00:00
feat(processors): add explicitly bound plugin event processors
This commit is contained in:
@@ -23,6 +23,7 @@ import {
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import PipelineDetailContent from '@/app/home/pipelines/PipelineDetailContent';
|
||||
import EventProcessorDetailContent from './EventProcessorDetailContent';
|
||||
import AgentCreateContent from './components/AgentCreateContent';
|
||||
import AgentDebugPanel from './components/AgentDebugPanel';
|
||||
import AgentFormComponent, {
|
||||
@@ -167,87 +168,104 @@ export default function AgentDetailContent({ id }: { id: string }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProcessorDetailWorkbench
|
||||
key={id}
|
||||
title={`${agent.emoji || '🤖'} ${agent.name}`}
|
||||
titleBadge={
|
||||
supportedEventPatterns.length === 0 ? (
|
||||
<Badge
|
||||
variant="outline"
|
||||
role="status"
|
||||
className="shrink-0 gap-1 rounded-full border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-300"
|
||||
>
|
||||
<AlertTriangle className="size-3" />
|
||||
{t('agents.noEventsConfiguredBadge')}
|
||||
</Badge>
|
||||
) : undefined
|
||||
}
|
||||
titleAction={
|
||||
canManage ? (
|
||||
<EntityTitleEditButton onClick={() => setBasicInfoOpen(true)} />
|
||||
) : undefined
|
||||
}
|
||||
status={runnerStatus}
|
||||
saveLabel={t('common.save')}
|
||||
saveFormId="agent-form"
|
||||
canSave={canManage}
|
||||
isDirty={formDirty}
|
||||
isSaving={formSaving}
|
||||
headerActions={
|
||||
canManage ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
disabled={formSaving || deleting}
|
||||
onClick={() => setDeleteConfirmOpen(true)}
|
||||
>
|
||||
<Trash2 className="size-4" />
|
||||
{t('common.delete')}
|
||||
</Button>
|
||||
) : undefined
|
||||
}
|
||||
configTitle={t('pipelines.configuration')}
|
||||
configContent={
|
||||
<fieldset className="contents" disabled={!canManage}>
|
||||
<AgentFormComponent
|
||||
ref={agentFormRef}
|
||||
agentId={id}
|
||||
availableEventTypes={availableEventTypes}
|
||||
onFinish={(updatedAgent) => {
|
||||
if (updatedAgent) {
|
||||
setAgent((current) =>
|
||||
current ? { ...current, ...updatedAgent } : current,
|
||||
);
|
||||
{agent.kind === 'event_processor' ? (
|
||||
<EventProcessorDetailContent
|
||||
key={id}
|
||||
id={id}
|
||||
agent={agent}
|
||||
canManage={canManage}
|
||||
onDelete={() => setDeleteConfirmOpen(true)}
|
||||
onEdit={() => setBasicInfoOpen(true)}
|
||||
onSaved={() => {
|
||||
void httpClient
|
||||
.getAgent(id)
|
||||
.then((response) => setAgent(response.agent));
|
||||
void refreshPipelines();
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<ProcessorDetailWorkbench
|
||||
key={id}
|
||||
title={`${agent.emoji || '🤖'} ${agent.name}`}
|
||||
titleBadge={
|
||||
supportedEventPatterns.length === 0 ? (
|
||||
<Badge
|
||||
variant="outline"
|
||||
role="status"
|
||||
className="shrink-0 gap-1 rounded-full border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-300"
|
||||
>
|
||||
<AlertTriangle className="size-3" />
|
||||
{t('agents.noEventsConfiguredBadge')}
|
||||
</Badge>
|
||||
) : undefined
|
||||
}
|
||||
titleAction={
|
||||
canManage ? (
|
||||
<EntityTitleEditButton onClick={() => setBasicInfoOpen(true)} />
|
||||
) : undefined
|
||||
}
|
||||
status={runnerStatus}
|
||||
saveLabel={t('common.save')}
|
||||
saveFormId="agent-form"
|
||||
canSave={canManage}
|
||||
isDirty={formDirty}
|
||||
isSaving={formSaving}
|
||||
headerActions={
|
||||
canManage ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
disabled={formSaving || deleting}
|
||||
onClick={() => setDeleteConfirmOpen(true)}
|
||||
>
|
||||
<Trash2 className="size-4" />
|
||||
{t('common.delete')}
|
||||
</Button>
|
||||
) : undefined
|
||||
}
|
||||
configTitle={t('pipelines.configuration')}
|
||||
configContent={
|
||||
<fieldset className="contents" disabled={!canManage}>
|
||||
<AgentFormComponent
|
||||
ref={agentFormRef}
|
||||
agentId={id}
|
||||
availableEventTypes={availableEventTypes}
|
||||
onFinish={(updatedAgent) => {
|
||||
if (updatedAgent) {
|
||||
setAgent((current) =>
|
||||
current ? { ...current, ...updatedAgent } : current,
|
||||
);
|
||||
}
|
||||
refreshPipelines();
|
||||
}}
|
||||
onDirtyChange={setFormDirty}
|
||||
onSavingChange={setFormSaving}
|
||||
onRunnerStatusChange={setRunnerStatus}
|
||||
onSupportedEventPatternsChange={setSupportedEventPatterns}
|
||||
onPlatformToolsChange={setPlatformTools}
|
||||
/>
|
||||
</fieldset>
|
||||
}
|
||||
debugTitle={canOperate ? t('agents.debugTab') : undefined}
|
||||
debugDescription={t('agents.debugPlatformNotice')}
|
||||
debugContent={
|
||||
canOperate ? (
|
||||
<AgentDebugPanel
|
||||
agentId={id}
|
||||
platformTools={platformTools}
|
||||
hasUnsavedChanges={formDirty}
|
||||
beforeRun={async () => agentFormRef.current?.save() ?? false}
|
||||
onOpenRunnerConfig={() =>
|
||||
agentFormRef.current?.openSection('runner_config')
|
||||
}
|
||||
refreshPipelines();
|
||||
}}
|
||||
onDirtyChange={setFormDirty}
|
||||
onSavingChange={setFormSaving}
|
||||
onRunnerStatusChange={setRunnerStatus}
|
||||
onSupportedEventPatternsChange={setSupportedEventPatterns}
|
||||
onPlatformToolsChange={setPlatformTools}
|
||||
/>
|
||||
</fieldset>
|
||||
}
|
||||
debugTitle={canOperate ? t('agents.debugTab') : undefined}
|
||||
debugDescription={t('agents.debugPlatformNotice')}
|
||||
debugContent={
|
||||
canOperate ? (
|
||||
<AgentDebugPanel
|
||||
agentId={id}
|
||||
platformTools={platformTools}
|
||||
hasUnsavedChanges={formDirty}
|
||||
beforeRun={async () => agentFormRef.current?.save() ?? false}
|
||||
onOpenRunnerConfig={() =>
|
||||
agentFormRef.current?.openSection('runner_config')
|
||||
}
|
||||
supportedEventPatterns={supportedEventPatterns}
|
||||
availableEventTypes={availableEventTypes}
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
unsavedLabel={t('pipelines.unsavedChanges')}
|
||||
/>
|
||||
supportedEventPatterns={supportedEventPatterns}
|
||||
availableEventTypes={availableEventTypes}
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
unsavedLabel={t('pipelines.unsavedChanges')}
|
||||
/>
|
||||
)}
|
||||
<EntityBasicInfoDialog
|
||||
open={basicInfoOpen}
|
||||
onOpenChange={setBasicInfoOpen}
|
||||
|
||||
@@ -0,0 +1,437 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { FileCode2, RefreshCw, Settings2, Trash2, Pencil } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import type {
|
||||
Agent,
|
||||
AgentPlatformTool,
|
||||
EventProcessorDescriptor,
|
||||
ProcessorRun,
|
||||
ProcessorRunEvent,
|
||||
} from '@/app/infra/entities/api';
|
||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { extractI18nObject } from '@/i18n/I18nProvider';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import EventProcessorSettings from './components/EventProcessorSettings';
|
||||
|
||||
export default function EventProcessorDetailContent({
|
||||
agent,
|
||||
id,
|
||||
canManage,
|
||||
onDelete,
|
||||
onEdit,
|
||||
onSaved,
|
||||
}: {
|
||||
agent: Agent;
|
||||
id: string;
|
||||
canManage: boolean;
|
||||
onDelete: () => void;
|
||||
onEdit: () => void;
|
||||
onSaved: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [platformTools, setPlatformTools] = useState<AgentPlatformTool[]>([]);
|
||||
const toolLabels = Object.fromEntries(
|
||||
platformTools.map((tool) => [tool.name, extractI18nObject(tool.label)]),
|
||||
);
|
||||
const [components, setComponents] = useState<EventProcessorDescriptor[]>([]);
|
||||
const [componentRef, setComponentRef] = useState(agent.component_ref ?? '');
|
||||
const initialParameters =
|
||||
(
|
||||
(agent.config?.runner_config ?? {}) as Record<
|
||||
string,
|
||||
Record<string, unknown>
|
||||
>
|
||||
)[agent.component_ref ?? ''] ?? {};
|
||||
const [parameters, setParameters] = useState(initialParameters);
|
||||
const [runs, setRuns] = useState<ProcessorRun[]>([]);
|
||||
const [cursor, setCursor] = useState<number | null>(null);
|
||||
const [selected, setSelected] = useState<ProcessorRun | null>(null);
|
||||
const [events, setEvents] = useState<ProcessorRunEvent[]>([]);
|
||||
const [eventCursor, setEventCursor] = useState<number | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [pagingRuns, setPagingRuns] = useState(false);
|
||||
const [pagingEvents, setPagingEvents] = useState(false);
|
||||
const [configOpen, setConfigOpen] = useState(false);
|
||||
const [failed, setFailed] = useState(false);
|
||||
const validate = useRef<(() => Promise<boolean>) | null>(null);
|
||||
const requestVersion = useRef(0);
|
||||
const available = components.some((item) => item.id === agent.component_ref);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setFailed(false);
|
||||
try {
|
||||
const [metadata, page] = await Promise.all([
|
||||
httpClient.getAgentMetadata(),
|
||||
httpClient.getProcessorRuns(id),
|
||||
]);
|
||||
setComponents(metadata.event_processors ?? []);
|
||||
setPlatformTools(metadata.platform_tools ?? []);
|
||||
setRuns(page.items);
|
||||
setCursor(page.has_more ? page.next_cursor : null);
|
||||
} catch {
|
||||
setFailed(true);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [id]);
|
||||
useEffect(() => {
|
||||
void load();
|
||||
}, [load]);
|
||||
useEffect(
|
||||
() => () => {
|
||||
requestVersion.current += 1;
|
||||
},
|
||||
[id],
|
||||
);
|
||||
|
||||
async function openRun(run: ProcessorRun) {
|
||||
const version = ++requestVersion.current;
|
||||
setSelected(run);
|
||||
setEvents([]);
|
||||
setEventCursor(null);
|
||||
try {
|
||||
const page = await httpClient.getProcessorRunEvents(id, run.run_id);
|
||||
if (version !== requestVersion.current) return;
|
||||
setSelected(page.run);
|
||||
setEvents(page.items);
|
||||
setEventCursor(page.has_more ? page.next_cursor : null);
|
||||
} catch {
|
||||
if (version === requestVersion.current)
|
||||
toast.error(t('agents.eventProcessor.loadError'));
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMoreEvents() {
|
||||
if (!selected || eventCursor === null || pagingEvents) return;
|
||||
setPagingEvents(true);
|
||||
const runId = selected.run_id;
|
||||
const version = requestVersion.current;
|
||||
try {
|
||||
const page = await httpClient.getProcessorRunEvents(
|
||||
id,
|
||||
runId,
|
||||
eventCursor,
|
||||
);
|
||||
if (version !== requestVersion.current) return;
|
||||
setEvents((current) => [...current, ...page.items]);
|
||||
setEventCursor(page.has_more ? page.next_cursor : null);
|
||||
} catch {
|
||||
toast.error(t('agents.eventProcessor.loadError'));
|
||||
} finally {
|
||||
setPagingEvents(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMoreRuns() {
|
||||
if (cursor === null || pagingRuns) return;
|
||||
setPagingRuns(true);
|
||||
try {
|
||||
const page = await httpClient.getProcessorRuns(id, cursor);
|
||||
setRuns((current) => [
|
||||
...new Map(
|
||||
[...current, ...page.items].map((run) => [run.run_id, run]),
|
||||
).values(),
|
||||
]);
|
||||
setCursor(page.has_more ? page.next_cursor : null);
|
||||
} catch {
|
||||
toast.error(t('agents.eventProcessor.loadError'));
|
||||
} finally {
|
||||
setPagingRuns(false);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
let busy = false;
|
||||
const timer = window.setInterval(async () => {
|
||||
if (busy || document.hidden) return;
|
||||
busy = true;
|
||||
try {
|
||||
const page = await httpClient.getProcessorRuns(id);
|
||||
if (cancelled) return;
|
||||
setRuns((current) =>
|
||||
[
|
||||
...new Map(
|
||||
[...current, ...page.items].map((run) => [run.run_id, run]),
|
||||
).values(),
|
||||
].sort((a, b) => b.created_at - a.created_at),
|
||||
);
|
||||
if (
|
||||
selected &&
|
||||
!['completed', 'failed', 'cancelled'].includes(selected.status) &&
|
||||
eventCursor === null
|
||||
) {
|
||||
const trace = await httpClient.getProcessorRunEvents(
|
||||
id,
|
||||
selected.run_id,
|
||||
events.at(-1)?.sequence,
|
||||
);
|
||||
if (cancelled) return;
|
||||
setSelected(trace.run);
|
||||
setEvents((current) => [
|
||||
...new Map(
|
||||
[...current, ...trace.items].map((event) => [
|
||||
event.sequence,
|
||||
event,
|
||||
]),
|
||||
).values(),
|
||||
]);
|
||||
setEventCursor(trace.has_more ? trace.next_cursor : null);
|
||||
}
|
||||
} catch {
|
||||
/* Keep existing records visible across transient refresh failures. */
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
}, 3000);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
window.clearInterval(timer);
|
||||
};
|
||||
}, [id, selected, eventCursor, events]);
|
||||
|
||||
async function save() {
|
||||
if (!componentRef || !((await validate.current?.()) ?? true)) return;
|
||||
setSaving(true);
|
||||
try {
|
||||
await httpClient.updateAgent(id, {
|
||||
component_ref: componentRef,
|
||||
config: {
|
||||
...agent.config,
|
||||
runner: { id: componentRef },
|
||||
runner_config: { [componentRef]: parameters },
|
||||
},
|
||||
});
|
||||
toast.success(t('agents.saveSuccess'));
|
||||
onSaved();
|
||||
setConfigOpen(false);
|
||||
await load();
|
||||
} catch {
|
||||
toast.error(t('agents.saveError'));
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
function payload(value: unknown) {
|
||||
return (
|
||||
<pre className="mt-2 whitespace-pre-wrap break-all rounded-md bg-muted/50 p-3 text-xs">
|
||||
{JSON.stringify(value, null, 2)}
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full min-h-0 flex-col gap-4">
|
||||
<header className="flex flex-wrap items-center gap-3">
|
||||
<FileCode2 className="size-6" />
|
||||
<h1 className="text-2xl font-semibold">{agent.name}</h1>
|
||||
{canManage && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onEdit}
|
||||
aria-label={t('common.edit')}
|
||||
>
|
||||
<Pencil className="size-4" />
|
||||
</Button>
|
||||
)}
|
||||
<Badge variant="outline">{t('agents.eventProcessor.type')}</Badge>
|
||||
{!loading && !failed && !available && (
|
||||
<Badge variant="destructive">
|
||||
{t('agents.eventProcessor.unavailable')}
|
||||
</Badge>
|
||||
)}
|
||||
<div className="ml-auto flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
void load();
|
||||
if (selected) void openRun(selected);
|
||||
}}
|
||||
aria-label={t('agents.eventProcessor.refresh')}
|
||||
>
|
||||
<RefreshCw className="size-4" />
|
||||
</Button>
|
||||
{canManage && (
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setConfigOpen(!configOpen)}
|
||||
>
|
||||
<Settings2 className="size-4" />
|
||||
{t('pipelines.configuration')}
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={onDelete}>
|
||||
<Trash2 className="size-4" />
|
||||
{t('common.delete')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
<p className="shrink-0 break-all text-xs text-muted-foreground">
|
||||
{agent.component_ref}
|
||||
</p>
|
||||
{configOpen && (
|
||||
<div className="max-h-[45vh] shrink-0 overflow-y-auto rounded-xl border p-4">
|
||||
<EventProcessorSettings
|
||||
components={components}
|
||||
value={componentRef}
|
||||
parameters={parameters}
|
||||
onChange={(value) => {
|
||||
setComponentRef(value);
|
||||
setParameters({});
|
||||
validate.current = null;
|
||||
}}
|
||||
onParametersChange={setParameters}
|
||||
onValidate={(fn) => {
|
||||
validate.current = fn;
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
className="mt-4"
|
||||
disabled={
|
||||
saving || !components.some((item) => item.id === componentRef)
|
||||
}
|
||||
onClick={() => void save()}
|
||||
>
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{failed && (
|
||||
<p role="alert" className="text-destructive">
|
||||
{t('agents.eventProcessor.loadError')}
|
||||
</p>
|
||||
)}
|
||||
<div className="grid min-h-0 flex-1 gap-4 md:grid-cols-[minmax(240px,0.7fr)_minmax(0,1.3fr)]">
|
||||
<section className="min-h-0 overflow-y-auto rounded-xl border p-4">
|
||||
<h2 className="mb-3 font-semibold">
|
||||
{t('agents.eventProcessor.runs')}
|
||||
</h2>
|
||||
{loading ? (
|
||||
<p>{t('common.loading')}</p>
|
||||
) : runs.length === 0 && !failed ? (
|
||||
<div className="space-y-3 text-sm text-muted-foreground">
|
||||
<p>{t('agents.eventProcessor.noRuns')}</p>
|
||||
<Link className="text-primary underline" to="/home/bots">
|
||||
{t('agents.eventProcessor.bindBot')}
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
runs.map((run) => (
|
||||
<button
|
||||
key={run.run_id}
|
||||
onClick={() => void openRun(run)}
|
||||
className={`mb-2 block w-full rounded-lg border p-3 text-left text-sm ${selected?.run_id === run.run_id ? 'border-primary bg-primary/5' : 'hover:bg-muted/50'}`}
|
||||
>
|
||||
<span className="block break-all font-medium">
|
||||
{run.metadata.event_type}
|
||||
</span>
|
||||
<span className="mt-1 flex flex-wrap justify-between gap-1 text-xs text-muted-foreground">
|
||||
<span>
|
||||
{new Date(run.created_at * 1000).toLocaleString()}
|
||||
</span>
|
||||
<span>
|
||||
{t(`agents.eventProcessor.status_${run.status}`, {
|
||||
defaultValue: run.status,
|
||||
})}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
{cursor !== null && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
disabled={pagingRuns}
|
||||
onClick={() => void loadMoreRuns()}
|
||||
>
|
||||
{t('agents.eventProcessor.loadMore')}
|
||||
</Button>
|
||||
)}
|
||||
</section>
|
||||
<section className="min-h-0 overflow-y-auto rounded-xl border p-4">
|
||||
<h2 className="mb-3 font-semibold">
|
||||
{t('agents.eventProcessor.trace')}
|
||||
</h2>
|
||||
{!selected ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('agents.eventProcessor.selectRun')}
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<details className="rounded-lg border p-3">
|
||||
<summary className="cursor-pointer text-sm font-medium">
|
||||
{t('agents.eventProcessor.input')}
|
||||
</summary>
|
||||
{payload(selected.metadata.input_event)}
|
||||
</details>
|
||||
{selected.metadata.delivery != null && (
|
||||
<details className="rounded-lg border p-3">
|
||||
<summary className="cursor-pointer text-sm font-medium">
|
||||
{t('agents.eventProcessor.destination')}
|
||||
</summary>
|
||||
{payload(selected.metadata.delivery)}
|
||||
</details>
|
||||
)}
|
||||
{events.map((event) =>
|
||||
event.type === 'processor.log' ? (
|
||||
<div
|
||||
key={event.sequence}
|
||||
className="rounded-lg bg-muted/40 p-3 text-sm"
|
||||
>
|
||||
<span className="mr-2 text-xs text-muted-foreground">
|
||||
{String(event.data.level)}
|
||||
</span>
|
||||
<span className="whitespace-pre-wrap break-words">
|
||||
{String(event.data.text)}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<details
|
||||
key={event.sequence}
|
||||
className="rounded-lg border p-3"
|
||||
>
|
||||
<summary className="cursor-pointer break-all text-sm font-medium">
|
||||
{t(
|
||||
`agents.eventProcessor.trace_${event.type.replaceAll('.', '_')}`,
|
||||
{ defaultValue: event.type },
|
||||
)}
|
||||
{typeof event.data.tool_name === 'string' && (
|
||||
<span className="ml-2 text-muted-foreground">
|
||||
{toolLabels[event.data.tool_name] ||
|
||||
event.data.tool_name}
|
||||
</span>
|
||||
)}
|
||||
</summary>
|
||||
{payload(event.data)}
|
||||
</details>
|
||||
),
|
||||
)}
|
||||
{selected.status === 'failed' && selected.status_reason && (
|
||||
<p className="break-words text-sm text-destructive">
|
||||
{selected.status_reason}
|
||||
</p>
|
||||
)}
|
||||
{eventCursor !== null && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
disabled={pagingEvents}
|
||||
onClick={() => void loadMoreEvents()}
|
||||
>
|
||||
{t('agents.eventProcessor.loadMore')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toast } from 'sonner';
|
||||
import { Bot, Workflow } from 'lucide-react';
|
||||
import { Bot, Workflow, FileCode2 } from 'lucide-react';
|
||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||
import { AgentKind } from '@/app/infra/entities/api';
|
||||
import { AgentKind, EventProcessorDescriptor } from '@/app/infra/entities/api';
|
||||
import EventProcessorSettings from './EventProcessorSettings';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
|
||||
import {
|
||||
@@ -35,6 +36,23 @@ export default function AgentCreateContent({
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [kind, setKind] = useState<AgentKind>('agent');
|
||||
const [components, setComponents] = useState<EventProcessorDescriptor[]>([]);
|
||||
const [componentRef, setComponentRef] = useState('');
|
||||
const [parameters, setParameters] = useState<Record<string, unknown>>({});
|
||||
const validateParameters = useRef<(() => Promise<boolean>) | null>(null);
|
||||
useEffect(() => {
|
||||
if (kind !== 'event_processor') return;
|
||||
let cancelled = false;
|
||||
httpClient
|
||||
.getAgentMetadata()
|
||||
.then((metadata) => {
|
||||
if (!cancelled) setComponents(metadata.event_processors ?? []);
|
||||
})
|
||||
.catch(() => toast.error(t('agents.eventProcessor.loadError')));
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [kind, t]);
|
||||
const formSchema = z.object({
|
||||
name: z.string().min(1, { message: t('agents.nameRequired') }),
|
||||
description: z.string().optional(),
|
||||
@@ -51,8 +69,14 @@ export default function AgentCreateContent({
|
||||
});
|
||||
|
||||
function handleKindChange(nextKind: AgentKind) {
|
||||
const previousDefaultEmoji = kind === 'pipeline' ? '⚙️' : '🤖';
|
||||
const nextDefaultEmoji = nextKind === 'pipeline' ? '⚙️' : '🤖';
|
||||
const previousDefaultEmoji =
|
||||
kind === 'pipeline' ? '⚙️' : kind === 'event_processor' ? '⚡' : '🤖';
|
||||
const nextDefaultEmoji =
|
||||
nextKind === 'pipeline'
|
||||
? '⚙️'
|
||||
: nextKind === 'event_processor'
|
||||
? '⚡'
|
||||
: '🤖';
|
||||
setKind(nextKind);
|
||||
const currentEmoji = form.getValues('emoji');
|
||||
if (!currentEmoji || currentEmoji === previousDefaultEmoji) {
|
||||
@@ -60,10 +84,24 @@ export default function AgentCreateContent({
|
||||
}
|
||||
}
|
||||
|
||||
function handleSubmit(values: FormValues) {
|
||||
async function handleSubmit(values: FormValues) {
|
||||
if (
|
||||
kind === 'event_processor' &&
|
||||
(!componentRef || !((await validateParameters.current?.()) ?? true))
|
||||
)
|
||||
return;
|
||||
httpClient
|
||||
.createAgent({
|
||||
kind,
|
||||
...(kind === 'event_processor'
|
||||
? {
|
||||
component_ref: componentRef,
|
||||
config: {
|
||||
runner: { id: componentRef },
|
||||
runner_config: { [componentRef]: parameters },
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
name: values.name,
|
||||
description: values.description ?? '',
|
||||
emoji: values.emoji || (kind === 'pipeline' ? '⚙️' : '🤖'),
|
||||
@@ -90,13 +128,26 @@ export default function AgentCreateContent({
|
||||
title: t('agents.pipelineType'),
|
||||
description: t('agents.pipelineTypeDescription'),
|
||||
},
|
||||
{
|
||||
kind: 'event_processor' as const,
|
||||
icon: FileCode2,
|
||||
title: t('agents.eventProcessor.type'),
|
||||
description: t('agents.eventProcessor.description'),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="flex items-center justify-between pb-4 shrink-0">
|
||||
<h1 className="text-xl font-semibold">{t('agents.create')}</h1>
|
||||
<Button type="submit" form="agent-create-form">
|
||||
<Button
|
||||
type="submit"
|
||||
form="agent-create-form"
|
||||
disabled={
|
||||
form.formState.isSubmitting ||
|
||||
(kind === 'event_processor' && !componentRef)
|
||||
}
|
||||
>
|
||||
{t('common.submit')}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -158,6 +209,22 @@ export default function AgentCreateContent({
|
||||
</ToggleGroup>
|
||||
</section>
|
||||
|
||||
{kind === 'event_processor' && (
|
||||
<EventProcessorSettings
|
||||
components={components}
|
||||
value={componentRef}
|
||||
parameters={parameters}
|
||||
onChange={(value) => {
|
||||
setComponentRef(value);
|
||||
setParameters({});
|
||||
validateParameters.current = null;
|
||||
}}
|
||||
onParametersChange={setParameters}
|
||||
onValidate={(validate) => {
|
||||
validateParameters.current = validate;
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t('agents.basicInfo')}</CardTitle>
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
import type { EventProcessorDescriptor } from '@/app/infra/entities/api';
|
||||
import { extractI18nObject } from '@/i18n/I18nProvider';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent';
|
||||
|
||||
export default function EventProcessorSettings({
|
||||
components,
|
||||
value,
|
||||
parameters,
|
||||
onChange,
|
||||
onParametersChange,
|
||||
onValidate,
|
||||
}: {
|
||||
components: EventProcessorDescriptor[];
|
||||
value: string;
|
||||
parameters: Record<string, unknown>;
|
||||
onChange: (value: string) => void;
|
||||
onParametersChange: (value: Record<string, unknown>) => void;
|
||||
onValidate?: (validate: () => Promise<boolean>) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const selected = components.find((item) => item.id === value);
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
className="text-sm font-medium"
|
||||
htmlFor="event-processor-component"
|
||||
>
|
||||
{t('agents.eventProcessor.component')}
|
||||
</label>
|
||||
<Select value={value} onValueChange={onChange}>
|
||||
<SelectTrigger id="event-processor-component">
|
||||
<SelectValue
|
||||
placeholder={t('agents.eventProcessor.selectComponent')}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{value && !selected && (
|
||||
<SelectItem value={value}>
|
||||
{t('agents.eventProcessor.unavailable')}
|
||||
</SelectItem>
|
||||
)}
|
||||
{components.map((component) => (
|
||||
<SelectItem key={component.id} value={component.id}>
|
||||
{extractI18nObject({
|
||||
en_US: component.id,
|
||||
zh_Hans: component.id,
|
||||
...component.label,
|
||||
})}{' '}
|
||||
· {component.plugin_author}/{component.plugin_name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{components.length === 0 && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('agents.eventProcessor.noComponents')}{' '}
|
||||
<Link className="text-primary underline" to="/home/plugins">
|
||||
{t('agents.eventProcessor.installPlugin')}
|
||||
</Link>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{selected && (
|
||||
<p className="break-words text-xs text-muted-foreground">
|
||||
{selected.supported_event_patterns.join(' · ')}
|
||||
</p>
|
||||
)}
|
||||
{selected && selected.config_schema.length > 0 && (
|
||||
<DynamicFormComponent
|
||||
key={value}
|
||||
itemConfigList={selected.config_schema}
|
||||
initialValues={parameters}
|
||||
onSubmit={(values) =>
|
||||
onParametersChange(values as Record<string, unknown>)
|
||||
}
|
||||
onValidate={onValidate}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -374,5 +374,29 @@ function PipelineDiagram() {
|
||||
}
|
||||
|
||||
export default function ProcessorTypeDiagram({ kind }: { kind: AgentKind }) {
|
||||
const { t } = useTranslation();
|
||||
if (kind === 'event_processor')
|
||||
return (
|
||||
<div className="flex h-full flex-col justify-center gap-6 rounded-xl border bg-muted/20 p-8">
|
||||
<h3 className="text-lg font-semibold">
|
||||
{t('agents.eventProcessor.type')}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('agents.eventProcessor.description')}
|
||||
</p>
|
||||
{['input', 'component', 'trace'].map((step, index) => (
|
||||
<div
|
||||
key={step}
|
||||
className="flex items-center gap-3 rounded-lg border bg-background p-4"
|
||||
>
|
||||
<span className="text-primary">{index + 1}</span>
|
||||
<span>{t(`agents.eventProcessor.${step}`)}</span>
|
||||
</div>
|
||||
))}
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('agents.eventProcessor.activation')}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
return kind === 'agent' ? <AgentDiagram /> : <PipelineDiagram />;
|
||||
}
|
||||
|
||||
@@ -75,7 +75,12 @@ const getFormSchema = (t: (key: string) => string) =>
|
||||
z.object({
|
||||
id: z.string().optional(),
|
||||
event_pattern: z.string(),
|
||||
target_type: z.enum(['agent', 'pipeline', 'discard']),
|
||||
target_type: z.enum([
|
||||
'agent',
|
||||
'pipeline',
|
||||
'event_processor',
|
||||
'discard',
|
||||
]),
|
||||
target_uuid: z.string(),
|
||||
filters: z.array(z.record(z.string(), z.any())).optional(),
|
||||
priority: z.number(),
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
UserMinus,
|
||||
UserPlus,
|
||||
Workflow,
|
||||
FileCode2,
|
||||
XCircle,
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -518,6 +519,11 @@ function TargetCombobox({
|
||||
const pipelines = pipelineAllowed
|
||||
? agentOptions.filter((a) => a.kind === 'pipeline')
|
||||
: [];
|
||||
const eventProcessors = agentOptions.filter(
|
||||
(item) =>
|
||||
item.kind === 'event_processor' &&
|
||||
agentSupportsEventPattern(item, binding.event_pattern),
|
||||
);
|
||||
|
||||
function currentLabel() {
|
||||
if (targetType === 'discard')
|
||||
@@ -531,7 +537,9 @@ function TargetCombobox({
|
||||
if (agent)
|
||||
return (
|
||||
<span className="flex items-center gap-1.5">
|
||||
{agent.kind === 'pipeline' ? (
|
||||
{agent.kind === 'event_processor' ? (
|
||||
<FileCode2 className="size-3.5" />
|
||||
) : agent.kind === 'pipeline' ? (
|
||||
<Workflow className="size-3.5" />
|
||||
) : (
|
||||
<Bot className="size-3.5" />
|
||||
@@ -585,6 +593,26 @@ function TargetCombobox({
|
||||
))}
|
||||
</CommandGroup>
|
||||
)}
|
||||
{eventProcessors.length > 0 && (
|
||||
<CommandGroup heading={t('agents.eventProcessor.type')}>
|
||||
{eventProcessors.map((item) => (
|
||||
<CommandItem
|
||||
key={item.uuid}
|
||||
value={`event_processor:${item.uuid}:${item.name}`}
|
||||
onSelect={() =>
|
||||
select(encodeTarget('event_processor', item.uuid || ''))
|
||||
}
|
||||
>
|
||||
<FileCode2 className="mr-2 size-3.5 shrink-0" />
|
||||
<span className="truncate">{targetLabel(item)}</span>
|
||||
{current ===
|
||||
encodeTarget('event_processor', item.uuid || '') && (
|
||||
<Check className="ml-auto size-3.5 shrink-0" />
|
||||
)}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
)}
|
||||
{pipelines.length > 0 && (
|
||||
<CommandGroup heading={t('bots.targetPipeline')}>
|
||||
{pipelines.map((a) => (
|
||||
|
||||
@@ -730,13 +730,16 @@ function NavItems({
|
||||
const showAgentGroupHeaders =
|
||||
isAgents && !inPopover && sidebarData.agentsGroupByKind;
|
||||
|
||||
const agentGroupOrder: Array<'agent' | 'pipeline'> = [
|
||||
'agent',
|
||||
'pipeline',
|
||||
];
|
||||
const agentGroupLabelKey: Record<'agent' | 'pipeline', string> = {
|
||||
const agentGroupOrder: Array<
|
||||
'agent' | 'pipeline' | 'event_processor'
|
||||
> = ['agent', 'pipeline', 'event_processor'];
|
||||
const agentGroupLabelKey: Record<
|
||||
'agent' | 'pipeline' | 'event_processor',
|
||||
string
|
||||
> = {
|
||||
agent: 'agents.kindBadgeAgent',
|
||||
pipeline: 'agents.kindBadgePipeline',
|
||||
event_processor: 'agents.eventProcessor.type',
|
||||
};
|
||||
|
||||
const groupOrder: Array<'plugin' | 'mcp' | 'skill'> = [
|
||||
@@ -889,12 +892,16 @@ function NavItems({
|
||||
<span
|
||||
className="ml-auto flex shrink-0 items-center text-muted-foreground"
|
||||
title={
|
||||
item.kind === 'pipeline'
|
||||
? t('agents.kindBadgePipeline')
|
||||
: t('agents.kindBadgeAgent')
|
||||
item.kind === 'event_processor'
|
||||
? t('agents.eventProcessor.type')
|
||||
: item.kind === 'pipeline'
|
||||
? t('agents.kindBadgePipeline')
|
||||
: t('agents.kindBadgeAgent')
|
||||
}
|
||||
>
|
||||
{item.kind === 'pipeline' ? (
|
||||
{item.kind === 'event_processor' ? (
|
||||
<span className="text-xs">⚡</span>
|
||||
) : item.kind === 'pipeline' ? (
|
||||
<Workflow className="size-3.5" />
|
||||
) : (
|
||||
<Bot className="size-3.5" />
|
||||
|
||||
@@ -31,7 +31,7 @@ export interface SidebarEntityItem {
|
||||
// Set when this item appears in the unified extensions list
|
||||
extensionType?: 'plugin' | 'mcp' | 'skill';
|
||||
// Agent-specific: distinguishes Agent processors from Pipelines
|
||||
kind?: 'agent' | 'pipeline';
|
||||
kind?: 'agent' | 'pipeline' | 'event_processor';
|
||||
}
|
||||
|
||||
// Plugin page registered by a plugin
|
||||
|
||||
@@ -162,7 +162,43 @@ export interface ApiRespPipelines {
|
||||
pipelines: Pipeline[];
|
||||
}
|
||||
|
||||
export type AgentKind = 'agent' | 'pipeline';
|
||||
export type AgentKind = 'agent' | 'pipeline' | 'event_processor';
|
||||
|
||||
export interface EventProcessorDescriptor {
|
||||
id: string;
|
||||
label: Record<string, string>;
|
||||
plugin_author: string;
|
||||
plugin_name: string;
|
||||
config_schema: import('../form/dynamic').IDynamicFormItemSchema[];
|
||||
supported_event_patterns: string[];
|
||||
}
|
||||
|
||||
export interface ProcessorRun {
|
||||
run_id: string;
|
||||
status: string;
|
||||
status_reason?: string;
|
||||
created_at: number;
|
||||
metadata: { event_type?: string; input_event?: unknown; delivery?: unknown };
|
||||
}
|
||||
|
||||
export interface ProcessorRunEvent {
|
||||
sequence: number;
|
||||
type: string;
|
||||
data: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface ProcessorRunPage {
|
||||
items: ProcessorRun[];
|
||||
next_cursor: number | null;
|
||||
has_more: boolean;
|
||||
}
|
||||
|
||||
export interface ProcessorRunEventPage {
|
||||
run: ProcessorRun;
|
||||
items: ProcessorRunEvent[];
|
||||
next_cursor: number | null;
|
||||
has_more: boolean;
|
||||
}
|
||||
|
||||
export interface AgentCapability {
|
||||
supported_event_patterns: string[];
|
||||
@@ -192,6 +228,7 @@ export interface ApiRespAgent {
|
||||
}
|
||||
|
||||
export interface GetAgentMetadataResponseData {
|
||||
event_processors?: EventProcessorDescriptor[];
|
||||
runner_config?: PipelineConfigTab;
|
||||
platform_tools: AgentPlatformTool[];
|
||||
host_tools?: PluginTool[] | null;
|
||||
@@ -274,7 +311,7 @@ export interface Bot {
|
||||
export interface EventBinding {
|
||||
id?: string;
|
||||
event_pattern: string;
|
||||
target_type: 'agent' | 'pipeline' | 'discard';
|
||||
target_type: AgentKind | 'discard';
|
||||
target_uuid: string;
|
||||
filters?: Array<Record<string, unknown>>;
|
||||
priority: number;
|
||||
|
||||
@@ -268,6 +268,25 @@ export class BackendClient extends BaseHttpClient {
|
||||
return this.get('/api/v1/agents/_/metadata');
|
||||
}
|
||||
|
||||
public getProcessorRuns(
|
||||
uuid: string,
|
||||
beforeId?: number,
|
||||
): Promise<import('../entities/api').ProcessorRunPage> {
|
||||
return this.get(
|
||||
`/api/v1/agents/${encodeURIComponent(uuid)}/runs${beforeId === undefined ? '' : `?before_id=${beforeId}`}`,
|
||||
);
|
||||
}
|
||||
|
||||
public getProcessorRunEvents(
|
||||
uuid: string,
|
||||
runId: string,
|
||||
afterSequence?: number,
|
||||
): Promise<import('../entities/api').ProcessorRunEventPage> {
|
||||
return this.get(
|
||||
`/api/v1/agents/${encodeURIComponent(uuid)}/runs/${encodeURIComponent(runId)}/events${afterSequence === undefined ? '' : `?after_sequence=${afterSequence}`}`,
|
||||
);
|
||||
}
|
||||
|
||||
public createAgent(agent: Agent): Promise<{ uuid: string; kind: string }> {
|
||||
return this.post('/api/v1/agents', agent);
|
||||
}
|
||||
|
||||
@@ -395,20 +395,19 @@ const enUS = {
|
||||
commonScenarios: 'Common scenarios',
|
||||
dragEventRoute: 'Drag route {{index}}',
|
||||
behaviorReplyMessages: 'Reply to messages',
|
||||
behaviorReplyMessagesDescription:
|
||||
'Send incoming messages to an Agent or Pipeline.',
|
||||
behaviorReplyMessagesDescription: 'Send incoming messages to a processor.',
|
||||
behaviorWelcomeMembers: 'Welcome new members',
|
||||
behaviorWelcomeMembersDescription:
|
||||
'Run an Agent when someone joins a group.',
|
||||
'Run a processor when someone joins a group.',
|
||||
behaviorHandleDepartures: 'Handle member departures',
|
||||
behaviorHandleDeparturesDescription:
|
||||
'Run an Agent when someone leaves or is removed.',
|
||||
'Run a processor when someone leaves or is removed.',
|
||||
behaviorReviewFriendRequests: 'Review friend requests',
|
||||
behaviorReviewFriendRequestsDescription:
|
||||
'Let an Agent decide how to handle a new request.',
|
||||
'Send new friend requests to a processor.',
|
||||
behaviorHandleModeration: 'Handle moderation events',
|
||||
behaviorHandleModerationDescription:
|
||||
'Run an Agent when a group member is restricted.',
|
||||
'Run a processor when a group member is restricted.',
|
||||
behaviorCustom: 'Configure another event',
|
||||
behaviorCustomDescription:
|
||||
'Add a route and choose from every event supported by this adapter.',
|
||||
@@ -704,6 +703,36 @@ const enUS = {
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
eventProcessor: {
|
||||
type: 'Event processor',
|
||||
description: 'Process platform events using plugin code.',
|
||||
component: 'Plugin component',
|
||||
selectComponent: 'Select an event processor',
|
||||
unavailable: 'Component unavailable',
|
||||
noComponents: 'No event processor components installed.',
|
||||
installPlugin: 'Install a plugin',
|
||||
loadError: 'Unable to load processor details.',
|
||||
refresh: 'Refresh',
|
||||
runs: 'Runs',
|
||||
noRuns: 'No runs yet. Bind a Bot event to start.',
|
||||
bindBot: 'Bind Bot events',
|
||||
trace: 'Logs and message flow',
|
||||
selectRun: 'Select a run to view details.',
|
||||
input: 'Incoming event',
|
||||
destination: 'Delivery destination',
|
||||
loadMore: 'Load more',
|
||||
activation: 'Install a plugin, create an instance, then bind Bot events.',
|
||||
status_pending: 'Pending',
|
||||
status_running: 'Running',
|
||||
status_completed: 'Completed',
|
||||
status_failed: 'Failed',
|
||||
status_cancelled: 'Cancelled',
|
||||
status_queued: 'Queued',
|
||||
trace_run_completed: 'Run completed',
|
||||
trace_run_failed: 'Run failed',
|
||||
trace_tool_call_started: 'Action started',
|
||||
trace_tool_call_completed: 'Action result',
|
||||
},
|
||||
debugData: {
|
||||
title: 'Event data',
|
||||
form: 'Common fields',
|
||||
@@ -735,7 +764,7 @@ const enUS = {
|
||||
description: 'Create reusable processors and use them in bot event routing',
|
||||
create: 'Create Processor',
|
||||
editAgent: 'Edit Agent',
|
||||
selectFromSidebar: 'Select an Agent or Pipeline from the sidebar',
|
||||
selectFromSidebar: 'Select a processor from the sidebar',
|
||||
agentType: 'Agent',
|
||||
agentTypeDescription:
|
||||
'Use a runner to handle messages, group members, friends, feedback, and other platform events. Best for scenarios that need autonomous decisions, tool use, or non-message events.',
|
||||
|
||||
@@ -506,6 +506,37 @@ const esES = {
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
eventProcessor: {
|
||||
type: 'Procesador de eventos',
|
||||
description: 'Procesa eventos con código del plugin.',
|
||||
component: 'Componente del plugin',
|
||||
selectComponent: 'Seleccionar un procesador',
|
||||
unavailable: 'Componente no disponible',
|
||||
noComponents: 'No hay componentes de eventos instalados.',
|
||||
installPlugin: 'Instalar un plugin',
|
||||
loadError: 'No se pudieron cargar los detalles.',
|
||||
refresh: 'Actualizar',
|
||||
runs: 'Ejecuciones',
|
||||
noRuns: 'Sin ejecuciones. Vincula eventos de un Bot para empezar.',
|
||||
bindBot: 'Vincular eventos del Bot',
|
||||
trace: 'Registros y flujo de mensajes',
|
||||
selectRun: 'Selecciona una ejecución para ver los detalles.',
|
||||
input: 'Evento recibido',
|
||||
destination: 'Destino de entrega',
|
||||
loadMore: 'Cargar más',
|
||||
activation:
|
||||
'Instala un plugin, crea una instancia y vincula eventos del Bot.',
|
||||
status_pending: 'Pendiente',
|
||||
status_running: 'En ejecución',
|
||||
status_completed: 'Completado',
|
||||
status_failed: 'Error',
|
||||
status_cancelled: 'Cancelado',
|
||||
status_queued: 'En cola',
|
||||
trace_run_completed: 'Ejecución completada',
|
||||
trace_run_failed: 'Ejecución fallida',
|
||||
trace_tool_call_started: 'Acción iniciada',
|
||||
trace_tool_call_completed: 'Resultado de la acción',
|
||||
},
|
||||
debugData: {
|
||||
title: 'Datos del evento',
|
||||
form: 'Campos comunes',
|
||||
@@ -538,7 +569,7 @@ const esES = {
|
||||
'Crea procesadores reutilizables y úsalos en el enrutamiento de eventos del bot',
|
||||
create: 'Crear procesador',
|
||||
editAgent: 'Editar Agent',
|
||||
selectFromSidebar: 'Selecciona un Agent o Pipeline desde la barra lateral',
|
||||
selectFromSidebar: 'Selecciona un procesador en la barra lateral',
|
||||
agentType: 'Agent',
|
||||
agentTypeDescription:
|
||||
'Usa un runner para procesar mensajes, miembros de grupo, amigos, retroalimentación y otros eventos de plataforma.',
|
||||
|
||||
@@ -402,19 +402,19 @@ const jaJP = {
|
||||
dragEventRoute: 'ルート {{index}} をドラッグ',
|
||||
behaviorReplyMessages: '受信メッセージに返信',
|
||||
behaviorReplyMessagesDescription:
|
||||
'受信メッセージを Agent または Pipeline で処理します。',
|
||||
'受信メッセージをプロセッサーに渡します。',
|
||||
behaviorWelcomeMembers: '新しいメンバーを歓迎',
|
||||
behaviorWelcomeMembersDescription:
|
||||
'メンバーがグループに参加したときに Agent を実行します。',
|
||||
'グループへの参加時にプロセッサーを実行します。',
|
||||
behaviorHandleDepartures: 'メンバーの退出を処理',
|
||||
behaviorHandleDeparturesDescription:
|
||||
'メンバーが退出または削除されたときに Agent を実行します。',
|
||||
'グループからの退出時にプロセッサーを実行します。',
|
||||
behaviorReviewFriendRequests: '友だち申請を確認',
|
||||
behaviorReviewFriendRequestsDescription:
|
||||
'新しい申請の処理方法を Agent に判断させます。',
|
||||
'新しい友達リクエストをプロセッサーに渡します。',
|
||||
behaviorHandleModeration: 'モデレーションイベントを処理',
|
||||
behaviorHandleModerationDescription:
|
||||
'グループメンバーが制限されたときに Agent を実行します。',
|
||||
'グループメンバーの制限時にプロセッサーを実行します。',
|
||||
behaviorCustom: '別のイベントを設定',
|
||||
behaviorCustomDescription:
|
||||
'ルートを追加し、このアダプターが対応する全イベントから選択します。',
|
||||
@@ -716,6 +716,37 @@ const jaJP = {
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
eventProcessor: {
|
||||
type: 'イベントプロセッサー',
|
||||
description: 'プラグインのコードでイベントを処理します。',
|
||||
component: 'プラグインコンポーネント',
|
||||
selectComponent: 'イベントプロセッサーを選択',
|
||||
unavailable: 'コンポーネントを利用できません',
|
||||
noComponents: 'イベントプロセッサーがインストールされていません。',
|
||||
installPlugin: 'プラグインをインストール',
|
||||
loadError: '詳細を読み込めません。',
|
||||
refresh: '更新',
|
||||
runs: '実行履歴',
|
||||
noRuns: '実行履歴はありません。Bot イベントを紐付けて開始します。',
|
||||
bindBot: 'Bot イベントを紐付ける',
|
||||
trace: 'ログとメッセージの流れ',
|
||||
selectRun: '実行履歴を選択して詳細を表示します。',
|
||||
input: '受信イベント',
|
||||
destination: '送信先',
|
||||
loadMore: 'さらに読み込む',
|
||||
activation:
|
||||
'プラグインをインストールし、インスタンスを作成して Bot イベントを紐付けます。',
|
||||
status_pending: '待機中',
|
||||
status_running: '実行中',
|
||||
status_completed: '完了',
|
||||
status_failed: '失敗',
|
||||
status_cancelled: 'キャンセル済み',
|
||||
status_queued: 'キュー待ち',
|
||||
trace_run_completed: '実行完了',
|
||||
trace_run_failed: '実行失敗',
|
||||
trace_tool_call_started: 'アクション開始',
|
||||
trace_tool_call_completed: 'アクション結果',
|
||||
},
|
||||
debugData: {
|
||||
title: 'イベントデータ',
|
||||
form: '基本項目',
|
||||
@@ -772,8 +803,7 @@ const jaJP = {
|
||||
'再利用可能なプロセッサーを作成し、ボットのイベントルーティングで使用します',
|
||||
create: 'プロセッサーを作成',
|
||||
editAgent: 'Agent を編集',
|
||||
selectFromSidebar:
|
||||
'サイドバーから Agent または Pipeline を選択してください',
|
||||
selectFromSidebar: 'サイドバーからプロセッサーを選択',
|
||||
agentType: 'Agent',
|
||||
agentTypeDescription:
|
||||
'Runner を使ってメッセージ、グループメンバー、友だち、フィードバックなどのプラットフォームイベントを処理します。自律的な判断、ツール利用、メッセージ以外のイベント対応が必要な場合に適しています。',
|
||||
|
||||
@@ -503,6 +503,37 @@ const ruRU = {
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
eventProcessor: {
|
||||
type: 'Обработчик событий',
|
||||
description: 'Обрабатывает события кодом плагина.',
|
||||
component: 'Компонент плагина',
|
||||
selectComponent: 'Выберите обработчик',
|
||||
unavailable: 'Компонент недоступен',
|
||||
noComponents: 'Компоненты обработки событий не установлены.',
|
||||
installPlugin: 'Установить плагин',
|
||||
loadError: 'Не удалось загрузить данные.',
|
||||
refresh: 'Обновить',
|
||||
runs: 'Запуски',
|
||||
noRuns: 'Запусков пока нет. Привяжите события бота.',
|
||||
bindBot: 'Привязать события бота',
|
||||
trace: 'Журнал и поток сообщений',
|
||||
selectRun: 'Выберите запуск для просмотра.',
|
||||
input: 'Входящее событие',
|
||||
destination: 'Получатель',
|
||||
loadMore: 'Загрузить ещё',
|
||||
activation:
|
||||
'Установите плагин, создайте экземпляр и привяжите события бота.',
|
||||
status_pending: 'Ожидание',
|
||||
status_running: 'Выполняется',
|
||||
status_completed: 'Завершено',
|
||||
status_failed: 'Ошибка',
|
||||
status_cancelled: 'Отменено',
|
||||
status_queued: 'В очереди',
|
||||
trace_run_completed: 'Выполнение завершено',
|
||||
trace_run_failed: 'Ошибка выполнения',
|
||||
trace_tool_call_started: 'Действие начато',
|
||||
trace_tool_call_completed: 'Результат действия',
|
||||
},
|
||||
debugData: {
|
||||
title: 'Данные события',
|
||||
form: 'Основные поля',
|
||||
@@ -535,7 +566,7 @@ const ruRU = {
|
||||
'Создавайте переиспользуемые обработчики и используйте их в маршрутизации событий бота',
|
||||
create: 'Создать обработчик',
|
||||
editAgent: 'Редактировать Agent',
|
||||
selectFromSidebar: 'Выберите Agent или Pipeline на боковой панели',
|
||||
selectFromSidebar: 'Выберите обработчик на боковой панели',
|
||||
agentType: 'Agent',
|
||||
agentTypeDescription:
|
||||
'Используйте runner для обработки сообщений, участников групп, друзей, обратной связи и других событий платформы.',
|
||||
|
||||
@@ -490,6 +490,36 @@ const thTH = {
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
eventProcessor: {
|
||||
type: 'ตัวประมวลผลเหตุการณ์',
|
||||
description: 'ประมวลผลเหตุการณ์ด้วยโค้ดปลั๊กอิน',
|
||||
component: 'ส่วนประกอบปลั๊กอิน',
|
||||
selectComponent: 'เลือกตัวประมวลผลเหตุการณ์',
|
||||
unavailable: 'ส่วนประกอบไม่พร้อมใช้งาน',
|
||||
noComponents: 'ยังไม่ได้ติดตั้งส่วนประกอบประมวลผลเหตุการณ์',
|
||||
installPlugin: 'ติดตั้งปลั๊กอิน',
|
||||
loadError: 'ไม่สามารถโหลดรายละเอียดได้',
|
||||
refresh: 'รีเฟรช',
|
||||
runs: 'ประวัติการทำงาน',
|
||||
noRuns: 'ยังไม่มีการทำงาน เชื่อมโยงเหตุการณ์บอทเพื่อเริ่มต้น',
|
||||
bindBot: 'เชื่อมโยงเหตุการณ์บอท',
|
||||
trace: 'บันทึกและเส้นทางข้อความ',
|
||||
selectRun: 'เลือกการทำงานเพื่อดูรายละเอียด',
|
||||
input: 'เหตุการณ์ขาเข้า',
|
||||
destination: 'ปลายทางการส่ง',
|
||||
loadMore: 'โหลดเพิ่มเติม',
|
||||
activation: 'ติดตั้งปลั๊กอิน สร้างอินสแตนซ์ แล้วเชื่อมโยงเหตุการณ์บอท',
|
||||
status_pending: 'รอดำเนินการ',
|
||||
status_running: 'กำลังทำงาน',
|
||||
status_completed: 'เสร็จสิ้น',
|
||||
status_failed: 'ล้มเหลว',
|
||||
status_cancelled: 'ยกเลิกแล้ว',
|
||||
status_queued: 'อยู่ในคิว',
|
||||
trace_run_completed: 'ทำงานเสร็จสิ้น',
|
||||
trace_run_failed: 'การทำงานล้มเหลว',
|
||||
trace_tool_call_started: 'เริ่มดำเนินการ',
|
||||
trace_tool_call_completed: 'ผลการดำเนินการ',
|
||||
},
|
||||
debugData: {
|
||||
title: 'ข้อมูลเหตุการณ์',
|
||||
form: 'ฟิลด์ทั่วไป',
|
||||
@@ -521,7 +551,7 @@ const thTH = {
|
||||
description: 'สร้างตัวประมวลผลที่ใช้ซ้ำได้และใช้ในเส้นทางเหตุการณ์ของบอท',
|
||||
create: 'สร้างตัวประมวลผล',
|
||||
editAgent: 'แก้ไข Agent',
|
||||
selectFromSidebar: 'เลือก Agent หรือ Pipeline จากแถบด้านข้าง',
|
||||
selectFromSidebar: 'เลือกตัวประมวลผลจากแถบด้านข้าง',
|
||||
agentType: 'Agent',
|
||||
agentTypeDescription:
|
||||
'ใช้ runner เพื่อประมวลผลข้อความ สมาชิกกลุ่ม เพื่อน ฟีดแบ็ก และเหตุการณ์แพลตฟอร์มอื่นๆ',
|
||||
|
||||
@@ -499,6 +499,36 @@ const viVN = {
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
eventProcessor: {
|
||||
type: 'Bộ xử lý sự kiện',
|
||||
description: 'Xử lý sự kiện bằng mã plugin.',
|
||||
component: 'Thành phần plugin',
|
||||
selectComponent: 'Chọn bộ xử lý sự kiện',
|
||||
unavailable: 'Thành phần không khả dụng',
|
||||
noComponents: 'Chưa cài thành phần xử lý sự kiện.',
|
||||
installPlugin: 'Cài plugin',
|
||||
loadError: 'Không thể tải chi tiết.',
|
||||
refresh: 'Làm mới',
|
||||
runs: 'Lịch sử chạy',
|
||||
noRuns: 'Chưa có lần chạy nào. Liên kết sự kiện Bot để bắt đầu.',
|
||||
bindBot: 'Liên kết sự kiện Bot',
|
||||
trace: 'Nhật ký và luồng tin nhắn',
|
||||
selectRun: 'Chọn một lần chạy để xem chi tiết.',
|
||||
input: 'Sự kiện đầu vào',
|
||||
destination: 'Đích gửi',
|
||||
loadMore: 'Tải thêm',
|
||||
activation: 'Cài plugin, tạo phiên bản rồi liên kết sự kiện Bot.',
|
||||
status_pending: 'Đang chờ',
|
||||
status_running: 'Đang chạy',
|
||||
status_completed: 'Hoàn tất',
|
||||
status_failed: 'Thất bại',
|
||||
status_cancelled: 'Đã hủy',
|
||||
status_queued: 'Trong hàng đợi',
|
||||
trace_run_completed: 'Chạy hoàn tất',
|
||||
trace_run_failed: 'Chạy thất bại',
|
||||
trace_tool_call_started: 'Bắt đầu hành động',
|
||||
trace_tool_call_completed: 'Kết quả hành động',
|
||||
},
|
||||
debugData: {
|
||||
title: 'Dữ liệu sự kiện',
|
||||
form: 'Trường thường dùng',
|
||||
@@ -531,7 +561,7 @@ const viVN = {
|
||||
'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',
|
||||
create: 'Tạo bộ xử lý',
|
||||
editAgent: 'Chỉnh sửa Agent',
|
||||
selectFromSidebar: 'Chọn một Agent hoặc Pipeline từ thanh bên',
|
||||
selectFromSidebar: 'Chọn bộ xử lý từ thanh bên',
|
||||
agentType: 'Agent',
|
||||
agentTypeDescription:
|
||||
'Dùng runner để xử lý tin nhắn, thành viên nhóm, bạn bè, phản hồi và các sự kiện nền tảng khác.',
|
||||
|
||||
@@ -378,17 +378,15 @@ const zhHans = {
|
||||
commonScenarios: '常用场景',
|
||||
dragEventRoute: '拖动第 {{index}} 条路由',
|
||||
behaviorReplyMessages: '回复收到的消息',
|
||||
behaviorReplyMessagesDescription:
|
||||
'把收到的消息交给 Agent 或 Pipeline 处理。',
|
||||
behaviorReplyMessagesDescription: '把收到的消息交给处理器。',
|
||||
behaviorWelcomeMembers: '欢迎新成员',
|
||||
behaviorWelcomeMembersDescription: '有人加入群组时运行 Agent。',
|
||||
behaviorWelcomeMembersDescription: '有人加入群组时运行处理器。',
|
||||
behaviorHandleDepartures: '处理成员离群',
|
||||
behaviorHandleDeparturesDescription: '有人离开或被移出群组时运行 Agent。',
|
||||
behaviorHandleDeparturesDescription: '有人离开或被移出群组时运行处理器。',
|
||||
behaviorReviewFriendRequests: '审核好友请求',
|
||||
behaviorReviewFriendRequestsDescription:
|
||||
'让 Agent 决定如何处理新的好友请求。',
|
||||
behaviorReviewFriendRequestsDescription: '把新的好友请求交给处理器。',
|
||||
behaviorHandleModeration: '处理群管理事件',
|
||||
behaviorHandleModerationDescription: '群成员受到限制时运行 Agent。',
|
||||
behaviorHandleModerationDescription: '群成员受到限制时运行处理器。',
|
||||
behaviorCustom: '配置其他事件',
|
||||
behaviorCustomDescription: '添加路由,并从此适配器支持的全部事件中选择。',
|
||||
eventPattern: '事件',
|
||||
@@ -670,6 +668,36 @@ const zhHans = {
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
eventProcessor: {
|
||||
type: '事件处理器',
|
||||
description: '通过插件代码处理平台事件。',
|
||||
component: '插件组件',
|
||||
selectComponent: '选择事件处理器组件',
|
||||
unavailable: '组件不可用',
|
||||
noComponents: '尚未安装事件处理器组件。',
|
||||
installPlugin: '安装插件',
|
||||
loadError: '无法加载处理器详情。',
|
||||
refresh: '刷新',
|
||||
runs: '运行记录',
|
||||
noRuns: '暂无运行记录,绑定机器人事件后开始处理。',
|
||||
bindBot: '绑定机器人事件',
|
||||
trace: '日志与消息流向',
|
||||
selectRun: '选择一条运行记录查看详情。',
|
||||
input: '传入事件',
|
||||
destination: '投递目标',
|
||||
loadMore: '加载更多',
|
||||
activation: '安装插件,创建实例,再绑定机器人事件。',
|
||||
status_pending: '待执行',
|
||||
status_running: '运行中',
|
||||
status_completed: '已完成',
|
||||
status_failed: '失败',
|
||||
status_cancelled: '已取消',
|
||||
status_queued: '排队中',
|
||||
trace_run_completed: '运行完成',
|
||||
trace_run_failed: '运行失败',
|
||||
trace_tool_call_started: '开始执行动作',
|
||||
trace_tool_call_completed: '动作结果',
|
||||
},
|
||||
debugData: {
|
||||
title: '事件数据',
|
||||
form: '常用字段',
|
||||
@@ -701,7 +729,7 @@ const zhHans = {
|
||||
description: '创建可复用的处理器,并在机器人事件路由中使用',
|
||||
create: '创建处理器',
|
||||
editAgent: '编辑 Agent',
|
||||
selectFromSidebar: '从侧边栏选择一个 Agent 或 Pipeline',
|
||||
selectFromSidebar: '从侧边栏选择一个处理器',
|
||||
agentType: 'Agent',
|
||||
agentTypeDescription:
|
||||
'通过运行器处理消息、群成员、好友、反馈等平台事件。适合需要自主判断、调用工具或响应非消息事件的场景。',
|
||||
|
||||
@@ -474,6 +474,36 @@ const zhHant = {
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
eventProcessor: {
|
||||
type: '事件處理器',
|
||||
description: '透過外掛程式碼處理平台事件。',
|
||||
component: '外掛元件',
|
||||
selectComponent: '選擇事件處理器元件',
|
||||
unavailable: '元件無法使用',
|
||||
noComponents: '尚未安裝事件處理器元件。',
|
||||
installPlugin: '安裝外掛',
|
||||
loadError: '無法載入處理器詳情。',
|
||||
refresh: '重新整理',
|
||||
runs: '執行記錄',
|
||||
noRuns: '尚無執行記錄,綁定機器人事件後開始處理。',
|
||||
bindBot: '綁定機器人事件',
|
||||
trace: '日誌與訊息流向',
|
||||
selectRun: '選擇一筆執行記錄查看詳情。',
|
||||
input: '傳入事件',
|
||||
destination: '傳送目標',
|
||||
loadMore: '載入更多',
|
||||
activation: '安裝外掛、建立實例,再綁定機器人事件。',
|
||||
status_pending: '待執行',
|
||||
status_running: '執行中',
|
||||
status_completed: '已完成',
|
||||
status_failed: '失敗',
|
||||
status_cancelled: '已取消',
|
||||
status_queued: '排隊中',
|
||||
trace_run_completed: '執行完成',
|
||||
trace_run_failed: '執行失敗',
|
||||
trace_tool_call_started: '開始執行動作',
|
||||
trace_tool_call_completed: '動作結果',
|
||||
},
|
||||
debugData: {
|
||||
title: '事件資料',
|
||||
form: '常用欄位',
|
||||
@@ -505,7 +535,7 @@ const zhHant = {
|
||||
description: '建立可重用的處理器,並在機器人事件路由中使用',
|
||||
create: '建立處理器',
|
||||
editAgent: '編輯 Agent',
|
||||
selectFromSidebar: '從側邊欄選擇一個 Agent 或 Pipeline',
|
||||
selectFromSidebar: '從側邊欄選擇一個處理器',
|
||||
agentType: 'Agent',
|
||||
agentTypeDescription: '透過執行器處理訊息、群成員、好友、回饋等平台事件。',
|
||||
pipelineType: '流程線',
|
||||
|
||||
@@ -252,7 +252,7 @@ test.describe('frontend CRUD smoke flows', () => {
|
||||
|
||||
await expect(page).toHaveURL(/\/home\/agents$/);
|
||||
await expect(
|
||||
page.getByText('Select an Agent or Pipeline from the sidebar'),
|
||||
page.getByText('Select a processor from the sidebar'),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -1430,7 +1430,7 @@ test.describe('empty states', () => {
|
||||
|
||||
await page.goto('/home/agents');
|
||||
await expect(
|
||||
page.getByText('Select an Agent or Pipeline from the sidebar'),
|
||||
page.getByText('Select a processor from the sidebar'),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { installLangBotApiMocks } from './fixtures/langbot-api';
|
||||
|
||||
test('event processor shows isolated logs, paginates and scrolls expanded payloads', async ({
|
||||
page,
|
||||
}) => {
|
||||
await installLangBotApiMocks(page, { authenticated: true });
|
||||
const ref = 'event_processor:qa/welcome/default';
|
||||
const processor = {
|
||||
uuid: 'processor-qa',
|
||||
kind: 'event_processor',
|
||||
name: 'Welcome processor',
|
||||
component_ref: ref,
|
||||
supported_event_patterns: ['group.member_joined'],
|
||||
config: { runner: { id: ref }, runner_config: { [ref]: {} } },
|
||||
};
|
||||
const run = {
|
||||
run_id: 'run-one',
|
||||
status: 'completed',
|
||||
status_reason: 'stop',
|
||||
created_at: 1788000000,
|
||||
metadata: {
|
||||
event_type: 'group.member_joined',
|
||||
input_event: { member: { id: 'one' } },
|
||||
delivery: {
|
||||
reply_target: { target_type: 'group', target_id: 'test-group' },
|
||||
},
|
||||
},
|
||||
};
|
||||
const cursors: string[] = [];
|
||||
await page.route('**/api/v1/agents**', async (route) => {
|
||||
const url = new URL(route.request().url());
|
||||
let data: unknown;
|
||||
if (url.pathname.endsWith('/_/metadata')) {
|
||||
data = {
|
||||
kinds: [],
|
||||
event_processors: [
|
||||
{
|
||||
id: ref,
|
||||
label: { en_US: 'Welcome' },
|
||||
supported_event_patterns: ['group.member_joined'],
|
||||
config_schema: [],
|
||||
plugin_author: 'qa',
|
||||
plugin_name: 'welcome',
|
||||
},
|
||||
],
|
||||
};
|
||||
} else if (url.pathname.endsWith('/runs/run-one/events')) {
|
||||
cursors.push(url.searchParams.get('after_sequence') ?? '');
|
||||
data = {
|
||||
run,
|
||||
items: url.searchParams.has('after_sequence')
|
||||
? [
|
||||
{
|
||||
sequence: 101,
|
||||
type: 'processor.log',
|
||||
data: { level: 'info', text: 'Final log after pagination' },
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
sequence: 1,
|
||||
type: 'processor.log',
|
||||
data: { level: 'info', text: 'Member received' },
|
||||
},
|
||||
{
|
||||
sequence: 100,
|
||||
type: 'tool.call.completed',
|
||||
data: {
|
||||
result: Array.from(
|
||||
{ length: 100 },
|
||||
(_, i) => `Payload line ${i}`,
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
has_more: !url.searchParams.has('after_sequence'),
|
||||
next_cursor: url.searchParams.has('after_sequence') ? null : 100,
|
||||
};
|
||||
} else if (url.pathname.endsWith('/runs')) {
|
||||
data = { items: [run], has_more: false, next_cursor: null, total: 1 };
|
||||
} else if (url.pathname.endsWith('/processor-qa')) {
|
||||
data = { agent: processor };
|
||||
} else {
|
||||
data = { agents: [processor] };
|
||||
}
|
||||
await route.fulfill({ json: { code: 0, data } });
|
||||
});
|
||||
await page.goto('/home/agents?id=processor-qa');
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'Welcome processor' }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'Logs and message flow' }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByText('stop', { exact: true })).toHaveCount(0);
|
||||
await page
|
||||
.getByRole('button')
|
||||
.filter({ hasText: 'group.member_joined' })
|
||||
.click();
|
||||
await expect(
|
||||
page.getByText('Member received', { exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByText('Payload line 99', { exact: false }),
|
||||
).toBeHidden();
|
||||
await page.locator('summary').filter({ hasText: 'Action result' }).click();
|
||||
await page.getByRole('button', { name: 'Load more', exact: true }).click();
|
||||
await page.getByText('Final log after pagination').scrollIntoViewIfNeeded();
|
||||
await expect(page.getByText('Final log after pagination')).toBeInViewport();
|
||||
expect(cursors).toEqual(['', '100']);
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Load more', exact: true }),
|
||||
).toHaveCount(0);
|
||||
});
|
||||
Reference in New Issue
Block a user