feat(agent): add event-aware tool permissions

This commit is contained in:
Hyu
2026-09-04 15:37:41 +08:00
parent 7c64756203
commit a4d36aa2db
31 changed files with 2519 additions and 485 deletions
+14 -1
View File
@@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { toast } from 'sonner';
import { Trash2 } from 'lucide-react';
import { AlertTriangle, Trash2 } from 'lucide-react';
import { httpClient } from '@/app/infra/http/HttpClient';
import { useCurrentWorkspace } from '@/app/infra/http';
import { Agent } from '@/app/infra/entities/api';
@@ -13,6 +13,7 @@ import EntityBasicInfoDialog, {
} from '@/app/home/components/entity-basic-info/EntityBasicInfoDialog';
import EntityTitleEditButton from '@/app/home/components/entity-basic-info/EntityTitleEditButton';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import {
Dialog,
DialogContent,
@@ -168,6 +169,18 @@ export default function AgentDetailContent({ id }: { id: string }) {
<ProcessorDetailWorkbench
key={id}
title={`${agent.emoji || '🤖'} ${agent.name}`}
titleBadge={
supportedEventPatterns.length === 0 ? (
<Badge
variant="outline"
role="status"
className="shrink-0 gap-1 rounded-full border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-300"
>
<AlertTriangle className="size-3" />
{t('agents.noEventsConfiguredBadge')}
</Badge>
) : undefined
}
titleAction={
canManage ? (
<EntityTitleEditButton onClick={() => setBasicInfoOpen(true)} />
@@ -0,0 +1,383 @@
import { useEffect, useMemo, useState } from 'react';
import { ChevronDown, Search } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import type { AgentPlatformTool, PluginTool } from '@/app/infra/entities/api';
import { extractI18nObject } from '@/i18n/I18nProvider';
import { Checkbox } from '@/components/ui/checkbox';
import { Input } from '@/components/ui/input';
import { cn } from '@/lib/utils';
interface AgentApiToolPickerProps {
platformTools: AgentPlatformTool[];
platformValue: string[];
onPlatformChange: (value: string[]) => void;
hostTools: PluginTool[];
hostValue: string[];
onHostChange: (value: string[]) => void;
platformCatalogAvailable?: boolean;
hostCatalogAvailable?: boolean;
scopes?: readonly ToolScope[];
}
type ToolScope = 'event' | 'platform' | 'builtin' | 'mcp' | 'plugin' | 'skill';
type ToolEntry = {
key: string;
kind: 'platform' | 'host';
name: string;
scope: ToolScope;
group: string;
label: string;
description: string;
parameters: Record<string, unknown>;
api?: string;
eventPatterns?: string[];
risk?: AgentPlatformTool['risk'];
};
const PLATFORM_CATEGORY_LABELS: Record<string, { zh: string; en: string }> = {
message: { zh: '消息', en: 'Messages' },
identity: { zh: '用户与身份', en: 'Users & identity' },
group: { zh: '群组', en: 'Groups' },
moderation: { zh: '群管理', en: 'Moderation' },
request: { zh: '请求处理', en: 'Requests' },
};
const SCOPE_ORDER: ToolScope[] = [
'event',
'platform',
'builtin',
'mcp',
'plugin',
'skill',
];
function normalizeHostScope(tool: PluginTool): ToolScope {
if (tool.source === 'mcp' || tool.source === 'plugin') return tool.source;
if (tool.source === 'skill') return 'skill';
return 'builtin';
}
export default function AgentApiToolPicker({
platformTools,
platformValue,
onPlatformChange,
hostTools,
hostValue,
onHostChange,
platformCatalogAvailable = true,
hostCatalogAvailable = true,
scopes = SCOPE_ORDER,
}: AgentApiToolPickerProps) {
const { t, i18n } = useTranslation();
const [query, setQuery] = useState('');
const [activeScope, setActiveScope] = useState<ToolScope>(
scopes[0] ?? 'event',
);
const [expandedTool, setExpandedTool] = useState<string | null>(null);
const isChinese = i18n.language.startsWith('zh');
const selectedPlatform = useMemo(
() => new Set(platformValue),
[platformValue],
);
const selectedHost = useMemo(() => new Set(hostValue), [hostValue]);
const entries = useMemo<ToolEntry[]>(
() => [
...platformTools.map((tool) => ({
key: `platform:${tool.name}`,
kind: 'platform' as const,
name: tool.name,
scope: tool.scope,
group: tool.category,
label: extractI18nObject(tool.label),
description: extractI18nObject(tool.description),
parameters: tool.parameters,
api: tool.api,
eventPatterns: tool.event_patterns,
risk: tool.risk,
})),
...hostTools.map((tool) => ({
key: `host:${tool.source || 'builtin'}:${tool.source_id || ''}:${tool.name}`,
kind: 'host' as const,
name: tool.name,
scope: normalizeHostScope(tool),
group: tool.source_name || t('agents.langbotBuiltIn'),
label: tool.name,
description: tool.human_desc || tool.description || tool.name,
parameters: tool.parameters as Record<string, unknown>,
})),
],
[hostTools, platformTools, t],
);
const scopeCounts = useMemo(
() =>
Object.fromEntries(
SCOPE_ORDER.map((scope) => [
scope,
entries.filter((tool) => tool.scope === scope).length,
]),
) as Record<ToolScope, number>,
[entries],
);
const visibleScopes = SCOPE_ORDER.filter(
(scope) =>
scopes.includes(scope) && (scope !== 'skill' || scopeCounts.skill > 0),
);
useEffect(() => {
if (!visibleScopes.includes(activeScope) && visibleScopes[0]) {
setActiveScope(visibleScopes[0]);
setExpandedTool(null);
}
}, [activeScope, visibleScopes]);
const filteredEntries = useMemo(() => {
const needle = query.trim().toLocaleLowerCase();
return entries.filter(
(tool) =>
tool.scope === activeScope &&
(!needle ||
[tool.name, tool.label, tool.description, tool.api, tool.group]
.join(' ')
.toLocaleLowerCase()
.includes(needle)),
);
}, [activeScope, entries, query]);
const groupedEntries = useMemo(() => {
const groups = new Map<string, ToolEntry[]>();
for (const tool of filteredEntries) {
if (!groups.has(tool.group)) groups.set(tool.group, []);
groups.get(tool.group)!.push(tool);
}
return Array.from(groups.entries());
}, [filteredEntries]);
const selectedCount = entries.filter(
(tool) =>
scopes.includes(tool.scope) &&
(tool.kind === 'platform'
? selectedPlatform.has(tool.name)
: selectedHost.has(tool.name)),
).length;
const scopeLabel = (scope: ToolScope) => {
const keys: Record<ToolScope, string> = {
event: 'agents.eventApiTools',
platform: 'agents.platformApiTools',
builtin: 'agents.sandboxTools',
mcp: 'agents.mcpTools',
plugin: 'agents.pluginTools',
skill: 'agents.skillTools',
};
return t(keys[scope]);
};
const groupLabel = (group: string) => {
if (activeScope === 'event' || activeScope === 'platform') {
return isChinese
? PLATFORM_CATEGORY_LABELS[group]?.zh || group
: PLATFORM_CATEGORY_LABELS[group]?.en || group;
}
return group;
};
const setTool = (tool: ToolEntry, checked: boolean) => {
if (tool.kind === 'platform') {
const next = new Set(platformValue);
if (checked) next.add(tool.name);
else next.delete(tool.name);
onPlatformChange(
platformTools
.filter((item) => next.has(item.name))
.map((item) => item.name),
);
return;
}
const next = new Set(hostValue);
if (checked) next.add(tool.name);
else next.delete(tool.name);
onHostChange(
hostTools.filter((item) => next.has(item.name)).map((item) => item.name),
);
};
const catalogAvailable =
activeScope === 'event' || activeScope === 'platform'
? platformCatalogAvailable
: hostCatalogAvailable;
return (
<div className="space-y-4">
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
{visibleScopes.length === 1 ? (
<span className="pt-2 text-sm font-medium">
{scopeLabel(visibleScopes[0])}
<span className="ml-1.5 text-xs font-normal text-muted-foreground">
{scopeCounts[visibleScopes[0]]}
</span>
</span>
) : (
<div className="inline-flex max-w-full flex-wrap gap-1 rounded-lg bg-muted p-1">
{visibleScopes.map((scope) => (
<button
key={scope}
type="button"
aria-pressed={activeScope === scope}
onClick={() => {
setActiveScope(scope);
setExpandedTool(null);
}}
className={cn(
'rounded-md px-2.5 py-1.5 text-sm transition-colors active:scale-[0.98]',
activeScope === scope
? 'bg-background font-medium shadow-sm'
: 'text-muted-foreground hover:text-foreground',
)}
>
{scopeLabel(scope)}
<span className="ml-1.5 text-xs text-muted-foreground">
{scopeCounts[scope]}
</span>
</button>
))}
</div>
)}
<span className="shrink-0 pt-2 text-xs text-muted-foreground">
{t('agents.apiToolsSelected', {
count: selectedCount,
})}
</span>
</div>
<div className="relative max-w-sm">
<Search className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
<Input
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder={t('agents.apiToolsSearch')}
className="pl-9"
/>
</div>
{!catalogAvailable && (
<div className="rounded-lg border border-amber-300 bg-amber-50 px-4 py-4 text-sm text-amber-900 dark:border-amber-900 dark:bg-amber-950/30 dark:text-amber-200">
{activeScope === 'event' || activeScope === 'platform'
? t('agents.apiToolsCatalogUnavailable')
: t('agents.hostToolsCatalogUnavailable')}
</div>
)}
{catalogAvailable && !filteredEntries.length && (
<div className="rounded-lg border border-dashed px-4 py-8 text-center text-sm text-muted-foreground">
{t('agents.apiToolsNoResults')}
</div>
)}
{catalogAvailable && filteredEntries.length > 0 && (
<div className="overflow-hidden rounded-lg border bg-background">
{groupedEntries.map(([group, groupTools], groupIndex) => (
<section key={group} className={cn(groupIndex > 0 && 'border-t')}>
<div className="bg-muted/30 px-3 py-1.5 text-xs font-medium text-muted-foreground">
{groupLabel(group)}
</div>
<div className="divide-y">
{groupTools.map((tool) => {
const checked =
tool.kind === 'platform'
? selectedPlatform.has(tool.name)
: selectedHost.has(tool.name);
const expanded = expandedTool === tool.key;
const parameterNames = Object.keys(
(tool.parameters.properties as
| Record<string, unknown>
| undefined) ?? {},
);
return (
<div key={tool.key} className="bg-background px-3 py-2.5">
<label
className={cn(
'grid grid-cols-[auto_minmax(0,1fr)_auto] items-start gap-2.5',
'cursor-pointer',
)}
>
<Checkbox
checked={checked}
onCheckedChange={(next) =>
setTool(tool, next === true)
}
aria-label={tool.label}
className="mt-0.5"
/>
<span className="min-w-0">
<span className="block text-sm font-medium leading-5">
{tool.label}
</span>
<span className="block truncate text-xs leading-5 text-muted-foreground">
{tool.description}
</span>
</span>
<span
className={cn(
'min-w-12 pt-0.5 text-right text-xs text-muted-foreground',
tool.risk === 'dangerous' &&
'text-amber-700 dark:text-amber-400',
)}
>
{tool.risk
? t(`agents.apiToolRisk.${tool.risk}`)
: scopeLabel(tool.scope)}
</span>
</label>
<button
type="button"
aria-expanded={expanded}
onClick={() =>
setExpandedTool(expanded ? null : tool.key)
}
className="ml-7 mt-0.5 inline-flex items-center gap-1 rounded px-1 py-0.5 text-xs text-muted-foreground hover:text-foreground active:scale-[0.98]"
>
{expanded
? t('agents.apiToolHideDetails')
: t('agents.apiToolDetails')}
<ChevronDown
className={cn(
'size-3 transition-transform',
expanded && 'rotate-180',
)}
/>
</button>
{expanded && (
<div className="ml-8 mt-1.5 space-y-1 border-l pl-3 text-xs text-muted-foreground">
<div className="font-mono text-foreground/75">
{tool.name}
</div>
{tool.api && <div>API: {tool.api}</div>}
{tool.kind === 'host' && (
<div>
{t('agents.apiToolSource')}:{' '}
{groupLabel(tool.group)}
</div>
)}
{tool.eventPatterns && (
<div>
{t('agents.apiToolEvents')}:{' '}
{tool.eventPatterns.join(', ')}
</div>
)}
<div>
{t('agents.apiToolParameters')}:{' '}
{parameterNames.length
? parameterNames.join(', ')
: t('agents.apiToolNoParameters')}
</div>
</div>
)}
</div>
);
})}
</div>
</section>
))}
</div>
)}
</div>
);
}
@@ -3,11 +3,11 @@ import { useTranslation } from 'react-i18next';
import { toast } from 'sonner';
import {
AlertCircle,
AlertTriangle,
ChevronDown,
CircleHelp,
LoaderCircle,
Play,
RotateCcw,
} from 'lucide-react';
import { httpClient } from '@/app/infra/http/HttpClient';
import { Badge } from '@/components/ui/badge';
@@ -167,11 +167,6 @@ export default function AgentDebugPanel({
setEventDataText(JSON.stringify(nextPreset.data, null, 2));
}
function resetSession() {
sessionIdRef.current = createDebugSessionId(agentId);
setEntries([]);
}
async function runDebugEvent() {
if (!eventType) {
toast.error(t('agents.debugEventTypeRequired'));
@@ -368,130 +363,131 @@ export default function AgentDebugPanel({
</div>
<div className="shrink-0 space-y-3 border-t p-3">
<div className="flex items-end gap-2">
<div className="min-w-0 flex-1 space-y-1.5">
<Label>{t('agents.debugEventType')}</Label>
<Select value={preset} onValueChange={selectPreset}>
<SelectTrigger
className="w-full"
aria-label={t('agents.debugEventType')}
>
<SelectValue />
</SelectTrigger>
<SelectContent className="w-[var(--radix-select-trigger-width)] max-w-[calc(100vw-2rem)]">
{eventGroups.map((group) => (
<SelectGroup key={group.namespace}>
<SelectLabel>
{eventGroupLabel(group.namespace, t)}
</SelectLabel>
{group.patterns.map((event) => (
{supportedEventPatterns.length === 0 ? (
<Alert className="bg-amber-500/5 text-amber-800 dark:text-amber-200">
<AlertTriangle className="size-4" />
<AlertTitle>{t('agents.debugNoEventsTitle')}</AlertTitle>
<AlertDescription>
{t('agents.debugNoEventsDescription')}
</AlertDescription>
</Alert>
) : (
<>
<div className="space-y-1.5">
<Label>{t('agents.debugEventType')}</Label>
<Select value={preset} onValueChange={selectPreset}>
<SelectTrigger
className="w-full"
aria-label={t('agents.debugEventType')}
>
<SelectValue />
</SelectTrigger>
<SelectContent className="w-[var(--radix-select-trigger-width)] max-w-[calc(100vw-2rem)]">
{eventGroups.map((group) => (
<SelectGroup key={group.namespace}>
<SelectLabel>
{eventGroupLabel(group.namespace, t)}
</SelectLabel>
{group.patterns.map((event) => (
<SelectItem
key={event}
value={event}
description={eventPatternDescription(event, t)}
className="py-2"
>
<EventSelectOptionContent
event={event}
label={eventPatternLabel(event, t)}
/>
</SelectItem>
))}
</SelectGroup>
))}
{supportsCustomEvent && (
<SelectGroup>
<SelectLabel>{t('agents.debugCustomEvent')}</SelectLabel>
<SelectItem
key={event}
value={event}
description={eventPatternDescription(event, t)}
value="custom"
description={t('bots.eventDescriptions.custom')}
className="py-2"
>
<EventSelectOptionContent
event={event}
label={eventPatternLabel(event, t)}
event="custom.event"
label={t('agents.debugCustomEvent')}
/>
</SelectItem>
))}
</SelectGroup>
))}
{supportsCustomEvent && (
<SelectGroup>
<SelectLabel>{t('agents.debugCustomEvent')}</SelectLabel>
<SelectItem
value="custom"
description={t('bots.eventDescriptions.custom')}
className="py-2"
>
<EventSelectOptionContent
event="custom.event"
label={t('agents.debugCustomEvent')}
/>
</SelectItem>
</SelectGroup>
)}
</SelectContent>
</Select>
</div>
<Button
type="button"
variant="outline"
size="icon"
onClick={resetSession}
title={t('agents.debugResetSession')}
>
<RotateCcw className="size-4" />
</Button>
</div>
</SelectGroup>
)}
</SelectContent>
</Select>
</div>
{preset === 'custom' && (
<div className="space-y-1.5">
<Label htmlFor="agent-debug-custom-event">
{t('agents.debugCustomEventType')}
</Label>
<Input
id="agent-debug-custom-event"
value={customEventType}
onChange={(event) => setCustomEventType(event.target.value)}
placeholder="custom.event"
/>
</div>
{preset === 'custom' && (
<div className="space-y-1.5">
<Label htmlFor="agent-debug-custom-event">
{t('agents.debugCustomEventType')}
</Label>
<Input
id="agent-debug-custom-event"
value={customEventType}
onChange={(event) => setCustomEventType(event.target.value)}
placeholder="custom.event"
/>
</div>
)}
<div className="space-y-1.5">
<Label htmlFor="agent-debug-input">
{isMessageEvent
? t('agents.debugMessageInput')
: t('agents.debugEventSummary')}
</Label>
<Textarea
id="agent-debug-input"
value={inputText}
onChange={(event) => setInputText(event.target.value)}
className="min-h-20 resize-y"
placeholder={t('agents.debugInputPlaceholder')}
/>
</div>
<details className="rounded-md border bg-muted/20 px-3 py-2">
<summary className="cursor-pointer text-xs font-medium">
{t('agents.debugEventPayload')}
</summary>
<div className="mt-2 space-y-2">
<p className="text-xs text-muted-foreground">
{t('agents.debugSupportedEvents')}: {supportedLabel}
</p>
<Textarea
id="agent-debug-payload"
value={eventDataText}
onChange={(event) => setEventDataText(event.target.value)}
className="min-h-28 resize-y font-mono text-xs"
spellCheck={false}
/>
</div>
</details>
<Button
type="button"
className="w-full"
disabled={running}
onClick={runDebugEvent}
>
{running ? (
<LoaderCircle className="size-4 animate-spin" />
) : (
<Play className="size-4" />
)}
{running
? t('agents.debugRunning')
: hasUnsavedChanges
? t('agents.debugSaveAndRun')
: t('agents.debugRun')}
</Button>
</>
)}
<div className="space-y-1.5">
<Label htmlFor="agent-debug-input">
{isMessageEvent
? t('agents.debugMessageInput')
: t('agents.debugEventSummary')}
</Label>
<Textarea
id="agent-debug-input"
value={inputText}
onChange={(event) => setInputText(event.target.value)}
className="min-h-20 resize-y"
placeholder={t('agents.debugInputPlaceholder')}
/>
</div>
<details className="rounded-md border bg-muted/20 px-3 py-2">
<summary className="cursor-pointer text-xs font-medium">
{t('agents.debugEventPayload')}
</summary>
<div className="mt-2 space-y-2">
<p className="text-xs text-muted-foreground">
{t('agents.debugSupportedEvents')}: {supportedLabel}
</p>
<Textarea
id="agent-debug-payload"
value={eventDataText}
onChange={(event) => setEventDataText(event.target.value)}
className="min-h-28 resize-y font-mono text-xs"
spellCheck={false}
/>
</div>
</details>
<Button
type="button"
className="w-full"
disabled={running}
onClick={runDebugEvent}
>
{running ? (
<LoaderCircle className="size-4 animate-spin" />
) : (
<Play className="size-4" />
)}
{running
? t('agents.debugRunning')
: hasUnsavedChanges
? t('agents.debugSaveAndRun')
: t('agents.debugRun')}
</Button>
</div>
</div>
);
@@ -1,8 +1,15 @@
import { useMemo, useState } from 'react';
import { Check, ChevronsUpDown } from 'lucide-react';
import { Check, ChevronDown, Plus, X } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { Badge } from '@/components/ui/badge';
import type { AgentPlatformTool } from '@/app/infra/entities/api';
import { extractI18nObject } from '@/i18n/I18nProvider';
import { Button } from '@/components/ui/button';
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from '@/components/ui/collapsible';
import { cn } from '@/lib/utils';
import {
Command,
CommandEmpty,
@@ -16,7 +23,6 @@ import {
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import { cn } from '@/lib/utils';
import {
eventGroupLabel,
eventNamespaces,
@@ -31,18 +37,53 @@ interface AgentEventPatternPickerProps {
events: string[];
value: string[];
onChange: (patterns: string[]) => void;
tools: AgentPlatformTool[];
catalogAvailable?: boolean;
}
function eventPatternsIntersect(left: string, right: string) {
if (left === '*' || right === '*' || left === right) return true;
if (!left.includes('*')) {
return right.endsWith('.*') && left.startsWith(right.slice(0, -1));
}
if (!right.includes('*')) {
return left.endsWith('.*') && right.startsWith(left.slice(0, -1));
}
const leftPrefix = left.slice(0, left.indexOf('*'));
const rightPrefix = right.slice(0, right.indexOf('*'));
return (
leftPrefix.startsWith(rightPrefix) || rightPrefix.startsWith(leftPrefix)
);
}
export function isEventToolCompatibleWithPattern(
tool: AgentPlatformTool,
pattern: string,
) {
return (
tool.scope === 'event' &&
tool.event_patterns.some((toolPattern) =>
eventPatternsIntersect(toolPattern, pattern),
)
);
}
export default function AgentEventPatternPicker({
events,
value,
onChange,
tools,
catalogAvailable = true,
}: AgentEventPatternPickerProps) {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const selectedPatterns = useMemo(
() => (value.length > 0 ? value : ['*']),
[value],
const [expandedPatterns, setExpandedPatterns] = useState<Set<string>>(
new Set(),
);
const selectedPatterns = value;
const eventTools = useMemo(
() => tools.filter((tool) => tool.scope === 'event'),
[tools],
);
const options = useMemo(() => {
const concreteEvents = Array.from(
@@ -59,115 +100,216 @@ export default function AgentEventPatternPicker({
...selectedPatterns.filter((pattern) => pattern.endsWith('.*')),
]),
).sort();
return ['*', ...namespaces, ...concreteEvents];
return ['*', ...namespaces, ...concreteEvents].filter(
(pattern) => !selectedPatterns.includes(pattern),
);
}, [events, selectedPatterns]);
const optionGroups = useMemo(() => groupEventPatterns(options), [options]);
function togglePattern(pattern: string) {
function addPattern(pattern: string) {
let nextPatterns: string[];
if (pattern === '*') {
onChange(['*']);
return;
}
if (selectedPatterns.includes(pattern)) {
const next = selectedPatterns.filter((item) => item !== pattern);
onChange(next.length > 0 ? next : ['*']);
return;
}
let next = selectedPatterns.filter((item) => item !== '*');
const namespace = pattern.split('.')[0];
if (pattern.endsWith('.*')) {
next = next.filter(
(item) => item.split('.')[0] !== namespace || item.endsWith('.*'),
);
nextPatterns = ['*'];
} else {
next = next.filter((item) => item !== `${namespace}.*`);
const namespace = pattern.split('.')[0];
nextPatterns = selectedPatterns.filter((item) => {
if (item === '*') {
return false;
}
const overlaps = pattern.endsWith('.*')
? item.split('.')[0] === namespace
: item === `${namespace}.*`;
return !overlaps;
});
nextPatterns.push(pattern);
}
onChange(Array.from(new Set([...next, pattern])));
onChange(Array.from(new Set(nextPatterns)));
setOpen(false);
}
function removePattern(pattern: string) {
onChange(selectedPatterns.filter((item) => item !== pattern));
}
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
type="button"
variant="outline"
role="combobox"
aria-expanded={open}
aria-label={t('agents.supportedEvents')}
className="h-auto min-h-10 w-full min-w-0 justify-between gap-2 px-3 py-2 font-normal"
>
<span className="flex min-w-0 flex-1 flex-wrap gap-1.5">
{selectedPatterns.slice(0, 3).map((pattern) => (
<Badge
key={pattern}
variant="secondary"
className="max-w-full rounded-md font-normal"
>
<span className="truncate">
{eventPatternLabel(pattern, t)}
</span>
</Badge>
))}
{selectedPatterns.length > 3 && (
<Badge variant="outline" className="rounded-md font-normal">
+{selectedPatterns.length - 3}
</Badge>
)}
</span>
<ChevronsUpDown className="size-4 shrink-0 text-muted-foreground" />
</Button>
</PopoverTrigger>
<PopoverContent
align="start"
className="w-[var(--radix-popover-trigger-width)] p-0"
>
<Command>
<CommandInput placeholder={t('agents.searchEvents')} />
<CommandList>
<CommandEmpty>{t('agents.noEventsFound')}</CommandEmpty>
{optionGroups.map((group) => (
<CommandGroup
key={group.namespace}
heading={eventGroupLabel(group.namespace, t)}
>
{group.patterns.map((pattern) => {
const selected = selectedPatterns.includes(pattern);
return (
<CommandItem
key={pattern}
value={`${eventPatternLabel(pattern, t)} ${pattern}`}
onSelect={() => togglePattern(pattern)}
className="items-start gap-2 py-2"
>
<Check
className={cn(
'mt-0.5 size-4 shrink-0',
selected ? 'opacity-100' : 'opacity-0',
)}
/>
<span className="min-w-0 flex-1">
<span className="flex min-w-0 items-center gap-2">
<span className="truncate font-medium">
{eventPatternLabel(pattern, t)}
<div className="overflow-hidden rounded-xl border bg-background">
<div className="flex items-center justify-between gap-3 border-b bg-muted/30 px-4 py-3">
<div className="min-w-0">
<p className="text-sm font-medium">{t('agents.configuredEvents')}</p>
<p className="text-xs text-muted-foreground">
{t('agents.configuredEventsCount', {
count: selectedPatterns.length,
})}
</p>
</div>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
type="button"
variant="outline"
size="sm"
className="gap-1.5"
>
<Plus className="size-4" />
{t('agents.addEvent')}
</Button>
</PopoverTrigger>
<PopoverContent align="end" className="w-96 max-w-[90vw] p-0">
<Command>
<CommandInput placeholder={t('agents.searchEvents')} />
<CommandList>
<CommandEmpty>{t('agents.noEventsFound')}</CommandEmpty>
{optionGroups.map((group) => (
<CommandGroup
key={group.namespace}
heading={eventGroupLabel(group.namespace, t)}
>
{group.patterns.map((pattern) => (
<CommandItem
key={pattern}
value={`${eventPatternLabel(pattern, t)} ${pattern}`}
onSelect={() => addPattern(pattern)}
className="items-start gap-2 py-2"
>
<Plus className="mt-0.5 size-4 shrink-0" />
<span className="min-w-0 flex-1">
<span className="flex min-w-0 items-center gap-2">
<span className="truncate font-medium">
{eventPatternLabel(pattern, t)}
</span>
<code className="shrink-0 text-[10px] text-muted-foreground">
{pattern}
</code>
</span>
<span className="mt-0.5 block text-xs text-muted-foreground">
{eventPatternDescription(pattern, t)}
</span>
<code className="shrink-0 text-[10px] text-muted-foreground">
{pattern}
</code>
</span>
<span className="mt-0.5 block text-xs text-muted-foreground">
{eventPatternDescription(pattern, t)}
</CommandItem>
))}
</CommandGroup>
))}
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
<div className="divide-y">
{selectedPatterns.length === 0 && (
<div className="px-4 py-8 text-center">
<p className="text-sm font-medium">
{t('agents.noEventsConfigured')}
</p>
<p className="mt-1 text-xs text-muted-foreground">
{t('agents.noEventsConfiguredDescription')}
</p>
</div>
)}
{selectedPatterns.map((pattern) => {
const compatibleTools = eventTools.filter((tool) =>
isEventToolCompatibleWithPattern(tool, pattern),
);
return (
<Collapsible
key={pattern}
open={expandedPatterns.has(pattern)}
onOpenChange={(expanded) => {
setExpandedPatterns((current) => {
const next = new Set(current);
if (expanded) next.add(pattern);
else next.delete(pattern);
return next;
});
}}
>
<div className="flex items-center gap-1 bg-muted/20 px-3 py-2">
<CollapsibleTrigger asChild>
<button
type="button"
className="flex min-w-0 flex-1 items-center gap-3 rounded-md px-1 py-1 text-left hover:bg-muted/50"
>
<span className="min-w-0 flex-1">
<span className="flex min-w-0 items-baseline gap-2">
<span className="truncate text-sm font-medium">
{eventPatternLabel(pattern, t)}
</span>
<code className="shrink-0 text-[11px] text-muted-foreground">
{pattern}
</code>
</span>
</CommandItem>
);
})}
</CommandGroup>
))}
</CommandList>
</Command>
</PopoverContent>
</Popover>
<span className="mt-0.5 block truncate text-xs text-muted-foreground">
{eventPatternDescription(pattern, t)}
</span>
</span>
<span className="shrink-0 text-xs text-emerald-700 dark:text-emerald-300">
{t('agents.eventToolsEnabledCount', {
count: compatibleTools.length,
})}
</span>
<ChevronDown
className={cn(
'size-4 shrink-0 text-muted-foreground transition-transform',
expandedPatterns.has(pattern) && 'rotate-180',
)}
/>
</button>
</CollapsibleTrigger>
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => removePattern(pattern)}
aria-label={t('agents.removeEvent')}
className="size-8 shrink-0 text-muted-foreground"
>
<X className="size-4" />
</Button>
</div>
<CollapsibleContent className="border-t px-3 py-2.5">
{!catalogAvailable ? (
<div className="rounded-lg border border-amber-300 bg-amber-50 px-3 py-3 text-sm text-amber-900 dark:border-amber-900 dark:bg-amber-950/30 dark:text-amber-200">
{t('agents.apiToolsCatalogUnavailable')}
</div>
) : compatibleTools.length === 0 ? (
<p className="rounded-lg border border-dashed px-3 py-4 text-center text-sm text-muted-foreground">
{t('agents.noEventActions')}
</p>
) : (
<div className="overflow-hidden rounded-lg border bg-background">
<div className="divide-y">
{compatibleTools.map((tool) => {
const label = extractI18nObject(tool.label);
return (
<div
key={tool.name}
className="grid grid-cols-[minmax(0,1fr)_auto] items-start gap-3 bg-background px-3 py-2.5"
>
<span className="min-w-0">
<span className="flex min-w-0 items-center gap-2 text-sm font-medium leading-5">
<span className="truncate">{label}</span>
<span className="shrink-0 text-xs font-normal text-muted-foreground">
{t(`agents.apiToolRisk.${tool.risk}`)}
</span>
</span>
</span>
<span className="flex shrink-0 items-center gap-1 pt-0.5 text-xs font-medium text-emerald-700 dark:text-emerald-300">
<Check className="size-3.5" />
{t('agents.eventToolEnabled')}
</span>
</div>
);
})}
</div>
</div>
)}
</CollapsibleContent>
</Collapsible>
);
})}
</div>
</div>
);
}
@@ -13,9 +13,14 @@ import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { useTranslation } from 'react-i18next';
import { toast } from 'sonner';
import { Bot, Loader2, SlidersHorizontal, Zap } from 'lucide-react';
import { Bot, Loader2, SlidersHorizontal, Wrench } from 'lucide-react';
import { httpClient } from '@/app/infra/http/HttpClient';
import { Agent, ApiRespPluginSystemStatus } from '@/app/infra/entities/api';
import {
Agent,
AgentPlatformTool,
ApiRespPluginSystemStatus,
PluginTool,
} from '@/app/infra/entities/api';
import {
PipelineConfigStage,
PipelineConfigTab,
@@ -36,15 +41,18 @@ import {
CardHeader,
CardTitle,
} from '@/components/ui/card';
import {
Form,
FormDescription,
FormField,
FormItem,
FormMessage,
} from '@/components/ui/form';
import { Form, FormField, FormItem, FormMessage } from '@/components/ui/form';
import AgentEventPatternPicker from './AgentEventPatternPicker';
import AgentRunnerSelect from './AgentRunnerSelect';
import AgentApiToolPicker from './AgentApiToolPicker';
const OTHER_TOOL_SCOPES = [
'platform',
'builtin',
'mcp',
'plugin',
'skill',
] as const;
export interface AgentRunnerStatus {
label: string;
@@ -62,7 +70,10 @@ interface AgentFormComponentProps {
onSupportedEventPatternsChange?: (patterns: string[]) => void;
}
export type AgentConfigSection = 'events' | 'runner' | 'runner_config';
export type AgentConfigSection =
| 'runner'
| 'runner_config'
| 'events_and_tools';
export interface AgentFormHandle {
openSection: (section: AgentConfigSection) => void;
@@ -119,6 +130,12 @@ function AgentFormComponent(
const { t } = useTranslation();
const [runnerConfigSchema, setRunnerConfigSchema] =
useState<PipelineConfigTab | null>(null);
const [platformTools, setPlatformTools] = useState<AgentPlatformTool[]>([]);
const [platformToolCatalogAvailable, setPlatformToolCatalogAvailable] =
useState(true);
const [hostTools, setHostTools] = useState<PluginTool[]>([]);
const [hostToolCatalogAvailable, setHostToolCatalogAvailable] =
useState(true);
const [pluginSystemStatus, setPluginSystemStatus] =
useState<ApiRespPluginSystemStatus | null>(null);
const [pluginStatusLoading, setPluginStatusLoading] = useState(true);
@@ -129,6 +146,7 @@ function AgentFormComponent(
useState<AgentConfigSection>('runner');
const isSavingRef = useRef(false);
const hasUnsavedChangesRef = useRef(false);
const loadedHostToolPolicyRef = useRef<string[] | undefined>(undefined);
const formSchema = z.object({
basic: z.object({
@@ -138,7 +156,9 @@ function AgentFormComponent(
}),
runner: z.record(z.string(), z.any()),
runner_config: z.record(z.string(), z.any()),
supported_event_patterns: z.array(z.string()).min(1),
supported_event_patterns: z.array(z.string()),
allowed_platform_tools: z.array(z.string()),
allowed_tools: z.array(z.string()),
});
type FormValues = z.infer<typeof formSchema>;
@@ -153,6 +173,8 @@ function AgentFormComponent(
runner: {},
runner_config: {},
supported_event_patterns: ['*'],
allowed_platform_tools: [],
allowed_tools: [],
},
});
const runnerInstallScope = `agent:${agentId}`;
@@ -188,8 +210,43 @@ function AgentFormComponent(
.then(([metadata, resp]) => {
if (cancelled) return;
setRunnerConfigSchema(metadata.runner_config ?? null);
const hasPlatformToolCatalog = Array.isArray(metadata.platform_tools);
setPlatformToolCatalogAvailable(hasPlatformToolCatalog);
const availablePlatformTools = hasPlatformToolCatalog
? metadata.platform_tools
: [];
setPlatformTools(availablePlatformTools);
const hasHostToolCatalog = Array.isArray(metadata.host_tools);
const availableHostTools: PluginTool[] = Array.isArray(
metadata.host_tools,
)
? metadata.host_tools
: [];
setHostToolCatalogAvailable(hasHostToolCatalog);
setHostTools(availableHostTools);
const agent = resp.agent;
const config = (agent.config ?? {}) as Record<string, any>;
const configuredHostTools = Array.isArray(config.allowed_tools)
? config.allowed_tools.filter(
(name): name is string => typeof name === 'string',
)
: undefined;
const configuredPlatformTools = Array.isArray(
config.allowed_platform_tools,
)
? config.allowed_platform_tools.filter(
(name): name is string => typeof name === 'string',
)
: [];
const configuredEventPatterns =
agent.supported_event_patterns ??
agent.capability?.supported_event_patterns;
const normalizedEventPatterns = Array.isArray(configuredEventPatterns)
? configuredEventPatterns.filter(
(pattern): pattern is string => typeof pattern === 'string',
)
: ['*'];
loadedHostToolPolicyRef.current = configuredHostTools;
const loadedValues: FormValues = {
basic: {
name: agent.name ?? '',
@@ -199,8 +256,15 @@ function AgentFormComponent(
runner: (config.runner as Record<string, unknown>) ?? {},
runner_config:
(config.runner_config as Record<string, unknown>) ?? {},
supported_event_patterns: agent.supported_event_patterns ??
agent.capability?.supported_event_patterns ?? ['*'],
supported_event_patterns: normalizedEventPatterns,
allowed_platform_tools: configuredPlatformTools.filter((name) => {
const tool = availablePlatformTools.find(
(candidate) => candidate.name === name,
);
return !tool || tool.scope === 'platform';
}),
allowed_tools:
configuredHostTools ?? availableHostTools.map((tool) => tool.name),
};
form.reset(loadedValues);
savedSnapshotRef.current = JSON.stringify(loadedValues);
@@ -320,9 +384,9 @@ function AgentFormComponent(
icon: SlidersHorizontal,
},
{
name: 'events',
label: t('agents.bindableEvents'),
icon: Zap,
name: 'events_and_tools',
label: t('agents.eventsAndTools'),
icon: Wrench,
},
];
@@ -500,22 +564,32 @@ function AgentFormComponent(
if (isSavingRef.current) return false;
const submittedSnapshot = JSON.stringify(values);
const runner = values.runner || {};
const config: Record<string, unknown> = {
runner,
runner_config: values.runner_config ?? {},
allowed_platform_tools: values.allowed_platform_tools,
};
if (hostToolCatalogAvailable) {
config.allowed_tools = values.allowed_tools;
} else if (loadedHostToolPolicyRef.current !== undefined) {
config.allowed_tools = loadedHostToolPolicyRef.current;
}
const agent: Partial<Agent> = {
name: values.basic.name,
description: values.basic.description ?? '',
emoji: values.basic.emoji,
component_ref: (runner.id as string) || null,
supported_event_patterns: values.supported_event_patterns,
config: {
runner,
runner_config: values.runner_config ?? {},
},
config,
};
isSavingRef.current = true;
onSavingChange?.(true);
try {
await httpClient.updateAgent(agentId, agent);
if (hostToolCatalogAvailable) {
loadedHostToolPolicyRef.current = [...values.allowed_tools];
}
savedSnapshotRef.current = submittedSnapshot;
onFinish(agent);
toast.success(t('agents.saveSuccess'));
@@ -532,7 +606,7 @@ function AgentFormComponent(
onSavingChange?.(false);
}
},
[agentId, onFinish, onSavingChange, t],
[agentId, hostToolCatalogAvailable, onFinish, onSavingChange, t],
);
function handleSubmit(values: FormValues) {
@@ -653,32 +727,79 @@ function AgentFormComponent(
</div>
)}
{activeSection === 'events' && (
{activeSection === 'events_and_tools' && (
<Card>
<CardHeader>
<CardTitle>{t('agents.bindableEvents')}</CardTitle>
<CardHeader className="pb-4">
<CardTitle>{t('agents.eventsAndTools')}</CardTitle>
<CardDescription>
{t('agents.bindableEventsDescription')}
{t('agents.eventsAndToolsDescription')}
</CardDescription>
</CardHeader>
<CardContent>
<FormField
control={form.control}
name="supported_event_patterns"
render={({ field }) => (
<FormItem>
<AgentEventPatternPicker
events={availableEventTypes}
value={field.value}
onChange={field.onChange}
/>
<FormDescription>
{t('agents.supportedEventsDescription')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<CardContent className="space-y-6">
<section className="space-y-3">
<div className="space-y-1">
<h3 className="text-sm font-medium">
{t('agents.bindableEvents')}
</h3>
<p className="text-xs text-muted-foreground">
{t('agents.bindableEventsDescription')}
</p>
</div>
<FormField
control={form.control}
name="supported_event_patterns"
render={({ field }) => (
<FormItem>
<AgentEventPatternPicker
events={availableEventTypes}
value={field.value}
onChange={field.onChange}
tools={platformTools}
catalogAvailable={platformToolCatalogAvailable}
/>
<FormMessage />
</FormItem>
)}
/>
</section>
<section className="space-y-3 border-t pt-6">
<div className="space-y-1">
<h3 className="text-sm font-medium">
{t('agents.otherTools')}
</h3>
<p className="text-xs text-muted-foreground">
{t('agents.otherToolsDescription')}
</p>
</div>
<FormField
control={form.control}
name="allowed_platform_tools"
render={({ field }) => (
<FormItem>
<AgentApiToolPicker
platformTools={platformTools}
platformValue={field.value}
onPlatformChange={field.onChange}
hostTools={hostTools}
hostValue={form.watch('allowed_tools')}
onHostChange={(value) =>
form.setValue('allowed_tools', value, {
shouldDirty: true,
shouldValidate: true,
})
}
platformCatalogAvailable={
platformToolCatalogAvailable
}
hostCatalogAvailable={hostToolCatalogAvailable}
scopes={OTHER_TOOL_SCOPES}
/>
<FormMessage />
</FormItem>
)}
/>
</section>
</CardContent>
</Card>
)}
@@ -71,7 +71,14 @@ export function eventPatternLabel(pattern: string, t: TFunction) {
export function eventPatternDescription(pattern: string, t: TFunction) {
if (pattern === '*') return t('bots.eventDescriptions.all');
if (pattern.endsWith('.*')) {
return t('bots.eventDescriptions.namespace');
const namespace = pattern.slice(0, -2);
const key = `bots.eventDescriptions.namespace_${namespace}`;
const description = t(key);
return description === key
? t('bots.eventDescriptions.namespace', {
group: eventGroupLabel(namespace, t),
})
: description;
}
const key = `bots.eventDescriptions.${pattern.replace(/\./g, '_')}`;
const description = t(key);
@@ -1301,7 +1301,7 @@ function NavItems({
disabled={quota.disabled}
aria-disabled={quota.disabled}
aria-label={`${t('common.create')} ${config.name}`}
className="p-1 rounded-sm text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground [@media(hover:hover)]:opacity-0 group-hover/category-header:opacity-100 transition-all disabled:pointer-events-none disabled:opacity-40"
className="p-1 rounded-sm text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground transition-all disabled:pointer-events-none disabled:opacity-40"
onClick={(e) => e.stopPropagation()}
>
<Plus className="size-3.5" />
@@ -1347,7 +1347,7 @@ function NavItems({
disabled={quota.disabled}
aria-disabled={quota.disabled}
aria-label={`${t('common.create')} ${config.name}`}
className="p-1 rounded-sm text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground [@media(hover:hover)]:opacity-0 group-hover/category-header:opacity-100 transition-all disabled:pointer-events-none disabled:opacity-40"
className="p-1 rounded-sm text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground transition-all disabled:pointer-events-none disabled:opacity-40"
onClick={(e) => e.stopPropagation()}
>
<Plus className="size-3.5" />
@@ -1389,7 +1389,7 @@ function NavItems({
disabled={quota.disabled}
aria-disabled={quota.disabled}
aria-label={`${t('common.create')} ${config.name}`}
className="p-1 rounded-sm text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground [@media(hover:hover)]:opacity-0 group-hover/category-header:opacity-100 transition-all disabled:pointer-events-none disabled:opacity-40"
className="p-1 rounded-sm text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground transition-all disabled:pointer-events-none disabled:opacity-40"
onClick={(e) => {
e.stopPropagation();
navigate(`${routePrefix}?id=new`);
@@ -24,6 +24,7 @@ export interface ProcessorDetailStatus {
interface ProcessorDetailWorkbenchProps {
title: string;
titleBadge?: ReactNode;
titleAction?: ReactNode;
headerActions?: ReactNode;
status?: ProcessorDetailStatus | null;
@@ -45,6 +46,7 @@ interface ProcessorDetailWorkbenchProps {
export default function ProcessorDetailWorkbench({
title,
titleBadge,
titleAction,
headerActions,
status,
@@ -79,6 +81,7 @@ export default function ProcessorDetailWorkbench({
<div className="flex shrink-0 flex-wrap items-center justify-between gap-3 pb-4">
<div className="flex min-w-0 items-center gap-2">
<h1 className="truncate text-xl font-semibold">{title}</h1>
{titleBadge}
{titleAction}
{monitoring && (
<TabsList
+14
View File
@@ -193,6 +193,8 @@ export interface ApiRespAgent {
export interface GetAgentMetadataResponseData {
runner_config?: PipelineConfigTab;
platform_tools: AgentPlatformTool[];
host_tools?: PluginTool[] | null;
kinds: Array<{
name: AgentKind;
supported_event_patterns: string[];
@@ -200,6 +202,18 @@ export interface GetAgentMetadataResponseData {
}>;
}
export interface AgentPlatformTool {
name: string;
api: string;
scope: 'event' | 'platform';
category: string;
risk: 'read' | 'write' | 'dangerous';
label: I18nObject;
description: I18nObject;
event_patterns: string[];
parameters: Record<string, unknown>;
}
export interface Pipeline {
uuid?: string;
name: string;
+63 -3
View File
@@ -553,7 +553,17 @@ const enUS = {
},
eventDescriptions: {
all: 'Matches every event received by this adapter.',
namespace: 'Matches several concrete events in the same event group.',
namespace: 'Matches all {{group}} events.',
namespace_bot:
'Matches bot invitations, removals, mutes, and other status events.',
namespace_feedback: 'Matches feedback events from a platform or user.',
namespace_friend: 'Matches friend requests and friendship changes.',
namespace_group:
'Matches member joins, leaves, removals, and other group events.',
namespace_message:
'Matches received, edited, deleted, and reaction message events.',
namespace_platform:
'Matches platform-specific events provided by an adapter.',
custom: 'A custom event, or one without a description yet.',
message_received: 'A user or group sends a new message to the bot.',
message_edited: 'The platform reports that an existing message changed.',
@@ -740,9 +750,57 @@ const enUS = {
basicInfoDescription: 'Set the name, icon and description',
runnerSettings: 'Runner',
advanced: 'Advanced',
bindableEvents: 'Bindable Event Range',
eventsAndTools: 'Events & tools',
eventsAndToolsDescription: 'Set trigger events and available tools.',
bindableEvents: 'Event scope',
bindableEventsDescription:
'Limit which bot event routes can select this Agent. The default is suitable for most cases.',
'Add an event to automatically provide its tools to the Agent.',
configuredEvents: 'Added events',
configuredEventsCount: '{{count}} total',
addEvent: 'Add event',
removeEvent: 'Remove event',
eventActions: 'Automatically enabled tools',
eventToolEnabled: 'Enabled',
eventToolsEnabledCount: '{{count}} tools enabled',
noEventActions: 'No actions are available for this event.',
noEventsConfigured: 'No events added',
noEventsConfiguredDescription: 'No event can trigger this Agent.',
noEventsConfiguredBadge: 'No events',
apiTools: 'Tool access',
apiToolsDescription: 'Choose which tools this Agent can call.',
otherTools: 'Other tools',
otherToolsDescription:
'Choose platform, sandbox, MCP, plugin, and skill tools.',
apiToolsSelected: '{{count}} selected',
apiToolsSecurityHint: 'Only enable the tools this Agent needs.',
apiToolsSearch: 'Search tools…',
eventApiTools: 'Event tools',
eventToolUnavailable: 'Unavailable',
eventApiToolsDescription:
'Targets are frozen from the current event. The Agent only supplies action parameters.',
platformApiTools: 'Platform tools',
platformApiToolsDescription:
'The Agent may choose user, group, or message IDs. Grant only what the workflow needs.',
apiToolEvents: 'Events',
apiToolParameters: 'Agent parameters',
apiToolSource: 'Source',
apiToolNoParameters: 'none',
sandboxTools: 'Sandbox',
mcpTools: 'MCP',
pluginTools: 'Plugins',
skillTools: 'Skills',
langbotBuiltIn: 'LangBot',
apiToolDetails: 'Details',
apiToolHideDetails: 'Hide',
apiToolsNoResults: 'No matching API or tool',
apiToolsCatalogUnavailable:
'The LangBot backend did not return the API tool catalog. Make sure the backend is updated and restarted; this does not mean the current platform has no tools.',
hostToolsCatalogUnavailable: 'The tool catalog is temporarily unavailable.',
apiToolRisk: {
read: 'Read only',
write: 'Action',
dangerous: 'Sensitive',
},
supportedEvents: 'Event Range',
supportedEventsDescription:
'Choose all events, an event group, or individual events. Bot routes will only list this Agent for matching events.',
@@ -793,6 +851,8 @@ const enUS = {
'Run the current Agent with a message or platform event and inspect the real output.',
debugResetSession: 'Reset session',
debugEventType: 'Event type',
debugNoEventsTitle: 'No events to debug',
debugNoEventsDescription: 'Add an event under Events & tools first.',
debugMessageReceived: 'Message received',
debugGroupMemberJoined: 'Group member joined',
debugGroupMemberLeft: 'Group member left',
+12 -1
View File
@@ -560,7 +560,18 @@ const jaJP = {
},
eventDescriptions: {
all: 'このアダプターが受信するすべてのイベントに一致します。',
namespace: '同じイベントグループ内の複数の具体イベントに一致します。',
namespace: 'すべての{{group}}イベントに一致します。',
namespace_bot:
'ボットのグループ参加、退出、ミュートなどの状態イベントに一致します。',
namespace_feedback:
'プラットフォームまたはユーザーからのフィードバックイベントに一致します。',
namespace_friend: '友達リクエストや友達関係の変更イベントに一致します。',
namespace_group:
'メンバーの参加、退出、削除などのグループイベントに一致します。',
namespace_message:
'メッセージの受信、編集、削除、リアクションイベントに一致します。',
namespace_platform:
'アダプターが提供するプラットフォーム固有イベントに一致します。',
custom: 'カスタムイベント、または説明がまだないイベントです。',
message_received:
'ユーザーまたはグループがボットへ新しいメッセージを送信します。',
+58 -4
View File
@@ -528,7 +528,13 @@ const zhHans = {
},
eventDescriptions: {
all: '匹配此适配器收到的全部事件。',
namespace: '匹配同一事件分组下的多个具体事件。',
namespace: '匹配所有{{group}}事件。',
namespace_bot: '匹配机器人入群、退群、禁言和解除禁言等状态事件。',
namespace_feedback: '匹配平台或用户反馈事件。',
namespace_friend: '匹配好友请求、好友添加成功等好友关系事件。',
namespace_group: '匹配成员加入、离开或被移出群组等群组成员事件。',
namespace_message: '匹配消息接收、编辑、删除和表态事件。',
namespace_platform: '匹配适配器提供的平台专属事件。',
custom: '自定义或暂未提供说明的事件。',
message_received: '用户或群组向机器人发送新消息。',
message_edited: '平台通知已有消息内容发生变更。',
@@ -709,9 +715,55 @@ const zhHans = {
basicInfoDescription: '设置名称、图标和描述',
runnerSettings: '运行器',
advanced: '高级',
bindableEvents: '可绑定事件范围',
bindableEventsDescription:
'限制此 Agent 可被机器人事件路由选择的事件范围。通常保持默认即可。',
eventsAndTools: '事件与工具',
eventsAndToolsDescription: '设置触发范围和可用工具。',
bindableEvents: '事件范围',
bindableEventsDescription: '添加事件后,对应工具会自动提供给 Agent。',
configuredEvents: '已添加事件',
configuredEventsCount: '共 {{count}} 项',
addEvent: '添加事件',
removeEvent: '移除事件',
eventActions: '已自动启用的工具',
eventToolEnabled: '已启用',
eventToolsEnabledCount: '已启用 {{count}} 个工具',
noEventActions: '该事件暂无可用动作。',
noEventsConfigured: '暂未添加事件',
noEventsConfiguredDescription: '此 Agent 不会被任何事件触发。',
noEventsConfiguredBadge: '未配置事件',
apiTools: '工具权限',
apiToolsDescription: '选择 Agent 可以调用的工具。',
otherTools: '其他工具',
otherToolsDescription: '选择平台、沙盒、MCP、插件和技能工具。',
apiToolsSelected: '已选 {{count}}',
apiToolsSecurityHint: '只开放实际需要的工具。',
apiToolsSearch: '搜索工具…',
eventApiTools: '事件工具',
eventToolUnavailable: '不适用',
eventApiToolsDescription:
'目标从当前事件冻结,Agent 只能提供动作参数,适合回复、审核请求和处理相关成员。',
platformApiTools: '平台工具',
platformApiToolsDescription:
'Agent 可以指定用户、群组或消息标识。请只按实际业务需要授权。',
apiToolEvents: '适用事件',
apiToolParameters: 'Agent 可填写参数',
apiToolSource: '来源',
apiToolNoParameters: '无',
sandboxTools: '沙盒',
mcpTools: 'MCP',
pluginTools: '插件',
skillTools: '技能',
langbotBuiltIn: 'LangBot',
apiToolDetails: '详情',
apiToolHideDetails: '收起',
apiToolsNoResults: '没有匹配的 API 或工具',
apiToolsCatalogUnavailable:
'LangBot 主程序没有返回 API 工具目录。请确认后端已更新并重启;该状态不代表当前平台没有可用工具。',
hostToolsCatalogUnavailable: '暂时无法加载工具目录。',
apiToolRisk: {
read: '只读',
write: '操作',
dangerous: '敏感',
},
supportedEvents: '事件范围',
supportedEventsDescription:
'选择全部事件、事件组或具体事件。机器人路由只会在匹配的事件中显示此 Agent。',
@@ -758,6 +810,8 @@ const zhHans = {
debugDescription: '用消息或平台事件直接运行当前 Agent,并查看真实输出。',
debugResetSession: '重置会话',
debugEventType: '事件类型',
debugNoEventsTitle: '暂无可调试事件',
debugNoEventsDescription: '请先在事件与工具中添加一个事件。',
debugMessageReceived: '收到消息',
debugGroupMemberJoined: '成员加入群组',
debugGroupMemberLeft: '成员离开群组',