mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-25 06:54:19 +00:00
feat(agent): add event orchestration surface
This commit is contained in:
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
}}
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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: '安装和配置用于扩展功能的插件,请在流水线配置中选用',
|
||||
|
||||
@@ -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: (
|
||||
|
||||
Reference in New Issue
Block a user