mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-16 14:57:15 +00:00
feat(agent): add event-aware tool permissions
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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:
|
||||
'ユーザーまたはグループがボットへ新しいメッセージを送信します。',
|
||||
|
||||
@@ -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: '成员离开群组',
|
||||
|
||||
Reference in New Issue
Block a user