feat(agent): add event orchestration surface

This commit is contained in:
Junyan Qin
2026-06-23 23:23:09 +08:00
parent d7b97741e3
commit ed3598f8ac
35 changed files with 2983 additions and 142 deletions
@@ -0,0 +1,97 @@
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Button } from '@/components/ui/button';
import { httpClient } from '@/app/infra/http/HttpClient';
import { Agent } from '@/app/infra/entities/api';
import { useSidebarData } from '@/app/home/components/home-sidebar/SidebarDataContext';
import PipelineDetailContent from '@/app/home/pipelines/PipelineDetailContent';
import AgentCreateContent from './components/AgentCreateContent';
import AgentFormComponent from './components/AgentFormComponent';
export default function AgentDetailContent({ id }: { id: string }) {
const isCreateMode = id === 'new';
const navigate = useNavigate();
const { t } = useTranslation();
const { refreshPipelines, pipelines, setDetailEntityName } = useSidebarData();
const [agent, setAgent] = useState<Agent | null>(null);
const [loading, setLoading] = useState(!isCreateMode);
const [formDirty, setFormDirty] = useState(false);
useEffect(() => {
if (isCreateMode) {
setDetailEntityName(t('agents.create'));
return () => setDetailEntityName(null);
}
const sidebarItem = pipelines.find((p) => p.id === id);
setDetailEntityName(sidebarItem?.name ?? id);
return () => setDetailEntityName(null);
}, [id, isCreateMode, pipelines, setDetailEntityName, t]);
useEffect(() => {
if (isCreateMode) return;
let cancelled = false;
setLoading(true);
httpClient
.getAgent(id)
.then((resp) => {
if (!cancelled) setAgent(resp.agent);
})
.finally(() => {
if (!cancelled) setLoading(false);
});
return () => {
cancelled = true;
};
}, [id, isCreateMode]);
if (isCreateMode) {
return (
<AgentCreateContent
onCreated={(newAgentId) => {
refreshPipelines();
navigate(`/home/agents?id=${encodeURIComponent(newAgentId)}`);
}}
/>
);
}
if (loading || !agent) {
return (
<div className="flex h-full items-center justify-center text-muted-foreground">
{t('common.loading')}
</div>
);
}
if (agent.kind === 'pipeline') {
return <PipelineDetailContent id={id} routeBase="/home/agents" />;
}
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.editAgent')}</h1>
<Button type="submit" form="agent-form" disabled={!formDirty}>
{t('common.save')}
</Button>
</div>
<div className="flex-1 overflow-y-auto min-h-0">
<AgentFormComponent
agentId={id}
onFinish={() => {
refreshPipelines();
setFormDirty(false);
}}
onDeleted={() => {
refreshPipelines();
navigate('/home/agents');
}}
onDirtyChange={setFormDirty}
/>
</div>
</div>
);
}
@@ -0,0 +1,214 @@
import { useState } from 'react';
import type React 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 { httpClient } from '@/app/infra/http/HttpClient';
import { AgentKind } from '@/app/infra/entities/api';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import EmojiPicker from '@/components/ui/emoji-picker';
export default function AgentCreateContent({
onCreated,
}: {
onCreated: (agentId: string) => void;
}) {
const { t } = useTranslation();
const [kind, setKind] = useState<AgentKind>('agent');
const formSchema = z.object({
name: z.string().min(1, { message: t('agents.nameRequired') }),
description: z.string().optional(),
emoji: z.string().optional(),
});
type FormValues = z.infer<typeof formSchema>;
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
name: '',
description: '',
emoji: '🤖',
},
});
function handleKindChange(nextKind: AgentKind) {
setKind(nextKind);
if (!form.getValues('emoji')) {
form.setValue('emoji', nextKind === 'pipeline' ? '⚙️' : '🤖');
}
}
function handleSubmit(values: FormValues) {
httpClient
.createAgent({
kind,
name: values.name,
description: values.description ?? '',
emoji: values.emoji || (kind === 'pipeline' ? '⚙️' : '🤖'),
})
.then((resp) => {
toast.success(t('agents.createSuccess'));
onCreated(resp.uuid);
})
.catch((err) => {
toast.error(t('agents.createError') + err.msg);
});
}
const typeOptions: Array<{
kind: AgentKind;
icon: React.ElementType;
title: string;
description: string;
badge: string;
}> = [
{
kind: 'agent',
icon: Bot,
title: t('agents.agentOrchestration'),
description: t('agents.agentOrchestrationDescription'),
badge: t('agents.allEvents'),
},
{
kind: 'pipeline',
icon: Workflow,
title: t('agents.pipelineType'),
description: t('agents.pipelineTypeDescription'),
badge: t('agents.messageEventsOnly'),
},
];
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">
{t('common.submit')}
</Button>
</div>
<div className="flex-1 overflow-y-auto min-h-0">
<div className="mx-auto max-w-2xl space-y-6">
<div className="grid gap-3 sm:grid-cols-2">
{typeOptions.map((option) => {
const Icon = option.icon;
const selected = kind === option.kind;
return (
<button
key={option.kind}
type="button"
onClick={() => handleKindChange(option.kind)}
className={cn(
'rounded-lg border bg-card p-4 text-left transition-colors',
selected
? 'border-primary ring-2 ring-primary/20'
: 'hover:border-primary/60',
)}
>
<div className="flex items-start gap-3">
<Icon className="mt-0.5 size-5 text-blue-500" />
<div className="space-y-1">
<div className="font-medium">{option.title}</div>
<div className="text-xs text-muted-foreground">
{option.badge}
</div>
<p className="text-sm text-muted-foreground">
{option.description}
</p>
</div>
</div>
</button>
);
})}
</div>
<Card>
<CardHeader>
<CardTitle>{t('agents.basicInfo')}</CardTitle>
<CardDescription>
{t('agents.basicInfoDescription')}
</CardDescription>
</CardHeader>
<CardContent>
<Form {...form}>
<form
id="agent-create-form"
onSubmit={form.handleSubmit(handleSubmit)}
className="space-y-4"
>
<div className="flex gap-4 items-start">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>
{t('common.name')}
<span className="text-destructive">*</span>
</FormLabel>
<FormControl>
<Input {...field} value={field.value ?? ''} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="emoji"
render={({ field }) => (
<FormItem>
<FormLabel>{t('common.icon')}</FormLabel>
<FormControl>
<EmojiPicker
value={field.value}
onChange={field.onChange}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>{t('common.description')}</FormLabel>
<FormControl>
<Input {...field} value={field.value ?? ''} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</CardContent>
</Card>
</div>
</div>
</div>
);
}
@@ -0,0 +1,509 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import type React 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 { Brain, FileJson2, Info, Power, Trash2 } from 'lucide-react';
import { httpClient } from '@/app/infra/http/HttpClient';
import { Agent } from '@/app/infra/entities/api';
import {
PipelineConfigStage,
PipelineConfigTab,
} from '@/app/infra/entities/pipeline';
import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent';
import { extractI18nObject } from '@/i18n/I18nProvider';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
import { Switch } from '@/components/ui/switch';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import EmojiPicker from '@/components/ui/emoji-picker';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
interface AgentFormComponentProps {
agentId: string;
onFinish: () => void;
onDeleted: () => void;
onDirtyChange?: (dirty: boolean) => void;
}
interface SectionItem {
label: string;
name: 'basic' | 'runner' | 'events';
icon: React.ElementType;
}
export default function AgentFormComponent({
agentId,
onFinish,
onDeleted,
onDirtyChange,
}: AgentFormComponentProps) {
const { t } = useTranslation();
const [activeSection, setActiveSection] =
useState<SectionItem['name']>('basic');
const [runnerConfigSchema, setRunnerConfigSchema] =
useState<PipelineConfigTab | null>(null);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const formSchema = z.object({
basic: z.object({
name: z.string().min(1, { message: t('agents.nameRequired') }),
description: z.string().optional(),
emoji: z.string().optional(),
enabled: z.boolean().optional(),
}),
runner: z.record(z.string(), z.any()),
runner_config: z.record(z.string(), z.any()),
supported_event_patterns_text: z.string(),
});
type FormValues = z.infer<typeof formSchema>;
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
basic: {
name: '',
description: '',
emoji: '🤖',
enabled: true,
},
runner: {},
runner_config: {},
supported_event_patterns_text: '*',
},
});
const savedSnapshotRef = useRef('');
const initializedStagesRef = useRef<Set<string>>(new Set());
const watchedValues = form.watch();
const hasUnsavedChanges = useMemo(() => {
if (!savedSnapshotRef.current) return false;
return JSON.stringify(watchedValues) !== savedSnapshotRef.current;
}, [watchedValues]);
useEffect(() => {
onDirtyChange?.(hasUnsavedChanges);
}, [hasUnsavedChanges, onDirtyChange]);
useEffect(() => {
let cancelled = false;
Promise.all([httpClient.getAgentMetadata(), httpClient.getAgent(agentId)])
.then(([metadata, resp]) => {
if (cancelled) return;
setRunnerConfigSchema(metadata.runner_config ?? null);
const agent = resp.agent;
const config = (agent.config ?? {}) as Record<string, any>;
const loadedValues: FormValues = {
basic: {
name: agent.name ?? '',
description: agent.description ?? '',
emoji: agent.emoji || '🤖',
enabled: agent.enabled ?? true,
},
runner: (config.runner as Record<string, unknown>) ?? {},
runner_config:
(config.runner_config as Record<string, unknown>) ?? {},
supported_event_patterns_text: (
agent.supported_event_patterns ??
agent.capability?.supported_event_patterns ?? ['*']
).join('\n'),
};
form.reset(loadedValues);
savedSnapshotRef.current = JSON.stringify(loadedValues);
initializedStagesRef.current.clear();
})
.catch((err) => {
toast.error(t('agents.loadError') + err.msg);
});
return () => {
cancelled = true;
};
}, [agentId, form, t]);
const sections: SectionItem[] = [
{ label: t('agents.basicInfo'), name: 'basic', icon: Info },
{ label: t('agents.runnerSettings'), name: 'runner', icon: Brain },
{ label: t('agents.eventCapability'), name: 'events', icon: FileJson2 },
];
const currentRunner = (form.watch('runner') as Record<string, any>)?.id;
function updateSnapshotIfInitial(stageKey: string) {
if (!initializedStagesRef.current.has(stageKey)) {
initializedStagesRef.current.add(stageKey);
if (!hasUnsavedChanges) {
savedSnapshotRef.current = JSON.stringify(form.getValues());
}
}
}
function handleDynamicFormEmit(
formName: 'runner' | 'runner_config',
stageName: string,
values: object,
) {
if (formName === 'runner') {
form.setValue('runner', values, { shouldDirty: true });
updateSnapshotIfInitial(`runner.${stageName}`);
return;
}
const currentRunnerConfigs =
(form.getValues('runner_config') as Record<string, unknown>) || {};
form.setValue(
'runner_config',
{
...currentRunnerConfigs,
[stageName]: values,
},
{ shouldDirty: true },
);
updateSnapshotIfInitial(`runner_config.${stageName}`);
}
function renderDynamicStage(stage: PipelineConfigStage) {
const isRunnerSelector = stage.name === 'runner';
if (!isRunnerSelector && stage.name !== currentRunner) return null;
const initialValues = isRunnerSelector
? (form.watch('runner') as Record<string, unknown>) || {}
: ((form.watch('runner_config') as Record<string, any>) || {})[
stage.name
] || {};
return (
<Card key={stage.name}>
<CardHeader>
<CardTitle>{extractI18nObject(stage.label)}</CardTitle>
{stage.description && (
<CardDescription>
{extractI18nObject(stage.description)}
</CardDescription>
)}
</CardHeader>
<CardContent className="space-y-6">
<DynamicFormComponent
itemConfigList={stage.config}
initialValues={initialValues}
onSubmit={(values) =>
handleDynamicFormEmit(
isRunnerSelector ? 'runner' : 'runner_config',
stage.name,
values,
)
}
/>
</CardContent>
</Card>
);
}
function normalizeEventPatterns(value: string): string[] {
const patterns = value
.split(/[\n,]/)
.map((item) => item.trim())
.filter(Boolean);
return patterns.length > 0 ? patterns : ['*'];
}
function handleSubmit(values: FormValues) {
const runner = values.runner || {};
const agent: Partial<Agent> = {
name: values.basic.name,
description: values.basic.description ?? '',
emoji: values.basic.emoji,
enabled: values.basic.enabled ?? true,
component_ref: (runner.id as string) || null,
supported_event_patterns: normalizeEventPatterns(
values.supported_event_patterns_text,
),
config: {
runner,
runner_config: values.runner_config ?? {},
},
};
httpClient
.updateAgent(agentId, agent)
.then(() => {
const snapshotValues = form.getValues();
savedSnapshotRef.current = JSON.stringify(snapshotValues);
onFinish();
toast.success(t('agents.saveSuccess'));
})
.catch((err) => {
toast.error(t('agents.saveError') + err.msg);
});
}
function confirmDelete() {
httpClient
.deleteAgent(agentId)
.then(() => {
toast.success(t('agents.deleteSuccess'));
setShowDeleteConfirm(false);
onDeleted();
})
.catch((err) => {
toast.error(t('agents.deleteError') + err.msg);
});
}
return (
<>
<div className="h-full p-0 flex flex-col">
<Form {...form}>
<form
id="agent-form"
onSubmit={form.handleSubmit(handleSubmit)}
className="h-full flex flex-col flex-1 min-h-0 mb-2"
>
<div className="flex-1 flex flex-col md:flex-row min-h-0">
<nav className="shrink-0 mb-4 md:mb-0 md:w-44 md:pr-4 md:mr-4 md:border-r overflow-x-auto md:overflow-x-visible md:overflow-y-auto">
<ul className="flex md:flex-col gap-1 md:space-y-1">
{sections.map((section) => {
const Icon = section.icon;
return (
<li key={section.name}>
<button
type="button"
onClick={() => setActiveSection(section.name)}
className={cn(
'w-full flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-colors text-left cursor-pointer whitespace-nowrap',
activeSection === section.name
? 'bg-accent text-accent-foreground'
: 'text-muted-foreground hover:bg-muted hover:text-foreground',
)}
>
<Icon className="size-4 shrink-0" />
{section.label}
</button>
</li>
);
})}
</ul>
</nav>
<div className="flex-1 overflow-y-auto min-h-0">
{activeSection === 'basic' && (
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>{t('agents.basicInfo')}</CardTitle>
<CardDescription>
{t('agents.basicInfoDescription')}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex gap-4 items-start">
<FormField
control={form.control}
name="basic.name"
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>
{t('common.name')}
<span className="text-destructive">*</span>
</FormLabel>
<FormControl>
<Input {...field} value={field.value ?? ''} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="basic.emoji"
render={({ field }) => (
<FormItem>
<FormLabel>{t('common.icon')}</FormLabel>
<FormControl>
<EmojiPicker
value={field.value}
onChange={field.onChange}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="basic.description"
render={({ field }) => (
<FormItem>
<FormLabel>{t('common.description')}</FormLabel>
<FormControl>
<Input {...field} value={field.value ?? ''} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="basic.enabled"
render={({ field }) => (
<FormItem className="flex items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<FormLabel className="flex items-center gap-2">
<Power className="size-4" />
{t('agents.enabled')}
</FormLabel>
<FormDescription>
{t('agents.enabledDescription')}
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value ?? true}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
</CardContent>
</Card>
<Card className="border-destructive/50">
<CardHeader>
<CardTitle className="text-destructive">
{t('agents.dangerZone')}
</CardTitle>
<CardDescription>
{t('agents.dangerZoneDescription')}
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
<div className="space-y-1">
<p className="text-sm font-medium">
{t('agents.deleteAgentAction')}
</p>
<p className="text-sm text-muted-foreground">
{t('agents.deleteAgentHint')}
</p>
</div>
<Button
type="button"
variant="destructive"
size="sm"
onClick={() => setShowDeleteConfirm(true)}
>
<Trash2 className="size-4 mr-1.5" />
{t('common.delete')}
</Button>
</div>
</CardContent>
</Card>
</div>
)}
{activeSection === 'runner' && (
<div className="space-y-6">
{runnerConfigSchema?.stages.map((stage) =>
renderDynamicStage(stage),
)}
{!runnerConfigSchema && (
<Card>
<CardHeader>
<CardTitle>{t('agents.runnerSettings')}</CardTitle>
<CardDescription>
{t('agents.noRunnerMetadata')}
</CardDescription>
</CardHeader>
</Card>
)}
</div>
)}
{activeSection === 'events' && (
<Card>
<CardHeader>
<CardTitle>{t('agents.eventCapability')}</CardTitle>
<CardDescription>
{t('agents.eventCapabilityDescription')}
</CardDescription>
</CardHeader>
<CardContent>
<FormField
control={form.control}
name="supported_event_patterns_text"
render={({ field }) => (
<FormItem>
<FormLabel>{t('agents.supportedEvents')}</FormLabel>
<FormControl>
<Textarea
{...field}
className="min-h-32 font-mono text-sm"
placeholder={'*\nmessage.received\ngroup.*'}
/>
</FormControl>
<FormDescription>
{t('agents.supportedEventsDescription')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</CardContent>
</Card>
)}
</div>
</div>
</form>
</Form>
</div>
<Dialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}>
<DialogContent>
<DialogHeader>
<DialogTitle>{t('common.confirmDelete')}</DialogTitle>
</DialogHeader>
<div className="py-4">{t('agents.deleteConfirmation')}</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => setShowDeleteConfirm(false)}
>
{t('common.cancel')}
</Button>
<Button variant="destructive" onClick={confirmDelete}>
{t('common.confirmDelete')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}
+19
View File
@@ -0,0 +1,19 @@
import { useSearchParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import AgentDetailContent from './AgentDetailContent';
export default function AgentsPage() {
const { t } = useTranslation();
const [searchParams] = useSearchParams();
const detailId = searchParams.get('id');
if (detailId) {
return <AgentDetailContent id={detailId} />;
}
return (
<div className="flex h-full items-center justify-center text-muted-foreground">
<p>{t('agents.selectFromSidebar')}</p>
</div>
);
}
@@ -14,10 +14,11 @@ import { UUID } from 'uuidjs';
import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent';
import { httpClient } from '@/app/infra/http/HttpClient';
import { systemInfo } from '@/app/infra/http';
import { Bot } from '@/app/infra/entities/api';
import { Agent, Bot } from '@/app/infra/entities/api';
import { getAdapterDocUrl } from '@/app/infra/entities/adapter-docs';
import { ExternalLink } from 'lucide-react';
import RoutingRulesEditor from './RoutingRulesEditor';
import EventBindingsEditor from './EventBindingsEditor';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
@@ -88,6 +89,21 @@ const getFormSchema = (t: (key: string) => string) =>
}),
)
.optional(),
event_bindings: z
.array(
z.object({
id: z.string().optional(),
event_pattern: z.string(),
target_type: z.enum(['agent', 'pipeline', 'discard']),
target_uuid: z.string(),
filters: z.array(z.record(z.string(), z.any())).optional(),
priority: z.number(),
enabled: z.boolean(),
description: z.string().optional(),
order: z.number().optional(),
}),
)
.optional(),
});
export default function BotForm({
@@ -114,6 +130,7 @@ export default function BotForm({
enable: true,
use_pipeline_uuid: '',
pipeline_routing_rules: [],
event_bindings: [],
},
});
@@ -133,10 +150,14 @@ export default function BotForm({
const [adapterHelpLinks, setAdapterHelpLinks] = useState<
Record<string, Record<string, string>>
>({});
const [adapterSupportedEvents, setAdapterSupportedEvents] = useState<
Record<string, string[]>
>({});
const [pipelineNameList, setPipelineNameList] = useState<IPipelineEntity[]>(
[],
);
const [agentNameList, setAgentNameList] = useState<Agent[]>([]);
const [dynamicFormConfigList, setDynamicFormConfigList] = useState<
IDynamicFormItemSchema[]
@@ -181,6 +202,7 @@ export default function BotForm({
enable: val.enable,
use_pipeline_uuid: val.use_pipeline_uuid || '',
pipeline_routing_rules: val.pipeline_routing_rules || [],
event_bindings: val.event_bindings || [],
});
handleAdapterSelect(val.adapter);
@@ -220,6 +242,9 @@ export default function BotForm({
}),
);
const agentsRes = await httpClient.getAgents();
setAgentNameList(agentsRes.agents);
const adaptersRes = await httpClient.getAdapters();
setAdapterNameList(
adaptersRes.adapters.map((item) => {
@@ -253,6 +278,16 @@ export default function BotForm({
),
);
setAdapterSupportedEvents(
adaptersRes.adapters.reduce(
(acc, item) => {
acc[item.name] = item.spec.supported_events || [];
return acc;
},
{} as Record<string, string[]>,
),
);
adaptersRes.adapters.forEach((rawAdapter) => {
adapterNameToDynamicConfigMap.set(
rawAdapter.name,
@@ -298,6 +333,7 @@ export default function BotForm({
enable: bot.enable ?? true,
use_pipeline_uuid: bot.use_pipeline_uuid ?? '',
pipeline_routing_rules: bot.pipeline_routing_rules ?? [],
event_bindings: bot.event_bindings ?? [],
webhook_full_url: runtimeValues?.webhook_full_url as
| string
| undefined,
@@ -343,6 +379,7 @@ export default function BotForm({
enable: form.getValues().enable,
use_pipeline_uuid: form.getValues().use_pipeline_uuid,
pipeline_routing_rules: form.getValues().pipeline_routing_rules ?? [],
event_bindings: form.getValues().event_bindings ?? [],
};
httpClient
.updateBot(initBotId, updateBot)
@@ -503,7 +540,26 @@ export default function BotForm({
</Card>
)}
{/* Card 3: Adapter Configuration */}
{/* Card 3: Event Orchestration (edit mode only) */}
{initBotId && (
<Card>
<CardHeader>
<CardTitle>{t('bots.eventOrchestration')}</CardTitle>
<CardDescription>
{t('bots.eventOrchestrationDescription')}
</CardDescription>
</CardHeader>
<CardContent>
<EventBindingsEditor
form={form}
supportedEvents={adapterSupportedEvents[currentAdapter] || []}
agentOptions={agentNameList}
/>
</CardContent>
</Card>
)}
{/* Card 4: Adapter Configuration */}
<Card>
<CardHeader>
<CardTitle>{t('bots.adapterConfig')}</CardTitle>
@@ -561,7 +617,10 @@ export default function BotForm({
</SelectLabel>
)}
{group.items.map((item) => (
<SelectItem key={item.value} value={item.value}>
<SelectItem
key={`${group.categoryId ?? 'uncategorized'}:${item.value}`}
value={item.value}
>
<div className="flex items-center gap-2">
<img
src={httpClient.getAdapterIconURL(
@@ -0,0 +1,314 @@
'use client';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { UseFormReturn } from 'react-hook-form';
import { Ban, Bot, Plus, Trash2, Workflow } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Switch } from '@/components/ui/switch';
import { FormLabel } from '@/components/ui/form';
import {
Select,
SelectContent,
SelectItem,
SelectSeparator,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { EventBinding, Agent, AgentKind } from '@/app/infra/entities/api';
interface EventBindingsEditorProps {
form: UseFormReturn<any>;
supportedEvents: string[];
agentOptions: Agent[];
}
const DEFAULT_EVENTS = [
'message.received',
'feedback.received',
'group.member_joined',
'group.member_left',
'friend.request_received',
'bot.invited_to_group',
'platform.specific',
];
function isMessageEventPattern(pattern: string): boolean {
return pattern === 'message.*' || pattern.startsWith('message.');
}
function eventPatternCovers(
supportedPattern: string,
bindingPattern: string,
): boolean {
if (supportedPattern === '*') return true;
if (supportedPattern === bindingPattern) return true;
if (bindingPattern === '*') return false;
if (supportedPattern.endsWith('.*')) {
const namespace = supportedPattern.replace('.*', '');
return (
bindingPattern === `${namespace}.*` ||
bindingPattern.startsWith(`${namespace}.`)
);
}
return false;
}
function agentSupportsEventPattern(
agent: Agent,
bindingPattern: string,
): boolean {
const patterns = agent.supported_event_patterns ??
agent.capability?.supported_event_patterns ?? ['*'];
return patterns.some((pattern) =>
eventPatternCovers(pattern, bindingPattern),
);
}
function eventNamespaces(events: string[]): string[] {
const namespaces = new Set<string>();
for (const event of events) {
const namespace = event.split('.')[0];
if (namespace) namespaces.add(`${namespace}.*`);
}
return Array.from(namespaces).sort();
}
function targetLabel(agent: Agent): string {
return `${agent.emoji ? `${agent.emoji} ` : ''}${agent.name}`;
}
export default function EventBindingsEditor({
form,
supportedEvents,
agentOptions,
}: EventBindingsEditorProps) {
const { t } = useTranslation();
const bindings: EventBinding[] = form.watch('event_bindings') || [];
const eventOptions = useMemo(() => {
const concreteEvents =
supportedEvents.length > 0 ? supportedEvents : DEFAULT_EVENTS;
return ['*', ...eventNamespaces(concreteEvents), ...concreteEvents].filter(
(event, index, list) => list.indexOf(event) === index,
);
}, [supportedEvents]);
function updateBindings(nextBindings: EventBinding[]) {
form.setValue('event_bindings', nextBindings, { shouldDirty: true });
}
function addBinding() {
updateBindings([
...bindings,
{
event_pattern: 'message.received',
target_type: 'agent',
target_uuid: '',
priority: bindings.length,
enabled: true,
description: '',
filters: [],
},
]);
}
function updateBinding(index: number, patch: Partial<EventBinding>) {
const updated = [...bindings];
updated[index] = { ...updated[index], ...patch };
updateBindings(updated);
}
function removeBinding(index: number) {
const updated = [...bindings];
updated.splice(index, 1);
updateBindings(updated);
}
function getTargetOptions(binding: EventBinding, kind: AgentKind): Agent[] {
return agentOptions.filter((agent) => {
if (agent.kind !== kind) return false;
if (kind === 'pipeline') {
return isMessageEventPattern(binding.event_pattern);
}
if (kind === 'agent') {
return agentSupportsEventPattern(agent, binding.event_pattern);
}
return true;
});
}
return (
<div className="mt-6">
<div className="flex items-center justify-between mb-2">
<div>
<FormLabel>{t('bots.eventBindings')}</FormLabel>
<p className="text-sm text-muted-foreground mt-1">
{t('bots.eventOrchestrationDescription')}
</p>
</div>
<Button type="button" variant="outline" size="sm" onClick={addBinding}>
<Plus className="h-4 w-4 mr-1" />
{t('bots.addEventBinding')}
</Button>
</div>
{bindings.length === 0 && (
<div className="rounded-md border border-dashed p-4 text-sm text-muted-foreground">
{t('bots.noEventBindings')}
</div>
)}
<div className="space-y-2">
{bindings.map((binding, index) => {
const pipelineAllowed = isMessageEventPattern(binding.event_pattern);
const targetType = binding.target_type || 'agent';
const targetOptions =
targetType === 'discard'
? []
: getTargetOptions(binding, targetType as AgentKind);
return (
<div
key={binding.id ?? index}
className="grid gap-2 rounded-md border bg-muted/30 p-3 lg:grid-cols-[1.2fr_0.9fr_1.4fr_80px_72px_36px]"
>
<Select
value={binding.event_pattern}
onValueChange={(eventPattern) => {
const patch: Partial<EventBinding> = {
event_pattern: eventPattern,
};
if (
binding.target_type === 'pipeline' &&
!isMessageEventPattern(eventPattern)
) {
patch.target_type = 'agent';
patch.target_uuid = '';
}
updateBinding(index, patch);
}}
>
<SelectTrigger>
<SelectValue
placeholder={t('bots.eventPatternPlaceholder')}
/>
</SelectTrigger>
<SelectContent>
{eventOptions.map((event) => (
<SelectItem key={event} value={event}>
{event === '*'
? t('bots.eventWildcard')
: event.endsWith('.*')
? t('bots.eventNamespaceWildcard', {
namespace: event.replace('.*', ''),
})
: event}
</SelectItem>
))}
</SelectContent>
</Select>
<Select
value={targetType}
onValueChange={(nextType) => {
updateBinding(index, {
target_type: nextType as EventBinding['target_type'],
target_uuid: '',
});
}}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="agent">
<div className="flex items-center gap-2">
<Bot className="size-3.5" />
{t('bots.targetAgent')}
</div>
</SelectItem>
<SelectItem value="pipeline" disabled={!pipelineAllowed}>
<div className="flex items-center gap-2">
<Workflow className="size-3.5" />
{t('bots.targetPipeline')}
</div>
</SelectItem>
<SelectSeparator />
<SelectItem value="discard">
<div className="flex items-center gap-2 text-destructive">
<Ban className="size-3.5" />
{t('bots.targetDiscard')}
</div>
</SelectItem>
</SelectContent>
</Select>
{targetType === 'discard' ? (
<div className="flex items-center rounded-md border bg-background px-3 text-sm text-destructive">
<Ban className="mr-2 size-3.5" />
{t('bots.targetDiscard')}
</div>
) : (
<Select
value={binding.target_uuid}
onValueChange={(targetUuid) =>
updateBinding(index, { target_uuid: targetUuid })
}
>
<SelectTrigger>
<SelectValue placeholder={t('bots.selectTarget')} />
</SelectTrigger>
<SelectContent>
{targetOptions.map((agent) => (
<SelectItem key={agent.uuid} value={agent.uuid || ''}>
{targetLabel(agent)}
</SelectItem>
))}
</SelectContent>
</Select>
)}
<Input
type="number"
value={binding.priority ?? 0}
onChange={(event) =>
updateBinding(index, {
priority: Number(event.target.value || 0),
})
}
aria-label={t('bots.priority')}
/>
<div className="flex items-center justify-center rounded-md border bg-background">
<Switch
checked={binding.enabled ?? true}
onCheckedChange={(enabled) =>
updateBinding(index, { enabled })
}
aria-label={t('bots.enabled')}
/>
</div>
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => removeBinding(index)}
>
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
{!pipelineAllowed && binding.target_type === 'pipeline' && (
<div className="lg:col-span-6 text-xs text-destructive">
{t('bots.unsupportedPipelineEvent')}
</div>
)}
</div>
);
})}
</div>
</div>
);
}
@@ -311,7 +311,7 @@ export default function DynamicFormComponent({
}: {
itemConfigList: IDynamicFormItemSchema[];
onSubmit?: (val: object) => unknown;
initialValues?: Record<string, object>;
initialValues?: Record<string, any>;
onFileUploaded?: (fileKey: string) => void;
isEditing?: boolean;
externalDependentValues?: Record<string, unknown>;
@@ -65,6 +65,51 @@ import SettingsDialog, {
SettingsSection,
} from '@/app/home/components/settings-dialog/SettingsDialog';
function getPluginComponentIconURL(value?: string): string | null {
if (!value?.startsWith('plugin:')) {
return null;
}
const match = value.match(/^plugin:([^/]+)\/([^/]+)(?:\/|$)/);
if (!match) {
return null;
}
return httpClient.getPluginIconURL(match[1], match[2]);
}
function SelectOptionContent({
label,
value,
showDescription = false,
}: {
label: string;
value: string;
showDescription?: boolean;
}) {
const iconURL = getPluginComponentIconURL(value);
return (
<div className="flex min-w-0 items-center gap-2">
{iconURL && (
<img
src={iconURL}
alt=""
className="size-5 shrink-0 rounded object-cover"
/>
)}
<div className="min-w-0 flex flex-col">
<span className="truncate">{label}</span>
{showDescription && (
<span className="truncate text-xs text-muted-foreground">
{value}
</span>
)}
</div>
</div>
);
}
export default function DynamicFormItemComponent({
config,
field,
@@ -378,10 +423,20 @@ export default function DynamicFormItemComponent({
);
case DynamicFormItemType.SELECT:
const selectedOption = config.options?.find(
(option) => option.name === field.value,
);
return (
<Select value={field.value} onValueChange={field.onChange}>
<Select value={field.value ?? ''} onValueChange={field.onChange}>
<SelectTrigger className="w-full max-w-md bg-[#ffffff] dark:bg-[#2a2a2e]">
<SelectValue placeholder={t('common.select')} />
{selectedOption ? (
<SelectOptionContent
label={extractI18nObject(selectedOption.label)}
value={selectedOption.name}
/>
) : (
<SelectValue placeholder={t('common.select')} />
)}
</SelectTrigger>
<SelectContent>
<SelectGroup>
@@ -391,7 +446,11 @@ export default function DynamicFormItemComponent({
value={option.name}
description={option.name}
>
{extractI18nObject(option.label)}
<SelectOptionContent
label={extractI18nObject(option.label)}
value={option.name}
showDescription
/>
</SelectItem>
))}
</SelectGroup>
@@ -195,7 +195,7 @@ const ENTITY_KEY_MAP: Record<
// Route prefix map for entity detail pages
const ENTITY_ROUTE_MAP: Record<EntityCategoryId, string> = {
bots: '/home/bots',
pipelines: '/home/pipelines',
pipelines: '/home/agents',
knowledge: '/home/knowledge',
plugins: '/home/extensions',
mcp: '/home/mcp',
@@ -115,9 +115,9 @@ export function SidebarDataProvider({
const refreshPipelines = useCallback(async () => {
try {
const resp = await httpClient.getPipelines();
const resp = await httpClient.getAgents();
setPipelines(
resp.pipelines.map((p) => ({
resp.agents.map((p) => ({
id: p.uuid || '',
name: p.name,
description: p.description,
@@ -126,7 +126,7 @@ export function SidebarDataProvider({
})),
);
} catch (error) {
console.error('Failed to fetch pipelines for sidebar:', error);
console.error('Failed to fetch agents for sidebar:', error);
}
}, []);
@@ -57,10 +57,10 @@ export const sidebarConfigList = [
}),
new SidebarChildVO({
id: 'pipelines',
name: t('pipelines.title'),
name: t('agents.title'),
icon: <Workflow className="text-blue-500" />,
route: '/home/pipelines',
description: t('pipelines.description'),
route: '/home/agents',
description: t('agents.description'),
helpLink: {
en_US: 'https://link.langbot.app/en/docs/pipelines',
zh_Hans: 'https://link.langbot.app/zh/docs/pipelines',
+1
View File
@@ -60,6 +60,7 @@ const EXTENSIONS_ROUTES = [
const HOME_TITLE_KEYS: { match: (path: string) => boolean; key: string }[] = [
{ match: (p) => p.startsWith('/home/monitoring'), key: 'monitoring.title' },
{ match: (p) => p.startsWith('/home/bots'), key: 'bots.title' },
{ match: (p) => p.startsWith('/home/agents'), key: 'agents.title' },
{ match: (p) => p.startsWith('/home/pipelines'), key: 'pipelines.title' },
{
match: (p) => p.startsWith('/home/add-extension'),
@@ -9,7 +9,13 @@ import { useSidebarData } from '@/app/home/components/home-sidebar/SidebarDataCo
import { useTranslation } from 'react-i18next';
import { Settings, Bug, BarChart3 } from 'lucide-react';
export default function PipelineDetailContent({ id }: { id: string }) {
export default function PipelineDetailContent({
id,
routeBase = '/home/pipelines',
}: {
id: string;
routeBase?: string;
}) {
const isCreateMode = id === 'new';
const navigate = useNavigate();
const { t } = useTranslation();
@@ -36,7 +42,7 @@ export default function PipelineDetailContent({ id }: { id: string }) {
function handleNewPipelineCreated(newPipelineId: string) {
refreshPipelines();
navigate(`/home/pipelines?id=${encodeURIComponent(newPipelineId)}`);
navigate(`${routeBase}?id=${encodeURIComponent(newPipelineId)}`);
}
// ==================== Create Mode ====================
@@ -71,7 +77,7 @@ export default function PipelineDetailContent({ id }: { id: string }) {
function handleDeletePipeline() {
refreshPipelines();
navigate('/home/pipelines');
navigate(routeBase);
}
// ==================== Edit Mode ====================
@@ -132,7 +138,7 @@ export default function PipelineDetailContent({ id }: { id: string }) {
onFinish={handleFinish}
onNewPipelineCreated={handleNewPipelineCreated}
onDeletePipeline={handleDeletePipeline}
onCancel={() => navigate('/home/pipelines')}
onCancel={() => navigate(routeBase)}
onDirtyChange={setFormDirty}
/>
</TabsContent>
@@ -325,7 +325,7 @@ export default function PipelineFormComponent({
const isFirstEmission = !initializedStagesRef.current.has(stageKey);
const currentValues =
(form.getValues(formName) as Record<string, unknown>) || {};
(form.getValues(formName) as Record<string, unknown>) || {};
form.setValue(formName, {
...currentValues,
[stageName]: values,
@@ -402,7 +402,7 @@ export default function PipelineFormComponent({
<DynamicFormComponent
itemConfigList={stage.config}
initialValues={
(form.watch(formName) as Record<string, unknown>)?.[
(form.watch(formName) as Record<string, unknown>)?.[
stage.name
] || {}
}
@@ -451,7 +451,7 @@ export default function PipelineFormComponent({
<CardContent className="space-y-6">
<DynamicFormComponent
itemConfigList={stage.config}
initialValues={effectiveInitialValues}
initialValues={effectiveInitialValues}
onSubmit={(values) => {
handleRunnerConfigEmit(stage.name, values);
}}
+54
View File
@@ -138,6 +138,45 @@ export interface ApiRespPipelines {
pipelines: Pipeline[];
}
export type AgentKind = 'agent' | 'pipeline';
export interface AgentCapability {
supported_event_patterns: string[];
message_only: boolean;
}
export interface Agent {
uuid?: string;
name: string;
description: string;
emoji?: string;
kind: AgentKind;
component_ref?: string | null;
config?: Record<string, unknown>;
enabled?: boolean;
supported_event_patterns?: string[];
capability?: AgentCapability;
created_at?: string;
updated_at?: string;
}
export interface ApiRespAgents {
agents: Agent[];
}
export interface ApiRespAgent {
agent: Agent;
}
export interface GetAgentMetadataResponseData {
runner_config?: PipelineConfigTab;
kinds: Array<{
name: AgentKind;
supported_event_patterns: string[];
message_only: boolean;
}>;
}
export interface Pipeline {
uuid?: string;
name: string;
@@ -167,6 +206,8 @@ export interface Adapter {
spec: {
categories?: string[];
help_links?: Record<string, string>;
supported_events?: string[];
supported_apis?: string[];
config: IDynamicFormItemSchema[];
};
}
@@ -189,6 +230,7 @@ export interface Bot {
use_pipeline_name?: string;
use_pipeline_uuid?: string;
pipeline_routing_rules?: PipelineRoutingRule[];
event_bindings?: EventBinding[];
created_at?: string;
updated_at?: string;
adapter_runtime_values?: object;
@@ -213,6 +255,18 @@ export interface PipelineRoutingRule {
pipeline_uuid: string;
}
export interface EventBinding {
id?: string;
event_pattern: string;
target_type: 'agent' | 'pipeline' | 'discard';
target_uuid: string;
filters?: Array<Record<string, unknown>>;
priority: number;
enabled: boolean;
description?: string;
order?: number;
}
export interface ApiRespKnowledgeBases {
bases: KnowledgeBase[];
}
+35
View File
@@ -6,6 +6,9 @@ import {
ApiRespProviderLLMModel,
LLMModel,
ApiRespPipelines,
ApiRespAgents,
ApiRespAgent,
Agent,
Pipeline,
ApiRespPlatformAdapters,
ApiRespPlatformAdapter,
@@ -22,6 +25,7 @@ import {
ApiRespUserToken,
GetPipelineResponseData,
GetPipelineMetadataResponseData,
GetAgentMetadataResponseData,
AsyncTask,
ApiRespWebChatMessages,
ApiRespKnowledgeBases,
@@ -227,6 +231,37 @@ export class BackendClient extends BaseHttpClient {
}
// ============ Pipeline API ============
public getAgents(
sortBy?: string,
sortOrder?: string,
): Promise<ApiRespAgents> {
const params = new URLSearchParams();
if (sortBy) params.append('sort_by', sortBy);
if (sortOrder) params.append('sort_order', sortOrder);
const queryString = params.toString();
return this.get(`/api/v1/agents${queryString ? `?${queryString}` : ''}`);
}
public getAgent(uuid: string): Promise<ApiRespAgent> {
return this.get(`/api/v1/agents/${uuid}`);
}
public getAgentMetadata(): Promise<GetAgentMetadataResponseData> {
return this.get('/api/v1/agents/_/metadata');
}
public createAgent(agent: Agent): Promise<{ uuid: string; kind: string }> {
return this.post('/api/v1/agents', agent);
}
public updateAgent(uuid: string, agent: Partial<Agent>): Promise<object> {
return this.put(`/api/v1/agents/${uuid}`, agent);
}
public deleteAgent(uuid: string): Promise<object> {
return this.delete(`/api/v1/agents/${uuid}`);
}
public getGeneralPipelineMetadata(): Promise<GetPipelineMetadataResponseData> {
// as designed, this method will be deprecated, and only for developer to check the prefered config schema
return this.get('/api/v1/pipelines/_/metadata');
+65
View File
@@ -359,6 +359,27 @@ const enUS = {
routingConnection: 'Routing & Connection',
routingConnectionDescription:
'Bind the pipeline that processes messages for this bot',
eventOrchestration: 'Event Orchestration',
eventOrchestrationDescription:
'Bind different handling logic to different bot events. Pipelines only support message events.',
eventBindings: 'Event Bindings',
addEventBinding: 'Add Event Binding',
eventPattern: 'Event',
eventPatternPlaceholder: 'Select event',
targetType: 'Target Type',
target: 'Handling Logic',
targetAgent: 'Agent Orchestration',
targetPipeline: 'Pipeline',
targetDiscard: 'Discard',
selectTarget: 'Select handling logic',
priority: 'Priority',
enabled: 'Enabled',
eventBindingDescriptionPlaceholder: 'Rule description',
noEventBindings: 'No event bindings',
unsupportedPipelineEvent: 'Pipelines can only be used for message.* events',
eventCustom: 'Custom event',
eventWildcard: 'All events',
eventNamespaceWildcard: '{{namespace}}.*',
routingRules: 'Conditional Routing Rules',
routingRulesDescription:
'Rules are evaluated in order; first match routes to its pipeline. Fallback to the default pipeline above if none match.',
@@ -445,6 +466,50 @@ const enUS = {
botMessage: 'Assistant',
},
},
agents: {
title: 'Agent',
description:
'Manage Agent orchestrations and Pipelines, then bind them to bot events',
create: 'Create Agent',
editAgent: 'Edit Agent Orchestration',
selectFromSidebar: 'Select an Agent or Pipeline from the sidebar',
agentOrchestration: 'Agent Orchestration',
agentOrchestrationDescription:
'Event-first handling logic for messages, group members, friends, feedback, and other EBA events.',
pipelineType: 'Pipeline',
pipelineTypeDescription:
'Keep the existing no-code message pipeline for backward compatibility. It only handles message events.',
allEvents: 'Supports all EBA events',
messageEventsOnly: 'Message events only',
basicInfo: 'Basic Information',
basicInfoDescription: 'Set the name, icon, description and enabled state',
runnerSettings: 'Runner',
eventCapability: 'Event Capability',
eventCapabilityDescription:
'Declare which events this Agent orchestration can be bound to. Use one event pattern per line; * and namespace.* are supported.',
supportedEvents: 'Supported Events',
supportedEventsDescription:
'Examples: *, message.received, group.*. Pipelines are fixed to message.*.',
enabled: 'Enable Agent',
enabledDescription:
'When disabled, this Agent should not be selected by event routing.',
nameRequired: 'Name cannot be empty',
createSuccess: 'Created successfully',
createError: 'Creation failed: ',
loadError: 'Load failed: ',
saveSuccess: 'Saved successfully',
saveError: 'Save failed: ',
deleteSuccess: 'Deleted successfully',
deleteError: 'Delete failed: ',
deleteConfirmation:
'Are you sure you want to delete this Agent orchestration?',
dangerZone: 'Danger Zone',
dangerZoneDescription: 'Irreversible and destructive actions',
deleteAgentAction: 'Delete this Agent orchestration',
deleteAgentHint:
'Once deleted, events bound to it can no longer be executed.',
noRunnerMetadata: 'No AgentRunner metadata is currently available.',
},
plugins: {
title: 'Extensions',
description:
+64
View File
@@ -365,6 +365,28 @@ const jaJP = {
routingConnection: 'ルーティングと接続',
routingConnectionDescription:
'このボットのメッセージを処理するパイプラインを紐付け',
eventOrchestration: 'イベント編成',
eventOrchestrationDescription:
'このボットのイベントごとに異なる処理ロジックを紐付けます。Pipeline はメッセージイベントのみ対応します。',
eventBindings: 'イベントバインディング',
addEventBinding: 'イベントバインディングを追加',
eventPattern: 'イベント',
eventPatternPlaceholder: 'イベントを選択',
targetType: 'ターゲットタイプ',
target: '処理ロジック',
targetAgent: 'Agent 編成',
targetPipeline: 'Pipeline',
targetDiscard: '破棄',
selectTarget: '処理ロジックを選択',
priority: '優先度',
enabled: '有効',
eventBindingDescriptionPlaceholder: 'ルール説明',
noEventBindings: 'イベントバインディングはありません',
unsupportedPipelineEvent:
'Pipeline は message.* イベントにのみ使用できます',
eventCustom: 'カスタムイベント',
eventWildcard: 'すべてのイベント',
eventNamespaceWildcard: '{{namespace}}.*',
routingRules: '条件付きルーティングルール',
routingRulesDescription:
'ルールは順番に評価され、最初に一致したルールのパイプラインにルーティングされます。一致しない場合はデフォルトパイプラインが使用されます。',
@@ -451,6 +473,48 @@ const jaJP = {
botMessage: 'アシスタント',
},
},
agents: {
title: 'Agent',
description: 'Agent 編成と Pipeline を管理し、ボットのイベントに紐付けます',
create: 'Agent を作成',
editAgent: 'Agent 編成を編集',
selectFromSidebar:
'サイドバーから Agent または Pipeline を選択してください',
agentOrchestration: 'Agent 編成',
agentOrchestrationDescription:
'メッセージ、グループメンバー、友だち、フィードバックなどの EBA イベント向けの処理ロジックです。',
pipelineType: 'Pipeline',
pipelineTypeDescription:
'既存のノーコードメッセージ Pipeline を互換性のため保持します。メッセージイベントのみ処理できます。',
allEvents: 'すべての EBA イベントに対応',
messageEventsOnly: 'メッセージイベントのみ',
basicInfo: '基本情報',
basicInfoDescription: '名前、アイコン、説明、有効状態を設定します',
runnerSettings: 'Runner',
eventCapability: 'イベント能力',
eventCapabilityDescription:
'この Agent 編成をどのイベントに紐付けられるかを宣言します。1 行に 1 つのイベントパターンを指定し、* と namespace.* を利用できます。',
supportedEvents: '対応イベント',
supportedEventsDescription:
'例: *、message.received、group.*。Pipeline は message.* 固定です。',
enabled: 'Agent を有効化',
enabledDescription:
'無効化すると、この Agent はイベントルーティングで選択されません。',
nameRequired: '名前は必須です',
createSuccess: '作成に成功しました',
createError: '作成に失敗しました:',
loadError: '読み込みに失敗しました:',
saveSuccess: '保存に成功しました',
saveError: '保存に失敗しました:',
deleteSuccess: '削除に成功しました',
deleteError: '削除に失敗しました:',
deleteConfirmation: 'この Agent 編成を削除してもよろしいですか?',
dangerZone: '危険ゾーン',
dangerZoneDescription: '元に戻せない操作',
deleteAgentAction: 'この Agent 編成を削除',
deleteAgentHint: '削除すると、紐付けられたイベントは実行できなくなります。',
noRunnerMetadata: '現在利用可能な AgentRunner メタデータはありません。',
},
plugins: {
title: '拡張機能',
description:
+61
View File
@@ -343,6 +343,27 @@ const zhHans = {
basicInfoDescription: '设置机器人名称和描述',
routingConnection: '路由与连接',
routingConnectionDescription: '绑定处理此机器人消息的流水线',
eventOrchestration: '事件编排',
eventOrchestrationDescription:
'为此机器人不同事件绑定不同处理逻辑。Pipeline 仅支持消息事件。',
eventBindings: '事件绑定',
addEventBinding: '添加事件绑定',
eventPattern: '事件',
eventPatternPlaceholder: '选择事件',
targetType: '目标类型',
target: '处理逻辑',
targetAgent: 'Agent 编排',
targetPipeline: 'Pipeline',
targetDiscard: '丢弃',
selectTarget: '选择处理逻辑',
priority: '优先级',
enabled: '启用',
eventBindingDescriptionPlaceholder: '规则说明',
noEventBindings: '暂无事件绑定',
unsupportedPipelineEvent: 'Pipeline 仅可用于 message.* 事件',
eventCustom: '自定义事件',
eventWildcard: '全部事件',
eventNamespaceWildcard: '{{namespace}}.*',
routingRules: '条件路由规则',
routingRulesDescription:
'按顺序匹配,命中第一条规则后路由到对应流水线;都不匹配时使用上方默认流水线',
@@ -427,6 +448,46 @@ const zhHans = {
botMessage: '助手',
},
},
agents: {
title: 'Agent',
description: '管理 Agent 编排与 Pipeline,并将它们绑定到机器人事件',
create: '创建 Agent',
editAgent: '编辑 Agent 编排',
selectFromSidebar: '从侧边栏选择一个 Agent 或 Pipeline',
agentOrchestration: 'Agent 编排',
agentOrchestrationDescription:
'面向 EBA 事件的处理逻辑,可用于消息、群成员、好友、反馈等事件。',
pipelineType: 'Pipeline',
pipelineTypeDescription:
'保留现有无代码消息流水线,兼容旧配置,只能处理消息事件。',
allEvents: '支持全部 EBA 事件',
messageEventsOnly: '仅支持消息事件',
basicInfo: '基础信息',
basicInfoDescription: '设置名称、图标、描述和启用状态',
runnerSettings: '运行器',
eventCapability: '事件能力',
eventCapabilityDescription:
'声明此 Agent 编排可被绑定到哪些事件。每行一个事件模式,支持 * 与 namespace.*。',
supportedEvents: '支持的事件',
supportedEventsDescription:
'例如 *、message.received、group.*。Pipeline 固定仅支持 message.*。',
enabled: '启用 Agent',
enabledDescription: '禁用后,此 Agent 不应被事件路由选中。',
nameRequired: '名称不能为空',
createSuccess: '创建成功',
createError: '创建失败:',
loadError: '加载失败:',
saveSuccess: '保存成功',
saveError: '保存失败:',
deleteSuccess: '删除成功',
deleteError: '删除失败:',
deleteConfirmation: '你确定要删除这个 Agent 编排吗?',
dangerZone: '危险区域',
dangerZoneDescription: '不可逆的操作',
deleteAgentAction: '删除此 Agent 编排',
deleteAgentHint: '删除后,绑定到它的事件将无法继续执行。',
noRunnerMetadata: '当前没有可用的 AgentRunner 元数据。',
},
plugins: {
title: '插件扩展',
description: '安装和配置用于扩展功能的插件,请在流水线配置中选用',
+11
View File
@@ -16,6 +16,7 @@ import SpaceCallbackPage from '@/app/auth/space/callback/page';
import HomePage from '@/app/home/page';
import MonitoringPage from '@/app/home/monitoring/page';
import BotsPage from '@/app/home/bots/page';
import AgentsPage from '@/app/home/agents/page';
import PipelinesPage from '@/app/home/pipelines/page';
import PluginsPage from '@/app/home/plugins/page';
import AddExtensionPage from '@/app/home/add-extension/page';
@@ -104,6 +105,16 @@ export const router = createBrowserRouter([
</Suspense>
),
},
{
path: '/home/agents',
element: (
<Suspense fallback={<Loading />}>
<HomeLayout>
<AgentsPage />
</HomeLayout>
</Suspense>
),
},
{
path: '/home/pipelines',
element: (