mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-10 11:57:20 +00:00
feat(processors): refine plugin processor configuration and presentation
This commit is contained in:
@@ -122,12 +122,14 @@ status. It must not silently fall back to Agent or Pipeline.
|
||||
|
||||
Creation adds a third type next to Agent and Pipeline and asks only for basic
|
||||
instance information. Select the plugin component in the detail-page header.
|
||||
Keep component-defined configuration in the adjacent Plugin settings popover.
|
||||
Show component-defined configuration in the right pane, with Configuration and
|
||||
Logs tabs. Keep unsaved values when switching tabs, and open Logs after a debug
|
||||
run finishes. The component selector remains in the page header.
|
||||
If no component is installed, show a relevant plugin installation entry point;
|
||||
installing still does not create a binding.
|
||||
|
||||
The detail page shows event debugging on the left and logs on the right without
|
||||
view-switching tabs. A compact run list shows event type, time, status and known
|
||||
The detail page keeps event debugging on the left while the right pane switches
|
||||
between configuration and logs. A compact run list shows event type, time, status and known
|
||||
processing duration. Selecting a row shows that run's identity, input, logs,
|
||||
actions and outcome below. There is no shared timeline between unrelated runs.
|
||||
The additive `created_at_ms`, `started_at_ms`, and `finished_at_ms` fields retain
|
||||
|
||||
@@ -415,7 +415,7 @@ class AgentService:
|
||||
'uuid': new_uuid,
|
||||
'name': agent_data.get('name') or 'New Agent',
|
||||
'description': agent_data.get('description') or '',
|
||||
'emoji': agent_data.get('emoji') or '🤖',
|
||||
'emoji': agent_data.get('emoji') or ('🧩' if kind == AGENT_KIND_EVENT_PROCESSOR else '🤖'),
|
||||
'kind': kind,
|
||||
'component_ref': runner_id,
|
||||
'config': config,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { eventPatternLabel } from '@/app/home/components/event-patterns/event-pattern-groups';
|
||||
import { RefreshCw, Trash2, ScrollText } from 'lucide-react';
|
||||
import { RefreshCw, Trash2, ScrollText, Settings2 } from 'lucide-react';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import { toast } from 'sonner';
|
||||
import type {
|
||||
@@ -26,6 +26,7 @@ import EventProcessorTrace, {
|
||||
} from './components/EventProcessorTrace';
|
||||
import ProcessorRunList from './components/ProcessorRunList';
|
||||
import EventProcessorSettings from './components/EventProcessorSettings';
|
||||
import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent';
|
||||
|
||||
export default function EventProcessorDetailContent({
|
||||
agent,
|
||||
@@ -47,6 +48,7 @@ export default function EventProcessorDetailContent({
|
||||
onSaved: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [activeTab, setActiveTab] = useState('config');
|
||||
const [platformTools, setPlatformTools] = useState<AgentPlatformTool[]>([]);
|
||||
const toolLabels = Object.fromEntries(
|
||||
platformTools.map((tool) => [tool.name, extractI18nObject(tool.label)]),
|
||||
@@ -235,13 +237,11 @@ export default function EventProcessorDetailContent({
|
||||
}, [id, selected, eventCursor, events]);
|
||||
|
||||
async function save() {
|
||||
if (
|
||||
!canManage ||
|
||||
saving ||
|
||||
!component ||
|
||||
!((await validate.current?.()) ?? true)
|
||||
)
|
||||
if (!canManage || saving || !component) return false;
|
||||
if (!((await validate.current?.()) ?? true)) {
|
||||
setActiveTab('config');
|
||||
return false;
|
||||
}
|
||||
setSaving(true);
|
||||
try {
|
||||
await httpClient.updateAgent(id, {
|
||||
@@ -265,9 +265,126 @@ export default function EventProcessorDetailContent({
|
||||
}
|
||||
}
|
||||
|
||||
const logsContent = (
|
||||
<div className="flex h-full min-h-0 flex-col gap-3">
|
||||
<form
|
||||
id="event-processor-form"
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
void save();
|
||||
}}
|
||||
/>
|
||||
{failed && (
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription>
|
||||
{t('agents.eventProcessor.loadError')}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
<div className="flex shrink-0 items-center justify-between gap-2">
|
||||
<span className="text-sm font-medium">
|
||||
{t('agents.eventProcessor.runs')}{' '}
|
||||
<span className="text-muted-foreground">({runs.length})</span>
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
aria-label={t('agents.eventProcessor.refresh')}
|
||||
onClick={() => void refreshLatestRun()}
|
||||
>
|
||||
<RefreshCw className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
{runs.length > 0 && (
|
||||
<ProcessorRunList
|
||||
runs={runs}
|
||||
selectedId={selected?.run_id}
|
||||
onSelect={(run) => void openRun(run)}
|
||||
footer={
|
||||
cursor !== null ? (
|
||||
<Button
|
||||
className="w-full"
|
||||
variant="ghost"
|
||||
disabled={pagingRuns}
|
||||
onClick={() => void loadMoreRuns()}
|
||||
>
|
||||
{t('agents.eventProcessor.loadMore')}
|
||||
</Button>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<ScrollArea className="min-h-0 flex-1">
|
||||
<div className="space-y-2 pr-3">
|
||||
{!selected ? (
|
||||
<Alert>
|
||||
<AlertDescription>
|
||||
{loading
|
||||
? t('common.loading')
|
||||
: t('agents.eventProcessor.noRuns')}
|
||||
<Button asChild variant="link" className="h-auto px-0">
|
||||
<Link to="/home/bots">
|
||||
{t('agents.eventProcessor.bindBot')}
|
||||
</Link>
|
||||
</Button>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : (
|
||||
<>
|
||||
<div className="border-b pb-2">
|
||||
<p className="text-sm font-medium">
|
||||
{eventPatternLabel(selected.metadata.event_type ?? '', t)}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{new Date(selected.created_at * 1000).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
<Badge
|
||||
variant={
|
||||
selected.status === 'failed' ? 'destructive' : 'outline'
|
||||
}
|
||||
>
|
||||
{t(`agents.eventProcessor.status_${selected.status}`, {
|
||||
defaultValue: selected.status,
|
||||
})}
|
||||
</Badge>
|
||||
<ProcessorPayload
|
||||
title={t('agents.eventProcessor.input')}
|
||||
value={selected.metadata.input_event}
|
||||
/>
|
||||
{selected.metadata.delivery != null && (
|
||||
<ProcessorPayload
|
||||
title={t('agents.eventProcessor.destination')}
|
||||
value={selected.metadata.delivery}
|
||||
/>
|
||||
)}
|
||||
<EventProcessorTrace events={events} toolLabels={toolLabels} />
|
||||
{selected.status === 'failed' && selected.status_reason && (
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription className="break-words">
|
||||
{selected.status_reason}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
{eventCursor !== null && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
disabled={pagingEvents}
|
||||
onClick={() => void loadMoreEvents()}
|
||||
>
|
||||
{t('agents.eventProcessor.loadMore')}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<ProcessorDetailWorkbench
|
||||
title={`${agent.emoji || '⚡'} ${agent.name}`}
|
||||
title={`${agent.emoji || '🧩'} ${agent.name}`}
|
||||
titleAction={
|
||||
canManage ? <EntityTitleEditButton onClick={onEdit} /> : undefined
|
||||
}
|
||||
@@ -275,10 +392,10 @@ export default function EventProcessorDetailContent({
|
||||
<EventProcessorSettings
|
||||
components={components}
|
||||
value={componentRef}
|
||||
parameters={parameters}
|
||||
disabled={!canManage || saving || loading}
|
||||
onChange={(value) => {
|
||||
setComponentRef(value);
|
||||
setActiveTab('config');
|
||||
const descriptor = components.find((item) => item.id === value);
|
||||
setParameters(
|
||||
Object.fromEntries(
|
||||
@@ -289,10 +406,6 @@ export default function EventProcessorDetailContent({
|
||||
);
|
||||
validate.current = null;
|
||||
}}
|
||||
onParametersChange={setParameters}
|
||||
onValidate={(fn) => {
|
||||
validate.current = fn;
|
||||
}}
|
||||
/>
|
||||
}
|
||||
status={
|
||||
@@ -313,127 +426,51 @@ export default function EventProcessorDetailContent({
|
||||
</Button>
|
||||
) : undefined
|
||||
}
|
||||
configTitle={t('agents.eventProcessor.trace')}
|
||||
configIcon={<ScrollText className="size-4" />}
|
||||
configContent={
|
||||
<div className="flex h-full min-h-0 flex-col gap-3">
|
||||
<form
|
||||
id="event-processor-form"
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
void save();
|
||||
}}
|
||||
/>
|
||||
{failed && (
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription>
|
||||
{t('agents.eventProcessor.loadError')}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
<div className="flex shrink-0 items-center justify-between gap-2">
|
||||
<span className="text-sm font-medium">
|
||||
{t('agents.eventProcessor.runs')}{' '}
|
||||
<span className="text-muted-foreground">({runs.length})</span>
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
aria-label={t('agents.eventProcessor.refresh')}
|
||||
onClick={() => void refreshLatestRun()}
|
||||
>
|
||||
<RefreshCw className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
{runs.length > 0 && (
|
||||
<ProcessorRunList
|
||||
runs={runs}
|
||||
selectedId={selected?.run_id}
|
||||
onSelect={(run) => void openRun(run)}
|
||||
footer={
|
||||
cursor !== null ? (
|
||||
<Button
|
||||
className="w-full"
|
||||
variant="ghost"
|
||||
disabled={pagingRuns}
|
||||
onClick={() => void loadMoreRuns()}
|
||||
>
|
||||
{t('agents.eventProcessor.loadMore')}
|
||||
</Button>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<ScrollArea className="min-h-0 flex-1">
|
||||
<div className="space-y-2 pr-3">
|
||||
{!selected ? (
|
||||
<Alert>
|
||||
<AlertDescription>
|
||||
{loading
|
||||
? t('common.loading')
|
||||
: t('agents.eventProcessor.noRuns')}
|
||||
<Button asChild variant="link" className="h-auto px-0">
|
||||
<Link to="/home/bots">
|
||||
{t('agents.eventProcessor.bindBot')}
|
||||
</Link>
|
||||
</Button>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : (
|
||||
<>
|
||||
<div className="border-b pb-2">
|
||||
<p className="text-sm font-medium">
|
||||
{eventPatternLabel(selected.metadata.event_type ?? '', t)}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{new Date(selected.created_at * 1000).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
<Badge
|
||||
variant={
|
||||
selected.status === 'failed' ? 'destructive' : 'outline'
|
||||
}
|
||||
>
|
||||
{t(`agents.eventProcessor.status_${selected.status}`, {
|
||||
defaultValue: selected.status,
|
||||
})}
|
||||
</Badge>
|
||||
<ProcessorPayload
|
||||
title={t('agents.eventProcessor.input')}
|
||||
value={selected.metadata.input_event}
|
||||
/>
|
||||
{selected.metadata.delivery != null && (
|
||||
<ProcessorPayload
|
||||
title={t('agents.eventProcessor.destination')}
|
||||
value={selected.metadata.delivery}
|
||||
configTitle={t('agents.eventProcessor.type')}
|
||||
configTabs={{
|
||||
value: activeTab,
|
||||
onValueChange: setActiveTab,
|
||||
items: [
|
||||
{
|
||||
value: 'config',
|
||||
label: t('agents.eventProcessor.configTab'),
|
||||
icon: <Settings2 className="size-4" />,
|
||||
content: (
|
||||
<div className="h-full overflow-y-auto">
|
||||
{!component ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('agents.eventProcessor.selectComponent')}
|
||||
</p>
|
||||
) : component.config_schema.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('agents.eventProcessor.noSettings')}
|
||||
</p>
|
||||
) : (
|
||||
<fieldset disabled={!canManage || saving}>
|
||||
<DynamicFormComponent
|
||||
key={componentRef}
|
||||
itemConfigList={component.config_schema}
|
||||
initialValues={parameters}
|
||||
onSubmit={(values) =>
|
||||
setParameters(values as Record<string, unknown>)
|
||||
}
|
||||
onValidate={(fn) => {
|
||||
validate.current = fn;
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<EventProcessorTrace
|
||||
events={events}
|
||||
toolLabels={toolLabels}
|
||||
/>
|
||||
{selected.status === 'failed' && selected.status_reason && (
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription className="break-words">
|
||||
{selected.status_reason}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
{eventCursor !== null && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
disabled={pagingEvents}
|
||||
onClick={() => void loadMoreEvents()}
|
||||
>
|
||||
{t('agents.eventProcessor.loadMore')}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
}
|
||||
</fieldset>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
value: 'logs',
|
||||
label: t('agents.eventProcessor.logsTab'),
|
||||
icon: <ScrollText className="size-4" />,
|
||||
content: logsContent,
|
||||
},
|
||||
],
|
||||
}}
|
||||
debugTitle={canOperate ? t('agents.debugTab') : undefined}
|
||||
debugDescription={t('agents.eventProcessor.debugNotice')}
|
||||
debugContent={
|
||||
@@ -452,6 +489,7 @@ export default function EventProcessorDetailContent({
|
||||
hasUnsavedChanges={dirty}
|
||||
beforeRun={save}
|
||||
onRunFinished={() => {
|
||||
setActiveTab('logs');
|
||||
void refreshLatestRun();
|
||||
}}
|
||||
supportedEventPatterns={component.supported_event_patterns}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toast } from 'sonner';
|
||||
import { Bot, Workflow, FileCode2 } from 'lucide-react';
|
||||
import { Bot, Workflow, Puzzle } from 'lucide-react';
|
||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||
import { AgentKind } from '@/app/infra/entities/api';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -52,12 +52,12 @@ export default function AgentCreateContent({
|
||||
|
||||
function handleKindChange(nextKind: AgentKind) {
|
||||
const previousDefaultEmoji =
|
||||
kind === 'pipeline' ? '⚙️' : kind === 'event_processor' ? '⚡' : '🤖';
|
||||
kind === 'pipeline' ? '⚙️' : kind === 'event_processor' ? '🧩' : '🤖';
|
||||
const nextDefaultEmoji =
|
||||
nextKind === 'pipeline'
|
||||
? '⚙️'
|
||||
: nextKind === 'event_processor'
|
||||
? '⚡'
|
||||
? '🧩'
|
||||
: '🤖';
|
||||
setKind(nextKind);
|
||||
const currentEmoji = form.getValues('emoji');
|
||||
@@ -72,7 +72,13 @@ export default function AgentCreateContent({
|
||||
kind,
|
||||
name: values.name,
|
||||
description: values.description ?? '',
|
||||
emoji: values.emoji || (kind === 'pipeline' ? '⚙️' : '🤖'),
|
||||
emoji:
|
||||
values.emoji ||
|
||||
(kind === 'pipeline'
|
||||
? '⚙️'
|
||||
: kind === 'event_processor'
|
||||
? '🧩'
|
||||
: '🤖'),
|
||||
})
|
||||
.then((resp) => {
|
||||
toast.success(t('agents.createSuccess'));
|
||||
@@ -98,7 +104,7 @@ export default function AgentCreateContent({
|
||||
},
|
||||
{
|
||||
kind: 'event_processor' as const,
|
||||
icon: FileCode2,
|
||||
icon: Puzzle,
|
||||
title: t('agents.eventProcessor.type'),
|
||||
description: t('agents.eventProcessor.description'),
|
||||
},
|
||||
@@ -107,13 +113,19 @@ export default function AgentCreateContent({
|
||||
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>
|
||||
<h1 className="text-xl font-semibold">
|
||||
{t('agents.eventProcessor.createPageTitle')}
|
||||
</h1>
|
||||
<Button
|
||||
type="submit"
|
||||
form="agent-create-form"
|
||||
disabled={form.formState.isSubmitting}
|
||||
>
|
||||
{t('common.submit')}
|
||||
{t(
|
||||
kind === 'event_processor'
|
||||
? 'agents.eventProcessor.create'
|
||||
: 'common.submit',
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -396,19 +396,15 @@ export default function AgentDebugPanel({
|
||||
</p>
|
||||
</div>
|
||||
{entries.length === 0 ? (
|
||||
<Alert className="my-4 bg-muted/20">
|
||||
<CircleHelp className="size-4" />
|
||||
<AlertTitle>
|
||||
{t(processor ? 'agents.debugTab' : 'agents.debugEmptyTitle')}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
{t(
|
||||
processor
|
||||
? 'agents.eventProcessor.debugDescription'
|
||||
: 'agents.debugEmptyTranscript',
|
||||
)}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
!processor && (
|
||||
<Alert className="my-4 bg-muted/20">
|
||||
<CircleHelp className="size-4" />
|
||||
<AlertTitle>{t('agents.debugEmptyTitle')}</AlertTitle>
|
||||
<AlertDescription>
|
||||
{t('agents.debugEmptyTranscript')}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{entries
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { Settings2, Puzzle } from 'lucide-react';
|
||||
import {
|
||||
Popover,
|
||||
PopoverTrigger,
|
||||
PopoverContent,
|
||||
} from '@/components/ui/popover';
|
||||
import { Puzzle } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
import type { EventProcessorDescriptor } from '@/app/infra/entities/api';
|
||||
@@ -19,7 +13,6 @@ import {
|
||||
} from '@/components/ui/select';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent';
|
||||
|
||||
function ProcessorComponentContent({
|
||||
component,
|
||||
@@ -72,48 +65,25 @@ function ProcessorComponentContent({
|
||||
export default function EventProcessorSettings({
|
||||
components,
|
||||
value,
|
||||
parameters,
|
||||
onChange,
|
||||
onParametersChange,
|
||||
onValidate,
|
||||
disabled = false,
|
||||
}: {
|
||||
components: EventProcessorDescriptor[];
|
||||
value: string;
|
||||
parameters: Record<string, unknown>;
|
||||
onChange: (value: string) => void;
|
||||
onParametersChange: (value: Record<string, unknown>) => void;
|
||||
onValidate?: (validate: () => Promise<boolean>) => void;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const selected = components.find((item) => item.id === value);
|
||||
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||
return (
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<Label className="sr-only" htmlFor="event-processor-component">
|
||||
{t('agents.eventProcessor.component')}
|
||||
</Label>
|
||||
<Select
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
onValueChange={(next) => {
|
||||
onChange(next);
|
||||
const component = components.find((item) => item.id === next);
|
||||
setSettingsOpen(
|
||||
Boolean(
|
||||
component?.config_schema.some(
|
||||
(field) =>
|
||||
field.required &&
|
||||
(field.default == null || field.default === ''),
|
||||
),
|
||||
),
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Select value={value} disabled={disabled} onValueChange={onChange}>
|
||||
<SelectTrigger
|
||||
id="event-processor-component"
|
||||
className="w-[22rem] max-w-[calc(100vw-8rem)] bg-[#ffffff] dark:bg-[#2a2a2e]"
|
||||
className="w-[13.2rem] max-w-[calc(100vw-8rem)] bg-[#ffffff] dark:bg-[#2a2a2e]"
|
||||
>
|
||||
{selected ? (
|
||||
<ProcessorComponentContent component={selected} />
|
||||
@@ -153,44 +123,6 @@ export default function EventProcessorSettings({
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{selected && selected.config_schema.length > 0 && (
|
||||
<Popover open={settingsOpen} onOpenChange={setSettingsOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
disabled={disabled}
|
||||
aria-label={t('agents.eventProcessor.pluginSettings')}
|
||||
title={t('agents.eventProcessor.pluginSettings')}
|
||||
>
|
||||
<Settings2 className="size-4" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
align="start"
|
||||
className="max-h-[70vh] overflow-y-auto space-y-3"
|
||||
>
|
||||
<p className="text-sm font-medium">
|
||||
{t('agents.eventProcessor.pluginSettings')}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t('agents.eventProcessor.pluginSettingsDescription')}
|
||||
</p>
|
||||
<fieldset disabled={disabled}>
|
||||
<DynamicFormComponent
|
||||
key={value}
|
||||
itemConfigList={selected.config_schema}
|
||||
initialValues={parameters}
|
||||
onSubmit={(values) =>
|
||||
onParametersChange(values as Record<string, unknown>)
|
||||
}
|
||||
onValidate={onValidate}
|
||||
/>
|
||||
</fieldset>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -373,18 +373,38 @@ function PipelineDiagram() {
|
||||
);
|
||||
}
|
||||
|
||||
export default function ProcessorTypeDiagram({ kind }: { kind: AgentKind }) {
|
||||
function EventProcessorDiagram() {
|
||||
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">
|
||||
const codeLines = [
|
||||
<>
|
||||
<span className="text-[#8b5cf6]">@handler</span>
|
||||
</>,
|
||||
<>
|
||||
<span className="text-[#2288ee]">async def</span>{' '}
|
||||
<span className="text-[#19b8c9]">on_event</span>(event):
|
||||
</>,
|
||||
<>
|
||||
<span className="text-[#2288ee]">if</span> event.type =={' '}
|
||||
<span className="text-amber-500">"message"</span>:
|
||||
</>,
|
||||
<>
|
||||
<span className="text-[#2288ee]">await</span> event.process()
|
||||
</>,
|
||||
<>
|
||||
<span className="text-muted-foreground"># output → trace</span>
|
||||
</>,
|
||||
];
|
||||
|
||||
return (
|
||||
<div
|
||||
className="processor-diagram grid h-full w-full overflow-hidden bg-muted/20 lg:grid-cols-[minmax(0,1.05fr)_minmax(280px,0.95fr)]"
|
||||
data-testid="event-processor-diagram"
|
||||
>
|
||||
<div className="flex min-w-0 flex-col justify-center gap-5 p-8 lg:p-10">
|
||||
<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) => (
|
||||
{['input', 'processWithPlugin', 'trace'].map((step, index) => (
|
||||
<div
|
||||
key={step}
|
||||
className="flex items-center gap-3 rounded-lg border bg-background p-4"
|
||||
@@ -393,10 +413,52 @@ export default function ProcessorTypeDiagram({ kind }: { kind: AgentKind }) {
|
||||
<span>{t(`agents.eventProcessor.${step}`)}</span>
|
||||
</div>
|
||||
))}
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('agents.eventProcessor.activation')}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
<div
|
||||
className="relative hidden min-w-0 items-center border-l bg-background/45 p-8 lg:flex"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div className="absolute left-0 top-1/2 w-8 -translate-y-1/2 border-t border-dashed border-[#2288ee]/25" />
|
||||
|
||||
<div className="relative w-full rounded-xl border bg-card">
|
||||
<div className="flex h-11 items-center gap-2 border-b px-4">
|
||||
<span className="font-mono text-[11px] text-muted-foreground">
|
||||
processor.py
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 px-4 py-5 font-mono text-[12px] leading-5">
|
||||
{codeLines.map((line, index) => (
|
||||
<div key={index} className="flex min-w-0 gap-3">
|
||||
<span className="w-4 shrink-0 select-none text-right text-muted-foreground/45">
|
||||
{index + 1}
|
||||
</span>
|
||||
<code
|
||||
className={
|
||||
index === 2 || index === 3
|
||||
? 'min-w-0 pl-3 text-foreground/80'
|
||||
: 'min-w-0 text-foreground/80'
|
||||
}
|
||||
>
|
||||
{line}
|
||||
</code>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mx-4 mb-4 border-t pt-3">
|
||||
<span className="font-mono text-[11px] text-muted-foreground">
|
||||
event → plugin → log
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ProcessorTypeDiagram({ kind }: { kind: AgentKind }) {
|
||||
if (kind === 'event_processor') return <EventProcessorDiagram />;
|
||||
return kind === 'agent' ? <AgentDiagram /> : <PipelineDiagram />;
|
||||
}
|
||||
|
||||
@@ -37,7 +37,17 @@ interface ProcessorDetailWorkbenchProps {
|
||||
isSaving: boolean;
|
||||
configTitle: string;
|
||||
configIcon?: ReactNode;
|
||||
configContent: ReactNode;
|
||||
configContent?: ReactNode;
|
||||
configTabs?: {
|
||||
value: string;
|
||||
onValueChange: (value: string) => void;
|
||||
items: {
|
||||
value: string;
|
||||
label: string;
|
||||
icon?: ReactNode;
|
||||
content: ReactNode;
|
||||
}[];
|
||||
};
|
||||
debugTitle?: string;
|
||||
debugDescription?: string;
|
||||
debugContent?: ReactNode;
|
||||
@@ -63,6 +73,7 @@ export default function ProcessorDetailWorkbench({
|
||||
configTitle,
|
||||
configIcon,
|
||||
configContent,
|
||||
configTabs,
|
||||
debugTitle,
|
||||
debugDescription,
|
||||
debugContent,
|
||||
@@ -77,6 +88,52 @@ export default function ProcessorDetailWorkbench({
|
||||
);
|
||||
const hasDebug = Boolean(debugTitle && debugContent);
|
||||
|
||||
const configPanel = (
|
||||
<Card
|
||||
role="region"
|
||||
aria-label={configTitle}
|
||||
className="h-full min-h-[36rem] min-w-0 gap-0 overflow-hidden py-0 lg:min-h-0"
|
||||
>
|
||||
<CardHeader className="flex h-12 shrink-0 flex-row items-center gap-2 border-b px-4 font-medium [.border-b]:pb-0">
|
||||
{configTabs ? (
|
||||
<TabsList aria-label={configTitle}>
|
||||
{configTabs.items.map((tab) => (
|
||||
<TabsTrigger key={tab.value} value={tab.value}>
|
||||
{tab.icon}
|
||||
{tab.label}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
) : (
|
||||
<>
|
||||
{configIcon ?? <Settings className="size-4" />}
|
||||
<span className="truncate">{configTitle}</span>
|
||||
</>
|
||||
)}
|
||||
{isDirty && (
|
||||
<span className="ml-auto flex items-center gap-1.5 text-xs text-amber-600 dark:text-amber-400">
|
||||
<span className="size-1.5 rounded-full bg-amber-500" />
|
||||
{unsavedLabel}
|
||||
</span>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="min-h-0 min-w-0 flex-1 overflow-hidden p-4">
|
||||
{configTabs
|
||||
? configTabs.items.map((tab) => (
|
||||
<TabsContent
|
||||
key={tab.value}
|
||||
value={tab.value}
|
||||
forceMount
|
||||
className="m-0 h-full min-h-0 data-[state=inactive]:hidden"
|
||||
>
|
||||
{tab.content}
|
||||
</TabsContent>
|
||||
))
|
||||
: configContent}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
value={activeView}
|
||||
@@ -246,25 +303,17 @@ export default function ProcessorDetailWorkbench({
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Card
|
||||
role="region"
|
||||
aria-label={configTitle}
|
||||
className="min-h-[36rem] min-w-0 gap-0 overflow-hidden py-0 lg:min-h-0"
|
||||
>
|
||||
<CardHeader className="flex h-12 shrink-0 flex-row items-center gap-2 border-b px-4 font-medium [.border-b]:pb-0">
|
||||
{configIcon ?? <Settings className="size-4" />}
|
||||
<span className="truncate">{configTitle}</span>
|
||||
{isDirty && (
|
||||
<span className="ml-auto flex items-center gap-1.5 text-xs text-amber-600 dark:text-amber-400">
|
||||
<span className="size-1.5 rounded-full bg-amber-500" />
|
||||
{unsavedLabel}
|
||||
</span>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="min-h-0 min-w-0 flex-1 overflow-hidden p-4">
|
||||
{configContent}
|
||||
</CardContent>
|
||||
</Card>
|
||||
{configTabs ? (
|
||||
<Tabs
|
||||
value={configTabs.value}
|
||||
onValueChange={configTabs.onValueChange}
|
||||
className="min-h-0 min-w-0"
|
||||
>
|
||||
{configPanel}
|
||||
</Tabs>
|
||||
) : (
|
||||
configPanel
|
||||
)}
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
@@ -17,6 +17,7 @@ interface EmojiPickerProps {
|
||||
const EMOJI_CATEGORIES = {
|
||||
common: [
|
||||
'⚙️',
|
||||
'🧩',
|
||||
'📚',
|
||||
'🔗',
|
||||
'📁',
|
||||
|
||||
@@ -704,6 +704,11 @@ const enUS = {
|
||||
},
|
||||
agents: {
|
||||
eventProcessor: {
|
||||
configTab: 'Configuration',
|
||||
logsTab: 'Logs',
|
||||
noSettings: 'This plugin processor requires no configuration.',
|
||||
createPageTitle: 'Create event processor',
|
||||
processWithPlugin: 'Process with plugin code',
|
||||
pluginSettings: 'Plugin settings',
|
||||
pluginSettingsDescription: 'Parameters declared by this plugin.',
|
||||
selectToDebug: 'Select a plugin above to start debugging.',
|
||||
@@ -714,12 +719,14 @@ const enUS = {
|
||||
debugNotice:
|
||||
'The plugin processes a test event. Platform actions use Mock and do not send real messages; other tools run as configured.',
|
||||
|
||||
type: 'Event processor',
|
||||
description: 'Process platform events using plugin code.',
|
||||
component: 'Plugin component',
|
||||
selectComponent: 'Select an event processor',
|
||||
create: 'Create plugin processor',
|
||||
type: 'Plugin processor',
|
||||
description:
|
||||
'Handle events with code and processing logic provided by a plugin.',
|
||||
component: 'Plugin processor',
|
||||
selectComponent: 'Select a plugin processor',
|
||||
unavailable: 'Component unavailable',
|
||||
noComponents: 'No event processor components installed.',
|
||||
noComponents: 'No plugins providing processors are installed.',
|
||||
installPlugin: 'Install a plugin',
|
||||
loadError: 'Unable to load processor details.',
|
||||
refresh: 'Refresh',
|
||||
@@ -787,7 +794,7 @@ const enUS = {
|
||||
groupByKind: 'Group by type',
|
||||
groupByKindShort: 'Group',
|
||||
pipelineTypeDescription:
|
||||
'A classic “receive a message, ask AI, reply to the user” flow with common configuration options. Handles message events only and works best for clear, predictable processes.',
|
||||
'Follow a fixed flow: receive a message, call AI, and reply to the user, with configurable knowledge bases and plugins. Handles message events only, for tasks with clear steps and control over processing.',
|
||||
allEvents: 'Supports all events',
|
||||
messageEventsOnly: 'Message events only',
|
||||
chooseType: 'Choose how it works',
|
||||
|
||||
@@ -507,6 +507,11 @@ const esES = {
|
||||
},
|
||||
agents: {
|
||||
eventProcessor: {
|
||||
configTab: 'Configuración',
|
||||
logsTab: 'Registros',
|
||||
noSettings: 'Este procesador de plugin no requiere configuración.',
|
||||
createPageTitle: 'Crear procesador de eventos',
|
||||
processWithPlugin: 'Procesar con código del plugin',
|
||||
pluginSettings: 'Ajustes del plugin',
|
||||
pluginSettingsDescription: 'Parámetros definidos por este plugin.',
|
||||
selectToDebug: 'Selecciona un plugin arriba para iniciar la depuración.',
|
||||
@@ -517,12 +522,14 @@ const esES = {
|
||||
debugNotice:
|
||||
'El plugin procesa un evento de prueba. Las acciones de plataforma usan Mock y no envían mensajes reales; las demás herramientas se ejecutan según su configuración.',
|
||||
|
||||
type: 'Procesador de eventos',
|
||||
description: 'Procesa eventos con código del plugin.',
|
||||
component: 'Componente del plugin',
|
||||
selectComponent: 'Seleccionar un procesador',
|
||||
create: 'Crear procesador de plugin',
|
||||
type: 'Procesador de plugin',
|
||||
description:
|
||||
'Procesa eventos con código y lógica definidos por un plugin.',
|
||||
component: 'Procesador de plugin',
|
||||
selectComponent: 'Seleccionar un procesador de plugin',
|
||||
unavailable: 'Componente no disponible',
|
||||
noComponents: 'No hay componentes de eventos instalados.',
|
||||
noComponents: 'No hay plugins instalados que proporcionen procesadores.',
|
||||
installPlugin: 'Instalar un plugin',
|
||||
loadError: 'No se pudieron cargar los detalles.',
|
||||
refresh: 'Actualizar',
|
||||
@@ -593,7 +600,7 @@ const esES = {
|
||||
groupByKind: 'Agrupar por tipo',
|
||||
groupByKindShort: 'Agrupar',
|
||||
pipelineTypeDescription:
|
||||
'El flujo clásico de «recibir un mensaje, consultar a la IA y responder al usuario», con opciones de configuración habituales. Solo procesa eventos de mensaje y es ideal para procesos claros y predecibles.',
|
||||
'Sigue un flujo fijo: recibir un mensaje, consultar a la IA y responder al usuario, con bases de conocimiento y plugins configurables. Solo procesa eventos de mensaje, para tareas con pasos claros y control del proceso.',
|
||||
allEvents: 'Compatible con todos los eventos',
|
||||
messageEventsOnly: 'Solo eventos de mensaje',
|
||||
basicInfo: 'Información básica',
|
||||
|
||||
@@ -717,6 +717,11 @@ const jaJP = {
|
||||
},
|
||||
agents: {
|
||||
eventProcessor: {
|
||||
configTab: '設定',
|
||||
logsTab: 'ログ',
|
||||
noSettings: 'このプラグインプロセッサーに設定項目はありません。',
|
||||
createPageTitle: 'イベントプロセッサーを作成',
|
||||
processWithPlugin: 'プラグインコードで処理',
|
||||
pluginSettings: 'プラグイン設定',
|
||||
pluginSettingsDescription: 'このプラグインが定義するパラメーターです。',
|
||||
selectToDebug: '上でプラグインを選択してデバッグを開始してください。',
|
||||
@@ -727,12 +732,15 @@ const jaJP = {
|
||||
debugNotice:
|
||||
'プラグインはテストイベントを実際に処理します。返信や送信などは Mock を使用し、実際のメッセージは送信しません。他のツールは設定どおりに実行されます。',
|
||||
|
||||
type: 'イベントプロセッサー',
|
||||
description: 'プラグインのコードでイベントを処理します。',
|
||||
component: 'プラグインコンポーネント',
|
||||
selectComponent: 'イベントプロセッサーを選択',
|
||||
create: 'プラグインプロセッサーを作成',
|
||||
type: 'プラグインプロセッサー',
|
||||
description:
|
||||
'プラグインが提供するコードと処理ロジックでイベントを処理します。',
|
||||
component: 'プラグインプロセッサー',
|
||||
selectComponent: 'プラグインプロセッサーを選択',
|
||||
unavailable: 'コンポーネントを利用できません',
|
||||
noComponents: 'イベントプロセッサーがインストールされていません。',
|
||||
noComponents:
|
||||
'プロセッサーを提供するプラグインがインストールされていません。',
|
||||
installPlugin: 'プラグインをインストール',
|
||||
loadError: '詳細を読み込めません。',
|
||||
refresh: '更新',
|
||||
@@ -826,7 +834,7 @@ const jaJP = {
|
||||
groupByKind: 'タイプ別にグループ化',
|
||||
groupByKindShort: 'グループ',
|
||||
pipelineTypeDescription:
|
||||
'「メッセージを受信し、AIに問い合わせ、ユーザーへ返信する」という定番のフローに、よく使う設定機能を加えたものです。メッセージイベントのみを処理し、手順が明確で安定した実行が必要な場合に適しています。',
|
||||
'「メッセージ受信、AI呼び出し、ユーザーへの返信」の固定フローで動作し、ナレッジベースやプラグインを設定できます。メッセージイベントのみを処理し、手順が明確で処理の制御が必要な用途に適しています。',
|
||||
allEvents: 'すべてのイベントに対応',
|
||||
messageEventsOnly: 'メッセージイベントのみ',
|
||||
chooseType: '処理方法を選択',
|
||||
|
||||
@@ -504,6 +504,11 @@ const ruRU = {
|
||||
},
|
||||
agents: {
|
||||
eventProcessor: {
|
||||
configTab: 'Настройки',
|
||||
logsTab: 'Журнал',
|
||||
noSettings: 'Этот обработчик плагина не требует настройки.',
|
||||
createPageTitle: 'Создать обработчик событий',
|
||||
processWithPlugin: 'Обработка кодом плагина',
|
||||
pluginSettings: 'Настройки плагина',
|
||||
pluginSettingsDescription: 'Параметры, объявленные этим плагином.',
|
||||
selectToDebug: 'Выберите плагин выше, чтобы начать отладку.',
|
||||
@@ -514,12 +519,13 @@ const ruRU = {
|
||||
debugNotice:
|
||||
'Плагин обрабатывает тестовое событие. Действия платформы используют Mock и не отправляют реальные сообщения; остальные инструменты работают согласно настройкам.',
|
||||
|
||||
type: 'Обработчик событий',
|
||||
description: 'Обрабатывает события кодом плагина.',
|
||||
component: 'Компонент плагина',
|
||||
selectComponent: 'Выберите обработчик',
|
||||
create: 'Создать обработчик плагина',
|
||||
type: 'Обработчик плагина',
|
||||
description: 'Обрабатывает события с помощью кода и логики плагина.',
|
||||
component: 'Обработчик плагина',
|
||||
selectComponent: 'Выберите обработчик плагина',
|
||||
unavailable: 'Компонент недоступен',
|
||||
noComponents: 'Компоненты обработки событий не установлены.',
|
||||
noComponents: 'Плагины с обработчиками не установлены.',
|
||||
installPlugin: 'Установить плагин',
|
||||
loadError: 'Не удалось загрузить данные.',
|
||||
refresh: 'Обновить',
|
||||
@@ -589,7 +595,7 @@ const ruRU = {
|
||||
groupByKind: 'Группировать по типу',
|
||||
groupByKindShort: 'Группа',
|
||||
pipelineTypeDescription:
|
||||
'Классический процесс «получить сообщение, обратиться к AI, ответить пользователю» с основными настройками. Обрабатывает только события сообщений и подходит для четких, предсказуемых сценариев.',
|
||||
'Работает по заданному процессу: получить сообщение, вызвать AI и ответить пользователю. Поддерживает настройку баз знаний и плагинов; обрабатывает только события сообщений и подходит для задач с четкими шагами и контролем обработки.',
|
||||
allEvents: 'Поддерживает все события',
|
||||
messageEventsOnly: 'Только события сообщений',
|
||||
basicInfo: 'Основная информация',
|
||||
|
||||
@@ -491,6 +491,11 @@ const thTH = {
|
||||
},
|
||||
agents: {
|
||||
eventProcessor: {
|
||||
configTab: 'การตั้งค่า',
|
||||
logsTab: 'บันทึก',
|
||||
noSettings: 'ตัวประมวลผลปลั๊กอินนี้ไม่ต้องตั้งค่า',
|
||||
createPageTitle: 'สร้างตัวประมวลผลเหตุการณ์',
|
||||
processWithPlugin: 'ประมวลผลด้วยโค้ดปลั๊กอิน',
|
||||
pluginSettings: 'การตั้งค่าปลั๊กอิน',
|
||||
pluginSettingsDescription: 'พารามิเตอร์ที่ประกาศโดยปลั๊กอินนี้',
|
||||
selectToDebug: 'เลือกปลั๊กอินด้านบนเพื่อเริ่มแก้จุดบกพร่อง',
|
||||
@@ -501,12 +506,13 @@ const thTH = {
|
||||
debugNotice:
|
||||
'ปลั๊กอินประมวลผลเหตุการณ์ทดสอบจริง การตอบกลับและส่งข้อความใช้ Mock โดยไม่ส่งข้อความจริง เครื่องมืออื่นทำงานตามการตั้งค่า',
|
||||
|
||||
type: 'ตัวประมวลผลเหตุการณ์',
|
||||
description: 'ประมวลผลเหตุการณ์ด้วยโค้ดปลั๊กอิน',
|
||||
component: 'ส่วนประกอบปลั๊กอิน',
|
||||
selectComponent: 'เลือกตัวประมวลผลเหตุการณ์',
|
||||
create: 'สร้างตัวประมวลผลปลั๊กอิน',
|
||||
type: 'ตัวประมวลผลปลั๊กอิน',
|
||||
description: 'ประมวลผลเหตุการณ์ด้วยโค้ดและตรรกะที่ปลั๊กอินกำหนด',
|
||||
component: 'ตัวประมวลผลปลั๊กอิน',
|
||||
selectComponent: 'เลือกตัวประมวลผลปลั๊กอิน',
|
||||
unavailable: 'ส่วนประกอบไม่พร้อมใช้งาน',
|
||||
noComponents: 'ยังไม่ได้ติดตั้งส่วนประกอบประมวลผลเหตุการณ์',
|
||||
noComponents: 'ยังไม่ได้ติดตั้งปลั๊กอินที่มีตัวประมวลผล',
|
||||
installPlugin: 'ติดตั้งปลั๊กอิน',
|
||||
loadError: 'ไม่สามารถโหลดรายละเอียดได้',
|
||||
refresh: 'รีเฟรช',
|
||||
@@ -574,7 +580,7 @@ const thTH = {
|
||||
groupByKind: 'จัดกลุ่มตามประเภท',
|
||||
groupByKindShort: 'จัดกลุ่ม',
|
||||
pipelineTypeDescription:
|
||||
'โฟลว์มาตรฐาน “รับข้อความ ขอคำตอบจาก AI และตอบกลับผู้ใช้” พร้อมตัวเลือกการตั้งค่าที่ใช้บ่อย รองรับเฉพาะเหตุการณ์ข้อความและเหมาะกับงานที่มีขั้นตอนชัดเจนและคาดเดาได้',
|
||||
'ทำงานตามขั้นตอนที่กำหนด: รับข้อความ เรียก AI และตอบกลับผู้ใช้ พร้อมตั้งค่าฐานความรู้และปลั๊กอินได้ รองรับเฉพาะเหตุการณ์ข้อความ เหมาะกับงานที่มีขั้นตอนชัดเจนและต้องการควบคุมกระบวนการประมวลผล',
|
||||
allEvents: 'รองรับทุกเหตุการณ์',
|
||||
messageEventsOnly: 'เฉพาะเหตุการณ์ข้อความ',
|
||||
basicInfo: 'ข้อมูลพื้นฐาน',
|
||||
|
||||
@@ -500,6 +500,11 @@ const viVN = {
|
||||
},
|
||||
agents: {
|
||||
eventProcessor: {
|
||||
configTab: 'Cấu hình',
|
||||
logsTab: 'Nhật ký',
|
||||
noSettings: 'Bộ xử lý plugin này không cần cấu hình.',
|
||||
createPageTitle: 'Tạo bộ xử lý sự kiện',
|
||||
processWithPlugin: 'Xử lý bằng mã plugin',
|
||||
pluginSettings: 'Cài đặt plugin',
|
||||
pluginSettingsDescription: 'Tham số do plugin này khai báo.',
|
||||
selectToDebug: 'Chọn plugin ở trên để bắt đầu gỡ lỗi.',
|
||||
@@ -510,12 +515,13 @@ const viVN = {
|
||||
debugNotice:
|
||||
'Plugin xử lý sự kiện kiểm thử. Hành động nền tảng dùng Mock và không gửi tin nhắn thật; các công cụ khác chạy theo cấu hình.',
|
||||
|
||||
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',
|
||||
create: 'Tạo bộ xử lý plugin',
|
||||
type: 'Bộ xử lý plugin',
|
||||
description: 'Xử lý sự kiện bằng mã và logic do plugin cung cấp.',
|
||||
component: 'Bộ xử lý plugin',
|
||||
selectComponent: 'Chọn bộ xử lý plugin',
|
||||
unavailable: 'Thành phần không khả dụng',
|
||||
noComponents: 'Chưa cài thành phần xử lý sự kiện.',
|
||||
noComponents: 'Chưa cài plugin cung cấp bộ xử lý.',
|
||||
installPlugin: 'Cài plugin',
|
||||
loadError: 'Không thể tải chi tiết.',
|
||||
refresh: 'Làm mới',
|
||||
@@ -584,7 +590,7 @@ const viVN = {
|
||||
groupByKind: 'Nhóm theo loại',
|
||||
groupByKindShort: 'Nhóm',
|
||||
pipelineTypeDescription:
|
||||
'Quy trình quen thuộc “nhận tin nhắn, hỏi AI, trả lời người dùng” cùng các tùy chọn cấu hình thông dụng. Chỉ xử lý sự kiện tin nhắn và phù hợp với các quy trình rõ ràng, dễ dự đoán.',
|
||||
'Chạy theo quy trình cố định: nhận tin nhắn, gọi AI và trả lời người dùng, với cơ sở tri thức và plugin có thể cấu hình. Chỉ xử lý sự kiện tin nhắn, phù hợp với tác vụ có các bước rõ ràng và cần kiểm soát quá trình xử lý.',
|
||||
allEvents: 'Hỗ trợ tất cả sự kiện',
|
||||
messageEventsOnly: 'Chỉ sự kiện tin nhắn',
|
||||
basicInfo: 'Thông tin cơ bản',
|
||||
|
||||
@@ -669,6 +669,11 @@ const zhHans = {
|
||||
},
|
||||
agents: {
|
||||
eventProcessor: {
|
||||
configTab: '配置',
|
||||
logsTab: '日志',
|
||||
noSettings: '此插件处理器无需配置。',
|
||||
createPageTitle: '创建事件处理器',
|
||||
processWithPlugin: '插件代码处理',
|
||||
pluginSettings: '插件设置',
|
||||
pluginSettingsDescription: '由当前插件声明的参数。',
|
||||
selectToDebug: '请先在上方选择插件,再开始调试。',
|
||||
@@ -678,12 +683,13 @@ const zhHans = {
|
||||
debugNotice:
|
||||
'插件真实处理测试事件;回复、发送等平台动作使用 Mock,不发送真实消息。其他工具仍按实际配置执行。',
|
||||
|
||||
type: '事件处理器',
|
||||
description: '通过插件代码处理平台事件。',
|
||||
component: '插件组件',
|
||||
selectComponent: '选择事件处理器组件',
|
||||
create: '创建插件处理器',
|
||||
type: '插件处理器',
|
||||
description: '由插件中的代码处理事件,处理逻辑由插件实现。',
|
||||
component: '插件处理器',
|
||||
selectComponent: '选择插件处理器',
|
||||
unavailable: '组件不可用',
|
||||
noComponents: '尚未安装事件处理器组件。',
|
||||
noComponents: '尚未安装提供插件处理器的插件。',
|
||||
installPlugin: '安装插件',
|
||||
loadError: '无法加载处理器详情。',
|
||||
refresh: '刷新',
|
||||
@@ -751,7 +757,7 @@ const zhHans = {
|
||||
groupByKind: '按类型分组',
|
||||
groupByKindShort: '分组',
|
||||
pipelineTypeDescription:
|
||||
'流水线即为经典的“收到消息、请求AI、回复用户”流程,并辅以常用的配置功能。仅处理消息事件,适合步骤明确、需要稳定控制处理过程的场景。',
|
||||
'按“接收消息、调用 AI、回复用户”的固定流程运行,可配置知识库和插件扩展。仅处理消息事件,适合步骤明确、需要控制处理过程的场景。',
|
||||
allEvents: '支持全部事件',
|
||||
messageEventsOnly: '仅支持消息事件',
|
||||
chooseType: '选择处理方式',
|
||||
|
||||
@@ -475,6 +475,11 @@ const zhHant = {
|
||||
},
|
||||
agents: {
|
||||
eventProcessor: {
|
||||
configTab: '設定',
|
||||
logsTab: '日誌',
|
||||
noSettings: '此外掛處理器無需設定。',
|
||||
createPageTitle: '建立事件處理器',
|
||||
processWithPlugin: '外掛程式碼處理',
|
||||
pluginSettings: '外掛設定',
|
||||
pluginSettingsDescription: '由目前外掛宣告的參數。',
|
||||
selectToDebug: '請先在上方選擇外掛,再開始除錯。',
|
||||
@@ -484,12 +489,13 @@ const zhHant = {
|
||||
debugNotice:
|
||||
'外掛實際處理測試事件;回覆、傳送等平台動作使用 Mock,不傳送真實訊息。其他工具仍依實際設定執行。',
|
||||
|
||||
type: '事件處理器',
|
||||
description: '透過外掛程式碼處理平台事件。',
|
||||
component: '外掛元件',
|
||||
selectComponent: '選擇事件處理器元件',
|
||||
create: '建立外掛處理器',
|
||||
type: '外掛處理器',
|
||||
description: '由外掛中的程式碼處理事件,處理邏輯由外掛實作。',
|
||||
component: '外掛處理器',
|
||||
selectComponent: '選擇外掛處理器',
|
||||
unavailable: '元件無法使用',
|
||||
noComponents: '尚未安裝事件處理器元件。',
|
||||
noComponents: '尚未安裝提供外掛處理器的外掛。',
|
||||
installPlugin: '安裝外掛',
|
||||
loadError: '無法載入處理器詳情。',
|
||||
refresh: '重新整理',
|
||||
@@ -556,7 +562,7 @@ const zhHant = {
|
||||
groupByKind: '依類型分組',
|
||||
groupByKindShort: '分組',
|
||||
pipelineTypeDescription:
|
||||
'流程線即為經典的「收到訊息、請求 AI、回覆使用者」流程,並輔以常用的設定功能。僅處理訊息事件,適合步驟明確、需要穩定控制處理過程的場景。',
|
||||
'依「接收訊息、呼叫 AI、回覆使用者」的固定流程執行,可設定知識庫與外掛擴充。僅處理訊息事件,適合步驟明確、需要控制處理過程的場景。',
|
||||
allEvents: '支援全部事件',
|
||||
messageEventsOnly: '僅支援訊息事件',
|
||||
basicInfo: '基本資訊',
|
||||
|
||||
@@ -217,12 +217,14 @@ test('create first, select a plugin in the header, debug beside scrollable logs'
|
||||
await page.goto('/home/agents?id=new');
|
||||
await page.locator('[data-processor-kind="event_processor"]').click();
|
||||
await expect(
|
||||
page.getByRole('combobox', { name: 'Plugin component' }),
|
||||
page.getByRole('combobox', { name: 'Plugin processor' }),
|
||||
).toHaveCount(0);
|
||||
await page
|
||||
.getByRole('textbox', { name: 'Name', exact: false })
|
||||
.fill('Welcome processor');
|
||||
await page.getByRole('button', { name: 'Submit', exact: true }).click();
|
||||
await page
|
||||
.getByRole('button', { name: 'Create plugin processor', exact: true })
|
||||
.click();
|
||||
await expect(page).toHaveURL(/id=processor-qa/);
|
||||
expect(creations).toHaveLength(1);
|
||||
expect(creations[0]).toMatchObject({
|
||||
@@ -232,30 +234,42 @@ test('create first, select a plugin in the header, debug beside scrollable logs'
|
||||
expect(creations[0]).not.toHaveProperty('component_ref');
|
||||
expect(creations[0]).not.toHaveProperty('config');
|
||||
const panel = page.getByRole('region', { name: 'Event Debug' });
|
||||
const logs = page.getByRole('region', { name: 'Logs and message flow' });
|
||||
const logs = page.getByRole('region', { name: 'Plugin processor' });
|
||||
await expect(panel).toBeVisible();
|
||||
await expect(logs).toBeVisible();
|
||||
await expect(page.getByRole('tab')).toHaveCount(0);
|
||||
await expect(logs.getByRole('tab')).toHaveCount(2);
|
||||
await expect(
|
||||
logs.getByRole('tab', { name: 'Configuration', exact: true }),
|
||||
).toHaveAttribute('data-state', 'active');
|
||||
await expect(page.getByRole('dialog')).toHaveCount(0);
|
||||
await expect(panel.getByRole('button', { name: 'Run test' })).toHaveCount(0);
|
||||
await page.getByRole('combobox', { name: 'Plugin component' }).click();
|
||||
await page.getByRole('combobox', { name: 'Plugin processor' }).click();
|
||||
await page.getByRole('option').filter({ hasText: 'Welcome' }).click();
|
||||
await expect(
|
||||
panel.getByRole('combobox', { name: 'Event type' }),
|
||||
).toContainText('group.member_joined');
|
||||
await expect(page.getByText('Greeting', { exact: true })).toHaveCount(0);
|
||||
await page
|
||||
.getByRole('button', { name: 'Plugin settings', exact: true })
|
||||
.click();
|
||||
const settings = page.locator('[data-slot="popover-content"]');
|
||||
const settings = logs.getByRole('tabpanel', {
|
||||
name: 'Configuration',
|
||||
exact: true,
|
||||
});
|
||||
await expect(settings.getByText('Greeting *', { exact: true })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Plugin settings', exact: true }),
|
||||
).toHaveCount(0);
|
||||
await settings.getByRole('textbox').fill('Welcome');
|
||||
await page.keyboard.press('Escape');
|
||||
await logs.getByRole('tab', { name: 'Logs', exact: true }).click();
|
||||
await expect(settings).toBeHidden();
|
||||
await logs.getByRole('tab', { name: 'Configuration', exact: true }).click();
|
||||
await expect(settings.getByRole('textbox')).toHaveValue('Welcome');
|
||||
await panel
|
||||
.getByRole('textbox', { name: 'Member ID' })
|
||||
.fill('debug-member-42');
|
||||
await panel.getByRole('button', { name: 'Save and run' }).click();
|
||||
await expect(panel.getByText('Debug handler invoked once')).toBeVisible();
|
||||
expect(operations).toEqual(['save', 'debug']);
|
||||
await expect(
|
||||
logs.getByRole('tab', { name: 'Logs', exact: true }),
|
||||
).toHaveAttribute('data-state', 'active');
|
||||
expect(debugRequests).toHaveLength(1);
|
||||
expect(debugRequests[0]).toMatchObject({
|
||||
event_type: 'group.member_joined',
|
||||
|
||||
Reference in New Issue
Block a user