mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-21 07:55:18 +08:00
fix(acceptance): refine processor setup and dashboard monitoring
This commit is contained in:
@@ -24,6 +24,14 @@ def parse_iso_datetime(datetime_str: str | None) -> datetime.datetime | None:
|
||||
return dt
|
||||
|
||||
|
||||
def validate_monitoring_window(start_time: datetime.datetime | None, end_time: datetime.datetime | None) -> None:
|
||||
"""Bound explicit dashboard windows to one year without silently truncating them."""
|
||||
if start_time is None:
|
||||
return
|
||||
end = end_time or datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None)
|
||||
if start_time >= end or end - start_time > datetime.timedelta(days=365):
|
||||
quart.abort(400, description='Monitoring time range must be positive and no longer than 365 days')
|
||||
|
||||
@group.group_class('monitoring', '/api/v1/monitoring')
|
||||
class MonitoringRouterGroup(group.RouterGroup):
|
||||
async def initialize(self) -> None:
|
||||
@@ -39,6 +47,7 @@ class MonitoringRouterGroup(group.RouterGroup):
|
||||
# Parse datetime
|
||||
start_time = parse_iso_datetime(start_time_str)
|
||||
end_time = parse_iso_datetime(end_time_str)
|
||||
validate_monitoring_window(start_time, end_time)
|
||||
|
||||
metrics = await self.ap.monitoring_service.get_overview_metrics(
|
||||
request_context,
|
||||
@@ -61,6 +70,7 @@ class MonitoringRouterGroup(group.RouterGroup):
|
||||
mode = quart.request.args.get('mode', 'all')
|
||||
start_time = parse_iso_datetime(quart.request.args.get('startTime'))
|
||||
end_time = parse_iso_datetime(quart.request.args.get('endTime'))
|
||||
validate_monitoring_window(start_time, end_time)
|
||||
|
||||
result = await self.ap.monitoring_service.get_executions(
|
||||
request_context,
|
||||
@@ -111,6 +121,7 @@ class MonitoringRouterGroup(group.RouterGroup):
|
||||
|
||||
start_time = parse_iso_datetime(start_time_str)
|
||||
end_time = parse_iso_datetime(end_time_str)
|
||||
validate_monitoring_window(start_time, end_time)
|
||||
|
||||
stats = await self.ap.monitoring_service.get_token_statistics(
|
||||
request_context,
|
||||
@@ -146,6 +157,7 @@ class MonitoringRouterGroup(group.RouterGroup):
|
||||
# Parse datetime
|
||||
start_time = parse_iso_datetime(start_time_str)
|
||||
end_time = parse_iso_datetime(end_time_str)
|
||||
validate_monitoring_window(start_time, end_time)
|
||||
|
||||
messages, total = await self.ap.monitoring_service.get_messages(
|
||||
request_context,
|
||||
@@ -187,6 +199,7 @@ class MonitoringRouterGroup(group.RouterGroup):
|
||||
# Parse datetime
|
||||
start_time = parse_iso_datetime(start_time_str)
|
||||
end_time = parse_iso_datetime(end_time_str)
|
||||
validate_monitoring_window(start_time, end_time)
|
||||
|
||||
llm_calls, total = await self.ap.monitoring_service.get_llm_calls(
|
||||
request_context,
|
||||
@@ -226,6 +239,7 @@ class MonitoringRouterGroup(group.RouterGroup):
|
||||
|
||||
start_time = parse_iso_datetime(start_time_str)
|
||||
end_time = parse_iso_datetime(end_time_str)
|
||||
validate_monitoring_window(start_time, end_time)
|
||||
|
||||
tool_calls, total = await self.ap.monitoring_service.get_tool_calls(
|
||||
request_context,
|
||||
@@ -266,6 +280,7 @@ class MonitoringRouterGroup(group.RouterGroup):
|
||||
# Parse datetime
|
||||
start_time = parse_iso_datetime(start_time_str)
|
||||
end_time = parse_iso_datetime(end_time_str)
|
||||
validate_monitoring_window(start_time, end_time)
|
||||
|
||||
embedding_calls, total = await self.ap.monitoring_service.get_embedding_calls(
|
||||
request_context,
|
||||
@@ -307,6 +322,7 @@ class MonitoringRouterGroup(group.RouterGroup):
|
||||
# Parse datetime
|
||||
start_time = parse_iso_datetime(start_time_str)
|
||||
end_time = parse_iso_datetime(end_time_str)
|
||||
validate_monitoring_window(start_time, end_time)
|
||||
|
||||
# Parse is_active
|
||||
is_active = None
|
||||
@@ -354,6 +370,7 @@ class MonitoringRouterGroup(group.RouterGroup):
|
||||
# Parse datetime
|
||||
start_time = parse_iso_datetime(start_time_str)
|
||||
end_time = parse_iso_datetime(end_time_str)
|
||||
validate_monitoring_window(start_time, end_time)
|
||||
|
||||
errors, total = await self.ap.monitoring_service.get_errors(
|
||||
request_context,
|
||||
@@ -391,6 +408,7 @@ class MonitoringRouterGroup(group.RouterGroup):
|
||||
# Parse datetime
|
||||
start_time = parse_iso_datetime(start_time_str)
|
||||
end_time = parse_iso_datetime(end_time_str)
|
||||
validate_monitoring_window(start_time, end_time)
|
||||
|
||||
# Get overview metrics
|
||||
overview = await self.ap.monitoring_service.get_overview_metrics(
|
||||
@@ -512,6 +530,7 @@ class MonitoringRouterGroup(group.RouterGroup):
|
||||
"""Get detailed analysis for a specific session"""
|
||||
start_time = parse_iso_datetime(quart.request.args.get('startTime'))
|
||||
end_time = parse_iso_datetime(quart.request.args.get('endTime'))
|
||||
validate_monitoring_window(start_time, end_time)
|
||||
analysis = await self.ap.monitoring_service.get_session_analysis(
|
||||
request_context,
|
||||
session_id,
|
||||
@@ -553,6 +572,7 @@ class MonitoringRouterGroup(group.RouterGroup):
|
||||
# Parse datetime
|
||||
start_time = parse_iso_datetime(start_time_str)
|
||||
end_time = parse_iso_datetime(end_time_str)
|
||||
validate_monitoring_window(start_time, end_time)
|
||||
|
||||
# Get data based on export type
|
||||
if export_type == 'messages':
|
||||
@@ -743,6 +763,7 @@ class MonitoringRouterGroup(group.RouterGroup):
|
||||
# Parse datetime
|
||||
start_time = parse_iso_datetime(start_time_str)
|
||||
end_time = parse_iso_datetime(end_time_str)
|
||||
validate_monitoring_window(start_time, end_time)
|
||||
|
||||
stats = await self.ap.monitoring_service.get_feedback_stats(
|
||||
request_context,
|
||||
@@ -771,6 +792,7 @@ class MonitoringRouterGroup(group.RouterGroup):
|
||||
# Parse datetime
|
||||
start_time = parse_iso_datetime(start_time_str)
|
||||
end_time = parse_iso_datetime(end_time_str)
|
||||
validate_monitoring_window(start_time, end_time)
|
||||
|
||||
# Parse feedback type
|
||||
feedback_type = int(feedback_type_str) if feedback_type_str else None
|
||||
|
||||
@@ -531,6 +531,7 @@ class AgentService:
|
||||
|
||||
async def _prepare_event_processor(self, context, data, existing=None):
|
||||
"""Resolve an installed component and keep its capability declaration authoritative."""
|
||||
draft = existing is None and 'config' not in data and 'parameters' not in data
|
||||
config = copy.deepcopy(data.get('config', existing.config if existing is not None else {}))
|
||||
if not isinstance(config, dict):
|
||||
raise ValueError('Processor configuration must be an object')
|
||||
@@ -562,10 +563,12 @@ class AgentService:
|
||||
if not isinstance(parameters, dict):
|
||||
raise ValueError('Processor parameters must be an object')
|
||||
for field in descriptor.config_schema:
|
||||
if field.get('required') and parameters.get(field['name']) in (None, ''):
|
||||
if not draft and field.get('required') and parameters.get(field['name']) in (None, ''):
|
||||
raise ValueError(f'Required processor parameter: {field["name"]}')
|
||||
config['runner_config'] = {component_ref: parameters}
|
||||
return config, component_ref, descriptor.supported_event_patterns
|
||||
# Component-only creation keeps defaults for the detail form, but does
|
||||
# not subscribe to events until configuration is saved and validated.
|
||||
return config, component_ref, [] if draft else descriptor.supported_event_patterns
|
||||
|
||||
async def get_processor_runs(self, context, processor_id, *, before_id=None):
|
||||
"""Read only this Workspace's Agent or plugin processor runs."""
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import EntityLoadState from '@/components/EntityLoadState';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toast } from 'sonner';
|
||||
import { AlertTriangle, Trash2 } from 'lucide-react';
|
||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||
import { useCurrentWorkspace } from '@/app/infra/http';
|
||||
import { useCurrentWorkspace, userInfo } from '@/app/infra/http';
|
||||
import { Agent, AgentPlatformTool } from '@/app/infra/entities/api';
|
||||
import { useSidebarData } from '@/app/home/components/home-sidebar/SidebarDataContext';
|
||||
import ProcessorDetailWorkbench from '@/app/home/components/processor-detail/ProcessorDetailWorkbench';
|
||||
@@ -33,6 +33,9 @@ import AgentFormComponent, {
|
||||
RunnerStatus,
|
||||
} from './components/AgentFormComponent';
|
||||
|
||||
// Keep the last checked display state across navigation within this app session.
|
||||
const runnerStatusCache = new Map<string, RunnerStatus>();
|
||||
|
||||
export default function AgentDetailContent({
|
||||
id,
|
||||
pipelineRevision,
|
||||
@@ -42,7 +45,7 @@ export default function AgentDetailContent({
|
||||
}) {
|
||||
const isCreateMode = id === 'new';
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
const { t, i18n } = useTranslation();
|
||||
const currentWorkspace = useCurrentWorkspace();
|
||||
const canManage =
|
||||
currentWorkspace?.permissions.includes('resource.manage') ?? false;
|
||||
@@ -59,7 +62,32 @@ export default function AgentDetailContent({
|
||||
const [basicInfoOpen, setBasicInfoOpen] = useState(false);
|
||||
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
const [runnerStatus, setRunnerStatus] = useState<RunnerStatus | null>(null);
|
||||
const runnerStatusKey = JSON.stringify([
|
||||
userInfo?.account_uuid, currentWorkspace?.workspace.uuid, id, i18n.language,
|
||||
]);
|
||||
const [runnerStatusState, setRunnerStatusState] = useState<{
|
||||
key: string; status: RunnerStatus;
|
||||
} | null>(null);
|
||||
const runnerStatus = runnerStatusState?.key === runnerStatusKey
|
||||
? runnerStatusState.status : runnerStatusCache.get(runnerStatusKey) ?? null;
|
||||
const updateRunnerStatus = useCallback((status: RunnerStatus) => {
|
||||
setRunnerStatusState((previous) => {
|
||||
if (status.tone === 'neutral') {
|
||||
const cached = previous?.key === runnerStatusKey
|
||||
? previous.status : runnerStatusCache.get(runnerStatusKey);
|
||||
if (cached) return { key: runnerStatusKey, status: cached };
|
||||
}
|
||||
return { key: runnerStatusKey, status };
|
||||
});
|
||||
if (status.tone !== 'neutral' && !formDirty) {
|
||||
runnerStatusCache.delete(runnerStatusKey);
|
||||
runnerStatusCache.set(runnerStatusKey, status);
|
||||
if (runnerStatusCache.size > 100) {
|
||||
const oldest = runnerStatusCache.keys().next().value;
|
||||
if (oldest !== undefined) runnerStatusCache.delete(oldest);
|
||||
}
|
||||
}
|
||||
}, [runnerStatusKey, formDirty]);
|
||||
const [availableEventTypes, setAvailableEventTypes] = useState<string[]>([
|
||||
'message.received',
|
||||
]);
|
||||
@@ -79,10 +107,6 @@ export default function AgentDetailContent({
|
||||
return () => setDetailEntityName(null);
|
||||
}, [id, isCreateMode, pipelines, setDetailEntityName, t]);
|
||||
|
||||
useEffect(() => {
|
||||
setRunnerStatus(null);
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isCreateMode) return;
|
||||
let cancelled = false;
|
||||
@@ -258,7 +282,7 @@ export default function AgentDetailContent({
|
||||
}}
|
||||
onDirtyChange={setFormDirty}
|
||||
onSavingChange={setFormSaving}
|
||||
onRunnerStatusChange={setRunnerStatus}
|
||||
onRunnerStatusChange={updateRunnerStatus}
|
||||
onSupportedEventPatternsChange={setSupportedEventPatterns}
|
||||
onPlatformToolsChange={setPlatformTools}
|
||||
guideEnabled={canManage}
|
||||
|
||||
@@ -74,7 +74,10 @@ export default function PluginProcessorDetailContent({
|
||||
componentRef,
|
||||
parameters: initialParameters,
|
||||
});
|
||||
const dirty =
|
||||
const [needsInitialSave, setNeedsInitialSave] = useState(
|
||||
Boolean(agent.component_ref) && !agent.supported_event_patterns?.length,
|
||||
);
|
||||
const dirty = needsInitialSave ||
|
||||
componentRef !== savedConfig.componentRef ||
|
||||
!isEqual(parameters, savedConfig.parameters);
|
||||
const [runs, setRuns] = useState<ProcessorRun[]>([]);
|
||||
@@ -341,6 +344,7 @@ export default function PluginProcessorDetailContent({
|
||||
toast.success(t('agents.saveSuccess'));
|
||||
onSaved();
|
||||
setSavedConfig({ componentRef, parameters });
|
||||
setNeedsInitialSave(false);
|
||||
await load();
|
||||
return true;
|
||||
} catch {
|
||||
|
||||
@@ -206,6 +206,21 @@ export default function AgentApiToolPicker({
|
||||
? platformCatalogAvailable
|
||||
: hostCatalogAvailable;
|
||||
|
||||
const isSelected = (tool: ToolEntry) =>
|
||||
(tool.kind === 'platform' ? selectedPlatform : selectedHost).has(tool.name);
|
||||
|
||||
const setGroup = (tools: ToolEntry[], checked: boolean) => {
|
||||
const platform = new Set(platformValue);
|
||||
const host = new Set(hostValue);
|
||||
for (const tool of tools) {
|
||||
const selection = tool.kind === 'platform' ? platform : host;
|
||||
if (checked) selection.add(tool.name);
|
||||
else selection.delete(tool.name);
|
||||
}
|
||||
if (tools.some((tool) => tool.kind === 'platform')) onPlatformChange([...platform]);
|
||||
if (tools.some((tool) => tool.kind === 'host')) onHostChange([...host]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
@@ -275,8 +290,16 @@ export default function AgentApiToolPicker({
|
||||
<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 className="flex items-center justify-between gap-3 bg-muted/30 px-3 py-2 text-xs font-medium text-muted-foreground">
|
||||
<span>{groupLabel(group)}</span>
|
||||
<label className="flex cursor-pointer items-center gap-2">
|
||||
<Checkbox
|
||||
checked={groupTools.every(isSelected) ? true : groupTools.some(isSelected) ? 'indeterminate' : false}
|
||||
onCheckedChange={(checked) => setGroup(groupTools, checked === true)}
|
||||
aria-label={`${groupLabel(group)} · ${t('agents.apiToolsSelectAll')}`}
|
||||
/>
|
||||
{t('agents.apiToolsSelectAll')}
|
||||
</label>
|
||||
</div>
|
||||
<div className="divide-y">
|
||||
{groupTools.map((tool) => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
@@ -27,6 +27,10 @@ import {
|
||||
import { Input } from '@/components/ui/input';
|
||||
import EmojiPicker from '@/components/ui/emoji-picker';
|
||||
import ProcessorTypeDiagram from './ProcessorTypeDiagram';
|
||||
import RunnerSelect from './RunnerSelect';
|
||||
import type { GetAgentMetadataResponseData } from '@/app/infra/entities/api';
|
||||
import { getDefaultValues } from '@/app/home/components/dynamic-form/DynamicFormItemConfig';
|
||||
import { extractI18nObject } from '@/i18n/I18nProvider';
|
||||
|
||||
export default function AgentCreateContent({
|
||||
onCreated,
|
||||
@@ -34,10 +38,24 @@ export default function AgentCreateContent({
|
||||
onCreated: (agentId: string) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [kind, setKind] = useState<AgentKind>('agent');
|
||||
const [kind, setKind] = useState<AgentKind>('pipeline');
|
||||
const [metadata, setMetadata] = useState<GetAgentMetadataResponseData>();
|
||||
const [runner, setRunner] = useState('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const createdPipeline = useRef<string | null>(null);
|
||||
const loadMetadata = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try { setMetadata(await httpClient.getAgentMetadata()); }
|
||||
catch { toast.error(t('agents.createError')); }
|
||||
finally { setLoading(false); }
|
||||
}, [t]);
|
||||
useEffect(() => { void loadMetadata(); }, [loadMetadata]);
|
||||
const runnerOptions = kind === 'event_processor'
|
||||
? (metadata?.event_processors ?? []).map((item) => ({ name: item.id, label: { en_US: item.id, ...item.label } }))
|
||||
: metadata?.runner_config?.stages.find((stage) => stage.name === 'runner')?.config.find((item) => item.name === 'id')?.options ?? [];
|
||||
const selectorLabel = t(kind === 'event_processor' ? 'agents.selectProcessorPlugin' : 'agents.selectAgentRunner');
|
||||
const formSchema = z.object({
|
||||
name: z.string().min(1, { message: t('agents.nameRequired') }),
|
||||
description: z.string().optional(),
|
||||
emoji: z.string().optional(),
|
||||
});
|
||||
type FormValues = z.infer<typeof formSchema>;
|
||||
@@ -45,11 +63,21 @@ export default function AgentCreateContent({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
name: '',
|
||||
description: '',
|
||||
emoji: '🤖',
|
||||
emoji: '⚙️',
|
||||
},
|
||||
});
|
||||
|
||||
function selectRunner(value: string, name?: string) {
|
||||
setRunner(value);
|
||||
if (!form.getValues('name').trim()) {
|
||||
const option = runnerOptions.find((item) => item.name === value);
|
||||
const displayName = name || (option ? extractI18nObject(option.label) : '');
|
||||
if (displayName) {
|
||||
form.setValue('name', displayName, { shouldDirty: true, shouldValidate: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleKindChange(nextKind: AgentKind) {
|
||||
const previousDefaultEmoji =
|
||||
kind === 'pipeline' ? '⚙️' : kind === 'event_processor' ? '🧩' : '🤖';
|
||||
@@ -59,6 +87,9 @@ export default function AgentCreateContent({
|
||||
: nextKind === 'event_processor'
|
||||
? '🧩'
|
||||
: '🤖';
|
||||
if ((kind === 'event_processor') !== (nextKind === 'event_processor')) {
|
||||
setRunner('');
|
||||
}
|
||||
setKind(nextKind);
|
||||
const currentEmoji = form.getValues('emoji');
|
||||
if (!currentEmoji || currentEmoji === previousDefaultEmoji) {
|
||||
@@ -67,28 +98,34 @@ export default function AgentCreateContent({
|
||||
}
|
||||
|
||||
async function handleSubmit(values: FormValues) {
|
||||
return httpClient
|
||||
.createAgent({
|
||||
kind,
|
||||
name: values.name,
|
||||
description: values.description ?? '',
|
||||
emoji:
|
||||
values.emoji ||
|
||||
(kind === 'pipeline'
|
||||
? '⚙️'
|
||||
: kind === 'event_processor'
|
||||
? '🧩'
|
||||
: '🤖'),
|
||||
})
|
||||
.then((resp) => {
|
||||
if (!runner) { toast.error(selectorLabel); return; }
|
||||
const runnerStage = metadata?.runner_config?.stages.find((stage) => stage.name === runner);
|
||||
const parameters = runnerStage ? getDefaultValues(runnerStage.config) : {};
|
||||
const config = { runner: { id: runner }, runner_config: { [runner]: parameters } };
|
||||
try {
|
||||
if (kind === 'pipeline') {
|
||||
// Keep the created ID if configuration fails, so retry does not create duplicates.
|
||||
const uuid = createdPipeline.current ?? (await httpClient.createAgent({ kind, name: values.name, description: '', emoji: values.emoji })).uuid;
|
||||
createdPipeline.current = uuid;
|
||||
const { pipeline } = await httpClient.getPipeline(uuid);
|
||||
await httpClient.updatePipeline(uuid, {
|
||||
name: values.name, emoji: values.emoji,
|
||||
config: { ...pipeline.config, ai: { ...pipeline.config.ai, ...config } },
|
||||
});
|
||||
toast.success(t('agents.createSuccess'));
|
||||
onCreated(resp.uuid);
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(t('agents.createError') + err.msg);
|
||||
});
|
||||
onCreated(uuid);
|
||||
} else {
|
||||
const response = await httpClient.createAgent({
|
||||
kind, name: values.name, description: '', emoji: values.emoji,
|
||||
component_ref: runner, ...(kind === 'event_processor' ? {} : { config }),
|
||||
});
|
||||
toast.success(t('agents.createSuccess'));
|
||||
onCreated(response.uuid);
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(t('agents.createError') + String((error as { msg?: string }).msg ?? error));
|
||||
}
|
||||
}
|
||||
|
||||
const typeOptions = [
|
||||
{
|
||||
kind: 'pipeline' as const,
|
||||
@@ -118,7 +155,7 @@ export default function AgentCreateContent({
|
||||
<Button
|
||||
type="submit"
|
||||
form="agent-create-form"
|
||||
disabled={form.formState.isSubmitting}
|
||||
disabled={form.formState.isSubmitting || loading || !runner}
|
||||
>
|
||||
{t('common.submit')}
|
||||
</Button>
|
||||
@@ -157,8 +194,8 @@ export default function AgentCreateContent({
|
||||
{typeOptions.map((option) => {
|
||||
const Icon = option.icon;
|
||||
return (
|
||||
<div key={option.kind} className="relative w-full">
|
||||
<ToggleGroupItem
|
||||
key={option.kind}
|
||||
value={option.kind}
|
||||
data-processor-kind={option.kind}
|
||||
aria-label={`${option.title} ${option.description}`}
|
||||
@@ -176,6 +213,7 @@ export default function AgentCreateContent({
|
||||
</span>
|
||||
</span>
|
||||
</ToggleGroupItem>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</ToggleGroup>
|
||||
@@ -185,7 +223,7 @@ export default function AgentCreateContent({
|
||||
<CardHeader>
|
||||
<CardTitle>{t('agents.basicInfo')}</CardTitle>
|
||||
<CardDescription>
|
||||
{t('agents.basicInfoDescription')}
|
||||
{t('agents.createBasicInfoDescription')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -195,6 +233,27 @@ export default function AgentCreateContent({
|
||||
onSubmit={form.handleSubmit(handleSubmit)}
|
||||
className="space-y-4"
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium">{selectorLabel}</p>
|
||||
<RunnerSelect
|
||||
key={kind === 'event_processor' ? 'event' : 'agent'}
|
||||
usage={kind === 'event_processor' ? 'event' : 'agent'}
|
||||
options={runnerOptions}
|
||||
label={selectorLabel}
|
||||
value={runner}
|
||||
onValueChange={selectRunner}
|
||||
disabled={loading || form.formState.isSubmitting}
|
||||
installScope={`processor-create-${kind === 'event_processor' ? 'event' : 'agent'}`}
|
||||
onInstalled={(installed) => {
|
||||
setMetadata((previous) => previous ? { ...previous, runner_config: installed.configTab } : previous);
|
||||
selectRunner(installed.runner.name, extractI18nObject(installed.runner.label));
|
||||
}}
|
||||
onEventProcessorInstalled={(component) => {
|
||||
setMetadata((previous) => previous ? { ...previous, event_processors: [...(previous.event_processors ?? []).filter((item) => item.id !== component.id), component] } : previous);
|
||||
selectRunner(component.id, extractI18nObject({ en_US: component.id, ...component.label }));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-4 items-start">
|
||||
<FormField
|
||||
control={form.control}
|
||||
@@ -230,21 +289,9 @@ export default function AgentCreateContent({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('common.description')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} value={field.value ?? ''} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -521,6 +521,18 @@ export default function RunnerSelect({
|
||||
</div>
|
||||
)}
|
||||
</SelectGroup>
|
||||
<SelectSeparator />
|
||||
<a
|
||||
href="https://langbot.app/docs/zh/plugin/dev/components/runner"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2 rounded-sm px-2 py-2 text-xs text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
{t(usage === 'event' ? 'bots.routingHelp.docs' : 'bots.routingHelp.runnerDocs')}
|
||||
<ExternalLink className="size-3" />
|
||||
</a>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
|
||||
@@ -36,6 +36,8 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import EventBindingsEditor from './EventBindingsEditor';
|
||||
import EventRoutingHelp from './EventRoutingHelp';
|
||||
import LegacyAdapterHelp from './LegacyAdapterHelp';
|
||||
import PluginProcessorBindings from './PluginProcessorBindings';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
@@ -866,10 +868,11 @@ const BotForm = forwardRef<BotFormHandle, BotFormProps>(function BotForm(
|
||||
className="border-t pt-5"
|
||||
data-adapter-category="legacy"
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowLegacyAdapters((value) => !value)}
|
||||
className="flex w-full items-center gap-2 py-2 text-left text-sm font-medium text-muted-foreground hover:text-foreground"
|
||||
className="flex items-center gap-2 py-2 text-left text-sm font-medium text-muted-foreground hover:text-foreground"
|
||||
aria-expanded={showLegacyAdapters || !!adapterQuery}
|
||||
>
|
||||
{showLegacyAdapters || adapterQuery ? (
|
||||
@@ -882,6 +885,8 @@ const BotForm = forwardRef<BotFormHandle, BotFormProps>(function BotForm(
|
||||
{galleryLegacyAdapters.length}
|
||||
</span>
|
||||
</button>
|
||||
<LegacyAdapterHelp />
|
||||
</div>
|
||||
{(showLegacyAdapters || !!adapterQuery) && (
|
||||
<div className="mt-3 grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{galleryLegacyAdapters.map((item) => (
|
||||
@@ -1220,7 +1225,7 @@ const BotForm = forwardRef<BotFormHandle, BotFormProps>(function BotForm(
|
||||
)}
|
||||
>
|
||||
<CardHeader>
|
||||
<CardTitle>{t('bots.eventRouting')}</CardTitle>
|
||||
<CardTitle className="flex items-center gap-1.5">{t('bots.eventRouting')}<EventRoutingHelp /></CardTitle>
|
||||
<CardDescription>
|
||||
{t('bots.eventRoutingDescription')}
|
||||
</CardDescription>
|
||||
|
||||
@@ -1179,7 +1179,7 @@ function RouteDryRunDialog({
|
||||
interface BindingCardProps {
|
||||
binding: EventBinding;
|
||||
globalIndex: number;
|
||||
routeStatus?: BotEventRouteStatus;
|
||||
|
||||
eventOptions: string[];
|
||||
agentOptions: Agent[];
|
||||
expandedIds: Set<string>;
|
||||
@@ -1193,7 +1193,6 @@ interface BindingCardProps {
|
||||
function BindingCardContent({
|
||||
binding,
|
||||
globalIndex,
|
||||
routeStatus,
|
||||
eventOptions,
|
||||
agentOptions,
|
||||
expandedIds,
|
||||
@@ -1209,8 +1208,6 @@ function BindingCardContent({
|
||||
const isExpanded = expandedIds.has(id);
|
||||
const filterCount = (binding.filters as FilterRow[] | undefined)?.length ?? 0;
|
||||
const pipelineAllowed = isMessageEventPattern(binding.event_pattern);
|
||||
const statusTime = formatRouteStatusTime(routeStatus?.timestamp);
|
||||
const statusDetail = routeStatusDetail(routeStatus, t);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -1342,23 +1339,6 @@ function BindingCardContent({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 border-t px-3 py-1.5">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={`rounded-md px-2 py-0.5 text-[10px] font-medium ${routeStatusBadgeClass(
|
||||
routeStatus?.last_status,
|
||||
)}`}
|
||||
>
|
||||
{routeStatusLabel(routeStatus?.last_status, t)}
|
||||
</Badge>
|
||||
<span
|
||||
className="min-w-0 truncate text-[11px] text-muted-foreground"
|
||||
title={statusTime ? `${statusDetail} · ${statusTime}` : statusDetail}
|
||||
>
|
||||
{statusDetail || statusTime}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* conditions panel */}
|
||||
{isExpanded && (
|
||||
<div className="border-t px-3 py-2.5">
|
||||
@@ -1429,6 +1409,7 @@ export default function EventBindingsEditor({
|
||||
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
|
||||
const [disabledSectionOpen, setDisabledSectionOpen] = useState(false);
|
||||
const [activeId, setActiveId] = useState<string | null>(null);
|
||||
const [recordsOpen, setRecordsOpen] = useState(false);
|
||||
const [routeStatuses, setRouteStatuses] = useState<BotEventRouteStatus[]>([]);
|
||||
const [routeStatusLoading, setRouteStatusLoading] = useState(false);
|
||||
const [routeStatusError, setRouteStatusError] = useState<string | null>(null);
|
||||
@@ -1455,13 +1436,7 @@ export default function EventBindingsEditor({
|
||||
() => (supportedEvents.length > 0 ? supportedEvents : DEFAULT_EVENTS),
|
||||
[supportedEvents],
|
||||
);
|
||||
const routeStatusByBinding = useMemo(() => {
|
||||
const map = new Map<string, BotEventRouteStatus>();
|
||||
routeStatuses.forEach((status) => {
|
||||
if (status.binding_id) map.set(String(status.binding_id), status);
|
||||
});
|
||||
return map;
|
||||
}, [routeStatuses]);
|
||||
|
||||
const routeConflicts = useMemo(
|
||||
() => findRouteConflicts(bindings),
|
||||
[bindings],
|
||||
@@ -1496,7 +1471,9 @@ export default function EventBindingsEditor({
|
||||
setRouteStatusError(null);
|
||||
try {
|
||||
const response = await backendClient.getBotEventRouteStatuses(botId);
|
||||
setRouteStatuses(response.routes || []);
|
||||
setRouteStatuses([...response.routes, ...response.unmatched_events, ...response.stale_routes]
|
||||
.filter((record) => record.last_status)
|
||||
.sort((a, b) => (b.timestamp ?? 0) - (a.timestamp ?? 0)));
|
||||
} catch (error) {
|
||||
console.error('Failed to refresh Bot event route status', error);
|
||||
setRouteStatusError(t('bots.routeStatusRefreshFailed'));
|
||||
@@ -1506,8 +1483,8 @@ export default function EventBindingsEditor({
|
||||
}, [botId, t]);
|
||||
|
||||
useEffect(() => {
|
||||
refreshRouteStatuses();
|
||||
}, [refreshRouteStatuses]);
|
||||
if (recordsOpen) void refreshRouteStatuses();
|
||||
}, [recordsOpen, refreshRouteStatuses]);
|
||||
|
||||
function updateBindings(next: EventBinding[]) {
|
||||
form.setValue('event_bindings', next, { shouldDirty: true });
|
||||
@@ -1674,11 +1651,6 @@ export default function EventBindingsEditor({
|
||||
sortableId={idsRef.current[sortIdx]}
|
||||
binding={binding}
|
||||
globalIndex={globalIdx}
|
||||
routeStatus={
|
||||
binding.id
|
||||
? routeStatusByBinding.get(String(binding.id))
|
||||
: undefined
|
||||
}
|
||||
eventOptions={eventOptions}
|
||||
agentOptions={agentOptions}
|
||||
expandedIds={expandedIds}
|
||||
@@ -1695,11 +1667,6 @@ export default function EventBindingsEditor({
|
||||
<BindingCardContent
|
||||
binding={activeBinding}
|
||||
globalIndex={activeGlobalIdx}
|
||||
routeStatus={
|
||||
activeBinding.id
|
||||
? routeStatusByBinding.get(String(activeBinding.id))
|
||||
: undefined
|
||||
}
|
||||
eventOptions={eventOptions}
|
||||
agentOptions={agentOptions}
|
||||
expandedIds={expandedIds}
|
||||
@@ -1791,26 +1758,43 @@ export default function EventBindingsEditor({
|
||||
eventOptions={dryRunEventOptions}
|
||||
agentOptions={agentOptions}
|
||||
/>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={`size-8 ${routeStatusError ? 'text-destructive' : 'text-muted-foreground'}`}
|
||||
aria-label={t('bots.refreshRouteStatus')}
|
||||
onClick={refreshRouteStatuses}
|
||||
disabled={!botId || routeStatusLoading}
|
||||
>
|
||||
<RefreshCw
|
||||
className={`h-4 w-4 ${routeStatusLoading ? 'animate-spin' : ''}`}
|
||||
/>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{routeStatusError || t('bots.refreshRouteStatus')}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Button type="button" variant="outline" size="sm" disabled={!botId} onClick={() => setRecordsOpen(true)}>
|
||||
<ListChecks className="mr-1 size-4" />
|
||||
{t('bots.matchRecords')}
|
||||
</Button>
|
||||
<Dialog open={recordsOpen} onOpenChange={setRecordsOpen}>
|
||||
<DialogContent className="sm:max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('bots.matchRecords')}</DialogTitle>
|
||||
<DialogDescription>{t('bots.matchRecordsDescription')}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex justify-end">
|
||||
<Button type="button" variant="outline" size="sm" onClick={refreshRouteStatuses} disabled={routeStatusLoading}>
|
||||
<RefreshCw className={`mr-2 size-4 ${routeStatusLoading ? 'animate-spin' : ''}`} />
|
||||
{t('monitoring.refreshData')}
|
||||
</Button>
|
||||
</div>
|
||||
{routeStatusError && <p role="alert" className="text-sm text-destructive">{routeStatusError}</p>}
|
||||
<div className="max-h-[60vh] space-y-3 overflow-y-auto">
|
||||
{routeStatuses.map((record, index) => (
|
||||
<div key={`${record.binding_id}:${record.seq_id}:${index}`} className="space-y-2 rounded-lg border p-3">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="flex-1 text-sm font-medium">{eventLabel(record.event_type || record.event_pattern || '*', t)}</span>
|
||||
<Badge variant="outline" className={routeStatusBadgeClass(record.last_status)}>{routeStatusLabel(record.last_status, t)}</Badge>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">{formatRouteStatusTime(record.timestamp)}</div>
|
||||
{record.target_type && <div className="break-words text-sm">{record.target_type}{record.target_uuid ? ` · ${agentOptions.find((agent) => agent.uuid === record.target_uuid)?.name || record.target_uuid}` : ''}</div>}
|
||||
<p className="text-sm text-muted-foreground">{routeStatusDetail(record, t)}</p>
|
||||
<details className="text-xs text-muted-foreground">
|
||||
<summary className="cursor-pointer">{t('monitoring.unified.metadata')}</summary>
|
||||
<pre className="mt-2 whitespace-pre-wrap break-words [overflow-wrap:anywhere]">{JSON.stringify(record, null, 2)}</pre>
|
||||
</details>
|
||||
</div>
|
||||
))}
|
||||
{!routeStatuses.length && <p className="py-8 text-center text-sm text-muted-foreground">{t(routeStatusLoading ? 'common.loading' : 'monitoring.unified.noRecords')}</p>}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
{/* disabled section */}
|
||||
@@ -1838,9 +1822,6 @@ export default function EventBindingsEditor({
|
||||
key={b.id ?? i}
|
||||
binding={b}
|
||||
globalIndex={i}
|
||||
routeStatus={
|
||||
b.id ? routeStatusByBinding.get(String(b.id)) : undefined
|
||||
}
|
||||
eventOptions={eventOptions}
|
||||
agentOptions={agentOptions}
|
||||
expandedIds={expandedIds}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
.routing-help-dot {
|
||||
animation: route-travel 4.8s ease-in-out infinite;
|
||||
animation-delay: var(--route-delay);
|
||||
opacity: 0;
|
||||
}
|
||||
.routing-help-target {
|
||||
animation: route-receive 4.8s ease-in-out infinite;
|
||||
animation-delay: var(--route-delay);
|
||||
}
|
||||
.routing-help-paused * { animation-play-state: paused; }
|
||||
@keyframes route-travel {
|
||||
0%, 4% { opacity: 0; transform: translateX(0); }
|
||||
8% { opacity: 1; }
|
||||
25% { opacity: 1; transform: translateX(40px); }
|
||||
30%, 100% { opacity: 0; transform: translateX(40px); }
|
||||
}
|
||||
@keyframes route-receive {
|
||||
0%, 24%, 45%, 100% { transform: translateY(0); }
|
||||
30%, 36% { transform: translateY(-2px); }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.routing-help-dot, .routing-help-target { animation: none; }
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
'use client';
|
||||
|
||||
import { useState, type CSSProperties } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import './EventRoutingHelp.css';
|
||||
|
||||
const routeColors = [
|
||||
'text-sky-700 dark:text-sky-300 [&_.route-node]:border-sky-200 [&_.route-node]:bg-sky-50 dark:[&_.route-node]:border-sky-800 dark:[&_.route-node]:bg-sky-950/40',
|
||||
'text-amber-700 dark:text-amber-300 [&_.route-node]:border-amber-200 [&_.route-node]:bg-amber-50 dark:[&_.route-node]:border-amber-800 dark:[&_.route-node]:bg-amber-950/40',
|
||||
'text-emerald-700 dark:text-emerald-300 [&_.route-node]:border-emerald-200 [&_.route-node]:bg-emerald-50 dark:[&_.route-node]:border-emerald-800 dark:[&_.route-node]:bg-emerald-950/40',
|
||||
];
|
||||
|
||||
export default function EventRoutingHelp() {
|
||||
const { t } = useTranslation();
|
||||
const [paused, setPaused] = useState(false);
|
||||
const text = (key: string) => t(`bots.routingHelp.${key}`);
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<button type="button" aria-label={text('title')} className="inline-flex size-6 items-center justify-center rounded-full text-muted-foreground hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="9" /><path d="M12 11v6" /><circle cx="12" cy="7.5" r=".7" fill="currentColor" />
|
||||
</svg>
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="start" className="w-[min(32rem,calc(100vw-2rem))] max-h-[80vh] overflow-y-auto p-5">
|
||||
<h3 className="text-sm font-semibold">{text('title')}</h3>
|
||||
<p className="mt-2 text-sm leading-6 text-muted-foreground">{text('intro')}</p>
|
||||
<div className="my-4 overflow-hidden rounded-xl border bg-muted/20 p-3">
|
||||
<div className="mb-3 grid grid-cols-2 text-xs text-muted-foreground">
|
||||
<span>{text('events')}</span><span className="text-right">{text('agents')}</span>
|
||||
</div>
|
||||
<div className={`routing-help space-y-3 ${paused ? 'routing-help-paused' : ''}`}>
|
||||
{['message', 'friend', 'group'].map((kind, index) => (
|
||||
<div key={kind} className={`grid grid-cols-[minmax(0,1fr)_48px_minmax(0,1fr)] items-center ${routeColors[index]}`} style={{ '--route-delay': `${index * 1.6}s` } as CSSProperties}>
|
||||
<div className="route-node rounded-lg border px-2 py-3 text-center text-xs leading-5">{text(kind)}</div>
|
||||
<div className="relative h-8" aria-hidden="true">
|
||||
<svg viewBox="0 0 48 32" className="h-full w-full opacity-30"><path d="M0 16H45m-5-4 5 4-5 4" fill="none" stroke="currentColor" strokeWidth="1.5" /></svg>
|
||||
<span className="routing-help-dot absolute left-0 top-[13px] size-1.5 rounded-full bg-current" />
|
||||
</div>
|
||||
<div className="route-node routing-help-target rounded-lg border px-2 py-3 text-center text-xs leading-5">{text(`${kind}Action`)}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-3 flex items-center justify-between gap-3 text-xs text-muted-foreground">
|
||||
<span>{text('rules')}</span>
|
||||
<button type="button" className="shrink-0 rounded px-1 underline underline-offset-4 focus-visible:ring-2 focus-visible:ring-ring motion-reduce:hidden" onClick={() => setPaused(!paused)}>{text(paused ? 'play' : 'pause')}</button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm leading-6">{text('instructions')}</p>
|
||||
<div className="mt-4 border-t pt-3 text-xs leading-6 text-muted-foreground">
|
||||
<p>{text('plugin')}</p>
|
||||
<a href="https://langbot.app/docs/zh/plugin/dev/components/runner" target="_blank" rel="noopener noreferrer" className="font-medium text-foreground underline underline-offset-4">{text('docs')} ↗</a>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
|
||||
export default function LegacyAdapterHelp() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<button type="button" aria-label={t('bots.legacyAdapters')} className="inline-flex size-6 shrink-0 items-center justify-center rounded-full text-muted-foreground hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" aria-hidden="true"><circle cx="12" cy="12" r="9" /><path d="M12 11v6" /><circle cx="12" cy="7.5" r=".7" fill="currentColor" /></svg>
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="start" className="w-80 max-w-[calc(100vw-2rem)]">
|
||||
<h4 className="mb-2 text-sm font-medium">{t('bots.legacyAdapters')}</h4>
|
||||
<p className="whitespace-pre-line text-sm leading-6 text-muted-foreground">{t('bots.legacyAdaptersHint')}</p>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -1,24 +1,12 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
AlertCircle,
|
||||
Bot,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Clock,
|
||||
Cpu,
|
||||
Hash,
|
||||
User,
|
||||
Wrench,
|
||||
} from 'lucide-react';
|
||||
import { AlertCircle, Cpu, MessageCircle, Send, Wrench } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { ProcessorPayload } from '@/app/home/agents/components/PluginProcessorTrace';
|
||||
import { toolSummary } from '@/app/home/agents/components/tool-summary';
|
||||
import { MessageContentRenderer } from './MessageContentRenderer';
|
||||
import {
|
||||
ConversationTurn,
|
||||
hasRenderableMessageContent,
|
||||
} from '../utils/conversationTurns';
|
||||
import { MonitoringMessage } from '../types/monitoring';
|
||||
import { ConversationTurn } from '../utils/conversationTurns';
|
||||
|
||||
interface ConversationTurnListProps {
|
||||
turns: ConversationTurn[];
|
||||
@@ -26,626 +14,180 @@ interface ConversationTurnListProps {
|
||||
onToggleTurn: (turnId: string) => void;
|
||||
}
|
||||
|
||||
function shortId(id?: string) {
|
||||
if (!id) return '-';
|
||||
if (id.length <= 12) return id;
|
||||
return `${id.slice(0, 8)}...${id.slice(-4)}`;
|
||||
}
|
||||
|
||||
function formatDuration(ms: number) {
|
||||
if (!ms) return '0ms';
|
||||
if (ms < 1000) return `${ms}ms`;
|
||||
return `${(ms / 1000).toFixed(2)}s`;
|
||||
return ms < 1000 ? `${ms} ms` : `${(ms / 1000).toFixed(2)} s`;
|
||||
}
|
||||
|
||||
function truncateDetail(value?: string) {
|
||||
if (!value) return '';
|
||||
return value.length > 1200 ? `${value.slice(0, 1200)}...` : value;
|
||||
function parsePayload(value?: string): unknown {
|
||||
if (!value) return null;
|
||||
try { return JSON.parse(value); } catch { return value; }
|
||||
}
|
||||
|
||||
function roleLabel(message: MonitoringMessage | undefined) {
|
||||
const role = message?.role?.toLowerCase();
|
||||
if (role === 'assistant') return 'assistant';
|
||||
if (role === 'user') return 'user';
|
||||
return 'message';
|
||||
}
|
||||
|
||||
function statusClass(level: ConversationTurn['level']) {
|
||||
if (level === 'error') {
|
||||
return 'border-red-200 bg-red-50 text-red-700 dark:border-red-900 dark:bg-red-950/40 dark:text-red-300';
|
||||
}
|
||||
if (level === 'warning') {
|
||||
return 'border-yellow-200 bg-yellow-50 text-yellow-700 dark:border-yellow-900 dark:bg-yellow-950/40 dark:text-yellow-300';
|
||||
}
|
||||
return 'border-green-200 bg-green-50 text-green-700 dark:border-green-900 dark:bg-green-950/40 dark:text-green-300';
|
||||
}
|
||||
|
||||
function Metric({
|
||||
icon,
|
||||
label,
|
||||
tone = 'default',
|
||||
}: {
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
tone?: 'default' | 'error';
|
||||
}) {
|
||||
function MessageBody({ content }: { content: string }) {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
'inline-flex h-7 items-center gap-1.5 rounded-md border px-2 text-xs font-medium',
|
||||
tone === 'error'
|
||||
? 'border-red-200 bg-red-50 text-red-700 dark:border-red-900 dark:bg-red-950/40 dark:text-red-300'
|
||||
: 'border-border bg-background text-muted-foreground',
|
||||
)}
|
||||
>
|
||||
{icon}
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function MetaItem({ label, value }: { label: string; value?: string }) {
|
||||
return (
|
||||
<div className="min-w-0 rounded-md bg-background px-3 py-2">
|
||||
<div className="text-xs text-muted-foreground">{label}</div>
|
||||
<div className="truncate text-sm font-medium text-foreground">
|
||||
{value || '-'}
|
||||
</div>
|
||||
<div className="min-w-0 whitespace-pre-wrap break-words text-sm leading-7 [overflow-wrap:anywhere]">
|
||||
<MessageContentRenderer content={content} maxLines={0} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MessageLane({
|
||||
label,
|
||||
icon,
|
||||
content,
|
||||
empty,
|
||||
maxLines,
|
||||
}: {
|
||||
label: string;
|
||||
icon: React.ReactNode;
|
||||
content?: string;
|
||||
empty: string;
|
||||
maxLines: number;
|
||||
}) {
|
||||
return (
|
||||
<div className="grid grid-cols-[5.25rem_minmax(0,1fr)] items-start gap-3 text-sm sm:grid-cols-[6rem_minmax(0,1fr)]">
|
||||
<div className="flex h-7 items-center gap-1.5 text-xs font-medium text-muted-foreground">
|
||||
{icon}
|
||||
<span>{label}</span>
|
||||
</div>
|
||||
<div className="min-w-0 rounded-md bg-muted/45 px-3 py-2 text-foreground">
|
||||
{content && hasRenderableMessageContent(content) ? (
|
||||
<MessageContentRenderer content={content} maxLines={maxLines} />
|
||||
) : (
|
||||
<span className="italic text-muted-foreground">{empty}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ExpandedMessage({
|
||||
message,
|
||||
label,
|
||||
}: {
|
||||
message: MonitoringMessage;
|
||||
label: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="border-t border-border/70 py-3 first:border-t-0 first:pt-0 last:pb-0">
|
||||
<div className="mb-2 flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
||||
<span className="rounded-md bg-muted px-2 py-1 font-medium text-foreground">
|
||||
{label}
|
||||
</span>
|
||||
<span>{message.timestamp.toLocaleString()}</span>
|
||||
<span className="font-mono">ID: {shortId(message.id)}</span>
|
||||
</div>
|
||||
<div className="text-sm leading-6 text-foreground">
|
||||
<MessageContentRenderer content={message.messageContent} maxLines={4} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ConversationTurnList({
|
||||
turns,
|
||||
expandedTurnId,
|
||||
onToggleTurn,
|
||||
}: ConversationTurnListProps) {
|
||||
export function ConversationTurnList({ turns, expandedTurnId, onToggleTurn }: ConversationTurnListProps) {
|
||||
const { t } = useTranslation();
|
||||
const [expandedToolCallIds, setExpandedToolCallIds] = React.useState<
|
||||
Record<string, boolean>
|
||||
>({});
|
||||
const turn = turns.find((item) => item.id === expandedTurnId) ?? turns[0];
|
||||
if (!turn) return null;
|
||||
|
||||
const toggleToolCallDetails = (toolCallKey: string) => {
|
||||
setExpandedToolCallIds((previous) => ({
|
||||
...previous,
|
||||
[toolCallKey]: !previous[toolCallKey],
|
||||
}));
|
||||
};
|
||||
const statusBadge = (status: ConversationTurn['status']) => (
|
||||
<Badge status={status} className="shrink-0">
|
||||
{t(`monitoring.pipelineTrace.${status}`)}
|
||||
</Badge>
|
||||
);
|
||||
const steps = [
|
||||
...turn.llmCalls.map((call) => ({ kind: 'model' as const, call })),
|
||||
...turn.toolCalls.map((call) => ({ kind: 'tool' as const, call })),
|
||||
...turn.errors.map((call) => ({ kind: 'error' as const, call })),
|
||||
].sort((a, b) => a.call.timestamp.getTime() - b.call.timestamp.getTime());
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between text-sm text-muted-foreground">
|
||||
<span className="font-medium text-foreground">
|
||||
{t('monitoring.messageList.turns', {
|
||||
defaultValue: '{{count}} 轮对话',
|
||||
count: turns.length,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{turns.map((turn) => {
|
||||
const expanded = expandedTurnId === turn.id;
|
||||
const firstAssistant = turn.assistantMessages[0];
|
||||
const assistantOverflow = Math.max(
|
||||
turn.assistantMessages.length - 1,
|
||||
0,
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={turn.id}
|
||||
className={cn(
|
||||
'overflow-hidden rounded-xl border bg-card transition-colors',
|
||||
turn.level === 'error' && 'border-red-200 dark:border-red-900',
|
||||
)}
|
||||
>
|
||||
<div className="grid min-h-0 gap-5 lg:h-full lg:grid-cols-[18rem_minmax(0,1fr)]">
|
||||
<aside className="min-h-0 lg:overflow-y-auto lg:pr-1">
|
||||
<div className="mb-3 text-sm font-medium">
|
||||
{t('monitoring.messageList.turns', { count: turns.length })}
|
||||
</div>
|
||||
<div className="max-h-72 overflow-y-auto rounded-xl border lg:max-h-none">
|
||||
{turns.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="cursor-pointer p-3 outline-none transition-colors hover:bg-accent/60 focus-visible:ring-2 focus-visible:ring-ring sm:p-5"
|
||||
onClick={() => onToggleTurn(turn.id)}
|
||||
aria-pressed={turn.id === item.id}
|
||||
onClick={() => onToggleTurn(item.id)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
if (event.target === event.currentTarget && (event.key === 'Enter' || event.key === ' ')) {
|
||||
event.preventDefault();
|
||||
onToggleTurn(turn.id);
|
||||
onToggleTurn(item.id);
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
'cursor-pointer border-b border-l-2 border-l-transparent p-3 text-left last:border-b-0 hover:bg-muted/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring',
|
||||
turn.id === item.id && 'border-l-primary bg-primary/5',
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="mb-2 flex min-w-0 items-center gap-2">
|
||||
{expanded ? (
|
||||
<ChevronDown className="h-5 w-5 shrink-0 text-muted-foreground" />
|
||||
) : (
|
||||
<ChevronRight className="h-5 w-5 shrink-0 text-muted-foreground" />
|
||||
)}
|
||||
<span className="truncate font-mono text-xs text-muted-foreground">
|
||||
Turn: {shortId(turn.id)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mb-3 flex min-w-0 flex-wrap items-center gap-2">
|
||||
<span className="truncate text-sm font-medium text-foreground">
|
||||
{turn.botName}
|
||||
</span>
|
||||
<span className="text-muted-foreground">→</span>
|
||||
<span className="truncate text-sm text-muted-foreground">
|
||||
{turn.pipelineName}
|
||||
</span>
|
||||
{turn.runnerName && (
|
||||
<>
|
||||
<span className="text-muted-foreground">→</span>
|
||||
<span className="truncate text-sm text-muted-foreground">
|
||||
{turn.runnerName}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<MessageLane
|
||||
label={t('monitoring.messageList.userMessage', {
|
||||
defaultValue: '用户',
|
||||
})}
|
||||
icon={<User className="h-3.5 w-3.5" />}
|
||||
content={turn.userMessage?.messageContent}
|
||||
empty={t('monitoring.messageList.noUserMessage', {
|
||||
defaultValue: '未记录用户输入',
|
||||
})}
|
||||
maxLines={2}
|
||||
/>
|
||||
<MessageLane
|
||||
label={
|
||||
assistantOverflow > 0
|
||||
? t('monitoring.messageList.assistantMessageCount', {
|
||||
defaultValue: '助手 +{{count}}',
|
||||
count: assistantOverflow,
|
||||
})
|
||||
: t('monitoring.messageList.assistantMessage', {
|
||||
defaultValue: '助手',
|
||||
})
|
||||
}
|
||||
icon={<Bot className="h-3.5 w-3.5" />}
|
||||
content={firstAssistant?.messageContent}
|
||||
empty={t('monitoring.messageList.noAssistantMessage', {
|
||||
defaultValue: '未记录助手回复',
|
||||
})}
|
||||
maxLines={2}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex shrink-0 flex-col gap-2 lg:items-end">
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{turn.lastActivityAt.toLocaleString()}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
'inline-flex h-7 items-center rounded-md border px-2 text-xs font-medium',
|
||||
statusClass(turn.level),
|
||||
)}
|
||||
>
|
||||
{turn.level}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2 lg:justify-end">
|
||||
<Metric
|
||||
icon={<Cpu className="h-3.5 w-3.5" />}
|
||||
label={`${turn.llmCalls.length} LLM`}
|
||||
/>
|
||||
{turn.toolCalls.length > 0 && (
|
||||
<Metric
|
||||
icon={<Wrench className="h-3.5 w-3.5" />}
|
||||
label={`${turn.toolCalls.length} tools`}
|
||||
/>
|
||||
)}
|
||||
<Metric
|
||||
icon={<Hash className="h-3.5 w-3.5" />}
|
||||
label={`${turn.totalTokens.toLocaleString()} tokens`}
|
||||
/>
|
||||
<Metric
|
||||
icon={<Clock className="h-3.5 w-3.5" />}
|
||||
label={formatDuration(turn.totalDuration)}
|
||||
/>
|
||||
{turn.errors.length > 0 && (
|
||||
<Metric
|
||||
icon={<AlertCircle className="h-3.5 w-3.5" />}
|
||||
label={`${turn.errors.length} errors`}
|
||||
tone="error"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-2 flex items-center justify-between gap-2 text-xs">
|
||||
<span className="truncate font-medium">{item.botName}</span>
|
||||
{statusBadge(item.status)}
|
||||
</div>
|
||||
<div className="pointer-events-none max-h-12 overflow-hidden break-words text-sm">
|
||||
{item.userMessage ? (
|
||||
<MessageContentRenderer content={item.userMessage.messageContent} maxLines={2} />
|
||||
) : t('monitoring.messageList.noUserMessage')}
|
||||
</div>
|
||||
<div className="mt-2 text-xs tabular-nums text-muted-foreground">
|
||||
{item.startedAt.toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{expanded && (
|
||||
<div className="border-t bg-muted/40 p-3 sm:p-5">
|
||||
<div className="space-y-5 border-l-2 border-border pl-4 sm:pl-6">
|
||||
<div className="grid grid-cols-2 gap-2 lg:grid-cols-5">
|
||||
<MetaItem
|
||||
label={t('monitoring.messageList.platform', {
|
||||
defaultValue: '平台',
|
||||
})}
|
||||
value={turn.platform}
|
||||
/>
|
||||
<MetaItem
|
||||
label={t('monitoring.messageList.user', {
|
||||
defaultValue: '用户',
|
||||
})}
|
||||
value={turn.userName || turn.userId}
|
||||
/>
|
||||
<MetaItem
|
||||
label={t('monitoring.messageList.runner', {
|
||||
defaultValue: '执行器',
|
||||
})}
|
||||
value={turn.runnerName}
|
||||
/>
|
||||
<MetaItem
|
||||
label={t('monitoring.sessions.sessionId', {
|
||||
defaultValue: '会话 ID',
|
||||
})}
|
||||
value={turn.sessionId}
|
||||
/>
|
||||
<MetaItem
|
||||
label={t('monitoring.messageList.messageCount', {
|
||||
defaultValue: '消息数',
|
||||
})}
|
||||
value={String(turn.messages.length)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<h4 className="mb-3 flex items-center gap-2 text-sm font-semibold text-foreground">
|
||||
<Bot className="h-4 w-4" />
|
||||
{t('monitoring.messageList.conversationTrace', {
|
||||
defaultValue: '消息链路',
|
||||
})}
|
||||
</h4>
|
||||
<div className="rounded-lg bg-background px-3 py-3">
|
||||
{turn.messages.map((message) => (
|
||||
<ExpandedMessage
|
||||
key={message.id}
|
||||
message={message}
|
||||
label={t(
|
||||
`monitoring.messageList.roles.${roleLabel(message)}`,
|
||||
{
|
||||
defaultValue:
|
||||
roleLabel(message) === 'assistant'
|
||||
? '助手'
|
||||
: roleLabel(message) === 'user'
|
||||
? '用户'
|
||||
: '消息',
|
||||
},
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h4 className="mb-3 flex items-center gap-2 text-sm font-semibold text-foreground">
|
||||
<Cpu className="h-4 w-4" />
|
||||
{t('monitoring.llmCalls.title', {
|
||||
defaultValue: 'LLM 调用',
|
||||
})}{' '}
|
||||
({turn.llmCalls.length})
|
||||
</h4>
|
||||
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<MetaItem
|
||||
label={t('monitoring.llmCalls.totalTokens', {
|
||||
defaultValue: '总 Token',
|
||||
})}
|
||||
value={turn.totalTokens.toLocaleString()}
|
||||
/>
|
||||
<MetaItem
|
||||
label={t('monitoring.llmCalls.inputTokens', {
|
||||
defaultValue: '输入 Token',
|
||||
})}
|
||||
value={turn.inputTokens.toLocaleString()}
|
||||
/>
|
||||
<MetaItem
|
||||
label={t('monitoring.llmCalls.duration', {
|
||||
defaultValue: '耗时',
|
||||
})}
|
||||
value={formatDuration(turn.totalDuration)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 rounded-lg bg-background px-3 py-3">
|
||||
{turn.llmCalls.length > 0 ? (
|
||||
turn.llmCalls.map((call, index) => (
|
||||
<div
|
||||
key={call.id}
|
||||
className="border-t border-border/70 py-3 first:border-t-0 first:pt-0 last:pb-0"
|
||||
>
|
||||
<div className="mb-2 flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
||||
<span className="text-sm font-medium text-foreground">
|
||||
#{index + 1} {call.modelName}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
'rounded-md px-2 py-1 text-xs font-medium',
|
||||
call.status === 'success'
|
||||
? 'bg-green-100 text-green-700 dark:bg-green-950 dark:text-green-300'
|
||||
: 'bg-red-100 text-red-700 dark:bg-red-950 dark:text-red-300',
|
||||
)}
|
||||
>
|
||||
{call.status}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{formatDuration(call.duration)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-x-8 gap-y-1 text-xs text-muted-foreground">
|
||||
<span>In: {call.tokens.input}</span>
|
||||
<span>Out: {call.tokens.output}</span>
|
||||
<span>Total: {call.tokens.total}</span>
|
||||
<span className="font-mono">
|
||||
ID: {shortId(call.id)}
|
||||
</span>
|
||||
</div>
|
||||
{call.errorMessage && (
|
||||
<div className="mt-2 whitespace-pre-wrap break-words text-xs text-red-600 dark:text-red-400">
|
||||
{call.errorMessage}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="py-4 text-center text-sm text-muted-foreground">
|
||||
{t('monitoring.messageList.noLlmCalls', {
|
||||
defaultValue: '未记录模型调用',
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h4 className="mb-3 flex items-center gap-2 text-sm font-semibold text-foreground">
|
||||
<Wrench className="h-4 w-4" />
|
||||
{t('monitoring.toolCalls.title', {
|
||||
defaultValue: '工具调用',
|
||||
})}{' '}
|
||||
({turn.toolCalls.length})
|
||||
</h4>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2 lg:grid-cols-3">
|
||||
<MetaItem
|
||||
label={t('monitoring.toolCalls.totalCalls', {
|
||||
defaultValue: '调用次数',
|
||||
})}
|
||||
value={String(turn.toolCalls.length)}
|
||||
/>
|
||||
<MetaItem
|
||||
label={t('monitoring.toolCalls.duration', {
|
||||
defaultValue: '工具耗时',
|
||||
})}
|
||||
value={formatDuration(turn.totalToolDuration)}
|
||||
/>
|
||||
<MetaItem
|
||||
label={t('monitoring.toolCalls.errorCalls', {
|
||||
defaultValue: '失败次数',
|
||||
})}
|
||||
value={String(
|
||||
turn.toolCalls.filter(
|
||||
(call) => call.status === 'error',
|
||||
).length,
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 rounded-lg bg-background px-3 py-3">
|
||||
{turn.toolCalls.length > 0 ? (
|
||||
turn.toolCalls.map((call, index) => {
|
||||
const toolCallKey = `${turn.id}:${call.id}`;
|
||||
const hasToolDetails = Boolean(
|
||||
call.arguments || call.result || call.errorMessage,
|
||||
);
|
||||
const expandedToolCall = Boolean(
|
||||
expandedToolCallIds[toolCallKey],
|
||||
);
|
||||
const detailsId = `monitoring-tool-call-details-${call.id}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={call.id}
|
||||
className="border-t border-border/70 py-2 first:border-t-0 first:pt-0 last:pb-0"
|
||||
>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
'flex h-auto w-full items-start justify-between gap-3 rounded-md px-2 py-2 text-left',
|
||||
hasToolDetails &&
|
||||
'cursor-pointer hover:bg-muted/60 focus-visible:ring-2 focus-visible:ring-ring',
|
||||
)}
|
||||
aria-expanded={
|
||||
hasToolDetails ? expandedToolCall : undefined
|
||||
}
|
||||
aria-controls={
|
||||
hasToolDetails ? detailsId : undefined
|
||||
}
|
||||
aria-disabled={!hasToolDetails}
|
||||
onClick={() =>
|
||||
hasToolDetails &&
|
||||
toggleToolCallDetails(toolCallKey)
|
||||
}
|
||||
>
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
||||
{hasToolDetails &&
|
||||
(expandedToolCall ? (
|
||||
<ChevronDown className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
) : (
|
||||
<ChevronRight className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
))}
|
||||
<span className="min-w-0 truncate text-sm font-medium text-foreground">
|
||||
#{index + 1} {call.toolName}
|
||||
</span>
|
||||
<span className="rounded-md bg-muted px-2 py-1 text-xs font-medium text-muted-foreground">
|
||||
{call.toolSource}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
'rounded-md px-2 py-1 text-xs font-medium',
|
||||
call.status === 'success'
|
||||
? 'bg-green-100 text-green-700 dark:bg-green-950 dark:text-green-300'
|
||||
: 'bg-red-100 text-red-700 dark:bg-red-950 dark:text-red-300',
|
||||
)}
|
||||
>
|
||||
{call.status}
|
||||
</span>
|
||||
<span className="font-mono text-xs text-muted-foreground">
|
||||
ID: {shortId(call.id)}
|
||||
</span>
|
||||
</div>
|
||||
<span className="shrink-0 text-xs text-muted-foreground">
|
||||
{formatDuration(call.duration)}
|
||||
</span>
|
||||
</Button>
|
||||
|
||||
{hasToolDetails && expandedToolCall && (
|
||||
<div
|
||||
id={detailsId}
|
||||
className="mt-1 grid gap-2 px-2 pb-2 text-xs lg:grid-cols-2"
|
||||
>
|
||||
{call.arguments && (
|
||||
<div className="min-w-0 rounded-md bg-muted/50 p-2">
|
||||
<div className="mb-1 font-medium text-foreground">
|
||||
{t('monitoring.toolCalls.arguments', {
|
||||
defaultValue: '参数',
|
||||
})}
|
||||
</div>
|
||||
<pre className="whitespace-pre-wrap break-words font-mono text-muted-foreground">
|
||||
{truncateDetail(call.arguments)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
{call.result && (
|
||||
<div className="min-w-0 rounded-md bg-muted/50 p-2">
|
||||
<div className="mb-1 font-medium text-foreground">
|
||||
{t('monitoring.toolCalls.result', {
|
||||
defaultValue: '结果',
|
||||
})}
|
||||
</div>
|
||||
<pre className="whitespace-pre-wrap break-words font-mono text-muted-foreground">
|
||||
{truncateDetail(call.result)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
{call.errorMessage && (
|
||||
<div className="min-w-0 whitespace-pre-wrap break-words rounded-md bg-red-50 p-2 text-red-600 dark:bg-red-950/40 dark:text-red-400 lg:col-span-2">
|
||||
{call.errorMessage}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className="py-4 text-center text-sm text-muted-foreground">
|
||||
{t('monitoring.toolCalls.noToolCalls', {
|
||||
defaultValue: '未记录工具调用',
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{turn.errors.length > 0 && (
|
||||
<section>
|
||||
<h4 className="mb-3 flex items-center gap-2 text-sm font-semibold text-red-700 dark:text-red-300">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
{t('monitoring.errors.title', {
|
||||
defaultValue: '错误日志',
|
||||
})}{' '}
|
||||
({turn.errors.length})
|
||||
</h4>
|
||||
<div className="rounded-lg bg-background px-3 py-3">
|
||||
{turn.errors.map((error) => (
|
||||
<div
|
||||
key={error.id}
|
||||
className="border-t border-red-200/80 py-3 first:border-t-0 first:pt-0 last:pb-0 dark:border-red-900"
|
||||
>
|
||||
<div className="mb-2 flex flex-wrap items-center justify-between gap-2">
|
||||
<span className="text-sm font-medium text-red-700 dark:text-red-300">
|
||||
{error.errorType}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{error.timestamp.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="whitespace-pre-wrap break-words text-sm text-red-600 dark:text-red-400">
|
||||
{error.errorMessage}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<article key={turn.id} className="min-w-0 space-y-5 lg:overflow-y-auto lg:pr-2">
|
||||
<header className="space-y-3 border-b pb-4">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<h3 className="text-sm font-semibold">{turn.botName}</h3>
|
||||
{statusBadge(turn.status)}
|
||||
<span className="text-sm text-muted-foreground">{turn.userName || turn.userId}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="flex flex-wrap gap-x-5 gap-y-2 text-xs tabular-nums text-muted-foreground">
|
||||
<span>{turn.startedAt.toLocaleString()}</span>
|
||||
<span>{t('monitoring.llmCalls.title')} {turn.llmCalls.length}</span>
|
||||
<span>{t('monitoring.toolCalls.title')} {turn.toolCalls.length}</span>
|
||||
<span>{t('monitoring.llmCalls.totalTokens')} {turn.totalTokens.toLocaleString()}</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section className="rounded-xl border p-4">
|
||||
<h4 className="mb-3 flex items-center gap-2 text-sm font-medium">
|
||||
<MessageCircle className="size-4 text-muted-foreground" />
|
||||
{t('monitoring.pipelineTrace.received')}
|
||||
</h4>
|
||||
{turn.userMessage ? <MessageBody content={turn.userMessage.messageContent} /> : (
|
||||
<p className="text-sm text-muted-foreground">{t('monitoring.messageList.noUserMessage')}</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h4 className="mb-3 text-sm font-medium">
|
||||
{t('monitoring.pipelineTrace.processing')}
|
||||
<span className="ml-2 font-normal text-muted-foreground">{steps.length}</span>
|
||||
</h4>
|
||||
{steps.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">{t('monitoring.pipelineTrace.noSteps')}</p>
|
||||
) : (
|
||||
<ol className="space-y-3">
|
||||
{steps.map((step, index) => {
|
||||
const { call } = step;
|
||||
const parameters = step.kind === 'tool' ? parsePayload(step.call.arguments) : null;
|
||||
const summary = step.kind === 'tool' && parameters && typeof parameters === 'object' && !Array.isArray(parameters)
|
||||
? toolSummary(step.call.toolName, parameters as Record<string, unknown>).text : '';
|
||||
return (
|
||||
<li key={`${step.kind}:${call.id}`} className="relative flex gap-3">
|
||||
{index < steps.length - 1 && <div className="absolute bottom-[-12px] left-4 top-8 w-px bg-border" />}
|
||||
<span className="relative flex size-8 shrink-0 items-center justify-center rounded-full border bg-background text-xs tabular-nums text-muted-foreground">{index + 1}</span>
|
||||
<div className="min-w-0 flex-1 overflow-hidden rounded-xl border">
|
||||
<div className="flex flex-wrap items-center gap-2 border-b bg-muted/20 px-4 py-3 text-sm">
|
||||
{step.kind === 'model' ? <Cpu className="size-4" /> : step.kind === 'tool' ? <Wrench className="size-4" /> : <AlertCircle className="size-4 text-destructive" />}
|
||||
<span className="min-w-0 flex-1 break-words font-medium">
|
||||
{step.kind === 'model' ? step.call.modelName : step.kind === 'tool' ? step.call.toolName : step.call.errorType}
|
||||
</span>
|
||||
{statusBadge(step.kind === 'error' ? 'error' : step.call.status)}
|
||||
</div>
|
||||
<div className="space-y-3 p-4">
|
||||
<div className="flex flex-wrap gap-x-4 gap-y-1 text-xs tabular-nums text-muted-foreground">
|
||||
<span>{call.timestamp.toLocaleTimeString()}</span>
|
||||
{step.kind !== 'error' && <span>{formatDuration(step.call.duration)}</span>}
|
||||
{step.kind === 'tool' && <span>{step.call.toolSource}</span>}
|
||||
{step.kind === 'model' && <>
|
||||
<span>{t('monitoring.llmCalls.inputTokens')} {step.call.tokens.input.toLocaleString()}</span>
|
||||
<span>{t('monitoring.llmCalls.outputTokens')} {step.call.tokens.output.toLocaleString()}</span>
|
||||
</>}
|
||||
</div>
|
||||
{summary && <p className="whitespace-pre-wrap break-words text-sm [overflow-wrap:anywhere]">{summary}</p>}
|
||||
{call.errorMessage && <p className="whitespace-pre-wrap break-words text-sm text-destructive [overflow-wrap:anywhere]">{call.errorMessage}</p>}
|
||||
{step.kind === 'tool' && <div>
|
||||
{step.call.arguments && <ProcessorPayload title={t('monitoring.toolCalls.arguments')} value={parameters} />}
|
||||
{step.call.result && step.call.result !== 'null' && <ProcessorPayload title={t('monitoring.toolCalls.result')} value={parsePayload(step.call.result)} />}
|
||||
</div>}
|
||||
{step.kind === 'error' && step.call.stackTrace && <ProcessorPayload title={t('monitoring.unified.metadata')} value={step.call.stackTrace} />}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="rounded-xl border p-4">
|
||||
<h4 className="mb-3 flex items-center gap-2 text-sm font-medium">
|
||||
<Send className="size-4 text-muted-foreground" />
|
||||
{t('monitoring.pipelineTrace.replies')}
|
||||
<span className="font-normal text-muted-foreground">{turn.assistantMessages.length}</span>
|
||||
</h4>
|
||||
{turn.assistantMessages.length ? turn.assistantMessages.map((message) => (
|
||||
<div key={message.id} className="space-y-2 border-t py-3 first:border-t-0 first:pt-0 last:pb-0">
|
||||
<div className="flex items-center justify-between gap-2 text-xs text-muted-foreground">
|
||||
<span>{message.timestamp.toLocaleTimeString()}</span>
|
||||
{statusBadge(message.status)}
|
||||
</div>
|
||||
<MessageBody content={message.messageContent} />
|
||||
</div>
|
||||
)) : <p className="text-sm text-muted-foreground">{t('monitoring.messageList.noAssistantMessage')}</p>}
|
||||
</section>
|
||||
<ProcessorPayload title={t('monitoring.unified.metadata')} value={{
|
||||
turn_id: turn.id, session_id: turn.sessionId, bot: turn.botName,
|
||||
pipeline: turn.pipelineName, runner: turn.runnerName, platform: turn.platform,
|
||||
user_id: turn.userId, messages: turn.messages,
|
||||
}} />
|
||||
</article>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { backendClient } from '@/app/infra/http';
|
||||
import { resolveMonitoringWindow } from '../utils/dateUtils';
|
||||
import { FilterState } from '../types/monitoring';
|
||||
|
||||
export type ExportType =
|
||||
@@ -38,44 +39,7 @@ export function ExportDropdown({ filterState }: ExportDropdownProps) {
|
||||
const { t } = useTranslation();
|
||||
const [exporting, setExporting] = useState<ExportType | null>(null);
|
||||
|
||||
const getDateRangeParams = (): { startTime: string; endTime: string } => {
|
||||
const now = new Date();
|
||||
let startTime: Date;
|
||||
let endTime: Date = now;
|
||||
|
||||
switch (filterState.timeRange) {
|
||||
case 'lastHour':
|
||||
startTime = new Date(now.getTime() - 60 * 60 * 1000);
|
||||
break;
|
||||
case 'last6Hours':
|
||||
startTime = new Date(now.getTime() - 6 * 60 * 60 * 1000);
|
||||
break;
|
||||
case 'last24Hours':
|
||||
startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||
break;
|
||||
case 'last7Days':
|
||||
startTime = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
||||
break;
|
||||
case 'last30Days':
|
||||
startTime = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
||||
break;
|
||||
case 'custom':
|
||||
if (filterState.customDateRange) {
|
||||
startTime = filterState.customDateRange.from;
|
||||
endTime = filterState.customDateRange.to;
|
||||
} else {
|
||||
startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||
}
|
||||
|
||||
return {
|
||||
startTime: startTime.toISOString(),
|
||||
endTime: endTime.toISOString(),
|
||||
};
|
||||
};
|
||||
const getDateRangeParams = () => resolveMonitoringWindow(filterState.timeRange, filterState.customDateRange);
|
||||
|
||||
const handleExport = async (type: ExportType) => {
|
||||
setExporting(type);
|
||||
|
||||
@@ -25,6 +25,7 @@ import { formatRunDuration } from '@/app/home/agents/components/processor-run-ti
|
||||
import { formatDateTime } from '../../utils/dateUtils';
|
||||
import {
|
||||
executionKindLabel,
|
||||
ExecutionKindBadge,
|
||||
executionProcessorLabel,
|
||||
executionStatusLabel,
|
||||
} from './ExecutionTable';
|
||||
@@ -384,9 +385,7 @@ export default function ExecutionDetailSheet({
|
||||
<Badge status={record.status_group}>
|
||||
{executionStatusLabel(record, t)}
|
||||
</Badge>
|
||||
<Badge variant="outline">
|
||||
{executionKindLabel(record, t)}
|
||||
</Badge>
|
||||
<ExecutionKindBadge row={record} />
|
||||
{record.debug && (
|
||||
<Badge status="debug">
|
||||
{t('monitoring.execution.debug')}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Bot, Workflow, Puzzle, CircleDashed } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { TFunction } from 'i18next';
|
||||
import {
|
||||
@@ -63,6 +64,27 @@ export function executionKindLabel(row: ExecutionRow, t: TFunction): string {
|
||||
return row.target_kind;
|
||||
}
|
||||
|
||||
export function ExecutionKindBadge({ row }: { row: ExecutionRow }) {
|
||||
const { t } = useTranslation();
|
||||
const Icon = row.target_kind === 'pipeline' ? Workflow
|
||||
: row.target_kind === 'agent' ? Bot
|
||||
: ['processor', 'event_processor'].includes(row.target_kind) ? Puzzle
|
||||
: CircleDashed;
|
||||
const color = row.target_kind === 'pipeline'
|
||||
? 'border-blue-200/70 bg-blue-50/70 text-blue-700 dark:border-blue-800/50 dark:bg-blue-950/30 dark:text-blue-300'
|
||||
: row.target_kind === 'agent'
|
||||
? 'border-emerald-200/70 bg-emerald-50/70 text-emerald-700 dark:border-emerald-800/50 dark:bg-emerald-950/30 dark:text-emerald-300'
|
||||
: ['processor', 'event_processor'].includes(row.target_kind)
|
||||
? 'border-amber-200/70 bg-amber-50/70 text-amber-700 dark:border-amber-800/50 dark:bg-amber-950/30 dark:text-amber-300'
|
||||
: 'border-border bg-muted/40 text-muted-foreground';
|
||||
return (
|
||||
<Badge variant="outline" className={`gap-1 whitespace-nowrap ${color}`}>
|
||||
<Icon className="size-3 shrink-0" aria-hidden="true" />
|
||||
{executionKindLabel(row, t)}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
export function executionTargetLabel(row: ExecutionRow, t: TFunction): string {
|
||||
if (row.source !== 'pipeline') {
|
||||
return eventPatternLabel(row.title, t);
|
||||
@@ -133,9 +155,7 @@ export default function ExecutionTable({
|
||||
<TableCell>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline">
|
||||
{executionKindLabel(row, t)}
|
||||
</Badge>
|
||||
<ExecutionKindBadge row={row} />
|
||||
<span className="truncate font-medium">
|
||||
{row.source === 'event'
|
||||
? t('monitoring.unified.unhandled')
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Select,
|
||||
@@ -7,6 +7,13 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
import { DateRange } from '../../types/monitoring';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { backendClient, useCurrentWorkspace } from '@/app/infra/http';
|
||||
import { TimeRangeOption } from '../../types/monitoring';
|
||||
@@ -16,6 +23,8 @@ interface MonitoringFiltersProps {
|
||||
selectedBots: string[];
|
||||
selectedPipelines: string[];
|
||||
timeRange: TimeRangeOption;
|
||||
customDateRange?: DateRange | null;
|
||||
onCustomDateRangeChange?: (range: DateRange) => void;
|
||||
onBotsChange: (bots: string[]) => void;
|
||||
onPipelinesChange: (pipelines: string[]) => void;
|
||||
onTimeRangeChange: (timeRange: TimeRangeOption) => void;
|
||||
@@ -53,12 +62,15 @@ interface Processor {
|
||||
uuid: string;
|
||||
name: string;
|
||||
emoji?: string;
|
||||
kind: string;
|
||||
}
|
||||
|
||||
export default function MonitoringFilters({
|
||||
selectedBots,
|
||||
selectedPipelines,
|
||||
timeRange,
|
||||
customDateRange,
|
||||
onCustomDateRangeChange,
|
||||
onBotsChange,
|
||||
onPipelinesChange,
|
||||
onTimeRangeChange,
|
||||
@@ -122,6 +134,7 @@ export default function MonitoringFilters({
|
||||
uuid: pipeline.uuid,
|
||||
name: pipeline.name,
|
||||
emoji: pipeline.emoji,
|
||||
kind: pipeline.kind,
|
||||
}));
|
||||
if (active) setProcessors(validProcessors);
|
||||
} catch (error) {
|
||||
@@ -145,18 +158,28 @@ export default function MonitoringFilters({
|
||||
}
|
||||
};
|
||||
|
||||
const handlePipelineChange = (value: string) => {
|
||||
if (value === 'all') {
|
||||
onPipelinesChange([]);
|
||||
} else {
|
||||
onPipelinesChange([value]);
|
||||
}
|
||||
const customPending = useRef(false);
|
||||
const [dateOpen, setDateOpen] = useState(false);
|
||||
const [fromDate, setFromDate] = useState('');
|
||||
const [toDate, setToDate] = useState('');
|
||||
const localDate = (date: Date) => new Date(date.getTime() - date.getTimezoneOffset() * 60000).toISOString().slice(0, 16);
|
||||
const openCustom = () => {
|
||||
setFromDate(localDate(customDateRange?.from ?? new Date(Date.now() - 86400000)));
|
||||
setToDate(localDate(customDateRange?.to ?? new Date()));
|
||||
setDateOpen(true);
|
||||
};
|
||||
|
||||
const from = new Date(fromDate);
|
||||
const to = new Date(toDate);
|
||||
const validDates = Number.isFinite(from.getTime()) && Number.isFinite(to.getTime()) && from < to && to.getTime() - from.getTime() <= 365 * 86400000 && to.getTime() <= Date.now();
|
||||
const handleTimeRangeChange = (value: string) => {
|
||||
onTimeRangeChange(value as TimeRangeOption);
|
||||
if (value === 'custom') customPending.current = true;
|
||||
else onTimeRangeChange(value as TimeRangeOption);
|
||||
};
|
||||
const toggleProcessors = (ids: string[], checked: boolean) => {
|
||||
const next = new Set(selectedPipelines);
|
||||
ids.forEach((id) => checked ? next.add(id) : next.delete(id));
|
||||
onPipelinesChange([...next]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid min-w-0 flex-1 grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-5">
|
||||
{/* Bot Filter */}
|
||||
@@ -212,41 +235,33 @@ export default function MonitoringFilters({
|
||||
>
|
||||
{t('monitoring.filters.processor')}
|
||||
</Label>
|
||||
<Select
|
||||
value={selectedPipelines.length === 0 ? 'all' : selectedPipelines[0]}
|
||||
onValueChange={handlePipelineChange}
|
||||
disabled={loadingProcessors}
|
||||
>
|
||||
<SelectTrigger
|
||||
id="monitoring-filter-processor"
|
||||
className="h-9 w-full min-w-0"
|
||||
>
|
||||
<SelectValue
|
||||
placeholder={
|
||||
loadingProcessors
|
||||
? t('monitoring.filters.loading')
|
||||
: t('monitoring.filters.selectProcessor')
|
||||
}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">
|
||||
{t('monitoring.filters.allProcessors')}
|
||||
</SelectItem>
|
||||
{processors.map((pipeline) => (
|
||||
<SelectItem key={pipeline.uuid} value={pipeline.uuid}>
|
||||
<span className="flex min-w-0 items-center gap-2">
|
||||
{pipeline.emoji && (
|
||||
<span className="shrink-0 text-sm" aria-hidden="true">
|
||||
{pipeline.emoji}
|
||||
</span>
|
||||
)}
|
||||
<span className="truncate">{pipeline.name}</span>
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button id="monitoring-filter-processor" variant="outline" disabled={loadingProcessors} className="h-9 w-full justify-between font-normal">
|
||||
<span className="truncate">{selectedPipelines.length ? t('agents.apiToolsSelected', { count: selectedPipelines.length }) : t('monitoring.filters.allProcessors')}</span>
|
||||
<ChevronDown className="ml-2 size-4 shrink-0 text-muted-foreground" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="start" className="max-h-96 w-72 overflow-y-auto p-2">
|
||||
<Button variant="ghost" size="sm" className="mb-1 w-full justify-start" onClick={() => onPipelinesChange([])}>{t('monitoring.filters.allProcessors')}</Button>
|
||||
{['pipeline', 'agent', 'event_processor'].map((kind) => {
|
||||
const group = processors.filter((item) => item.kind === kind);
|
||||
if (!group.length) return null;
|
||||
const selected = group.filter((item) => selectedPipelines.includes(item.uuid)).length;
|
||||
return <div key={kind} className="border-t py-2">
|
||||
<label className="mb-1 flex cursor-pointer items-center gap-2 rounded px-2 py-1.5 text-xs font-medium text-muted-foreground hover:bg-muted">
|
||||
<Checkbox checked={selected === group.length ? true : selected ? 'indeterminate' : false} onCheckedChange={(checked) => toggleProcessors(group.map((item) => item.uuid), checked === true)} />
|
||||
<span className="flex-1">{t(`monitoring.execution.kind.${kind === 'event_processor' ? 'processor' : kind}`)}</span>
|
||||
<span>{t('agents.apiToolsSelectAll')}</span>
|
||||
</label>
|
||||
{group.map((item) => <label key={item.uuid} className="flex cursor-pointer items-center gap-2 rounded px-2 py-2 text-sm hover:bg-muted">
|
||||
<Checkbox checked={selectedPipelines.includes(item.uuid)} onCheckedChange={(checked) => toggleProcessors([item.uuid], checked === true)} />
|
||||
<span aria-hidden="true">{item.emoji}</span><span className="truncate">{item.name}</span>
|
||||
</label>)}
|
||||
</div>;
|
||||
})}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
{/* Mode Filter */}
|
||||
@@ -334,14 +349,21 @@ export default function MonitoringFilters({
|
||||
>
|
||||
{t('monitoring.filters.timeRange')}
|
||||
</Label>
|
||||
<Select value={timeRange} onValueChange={handleTimeRangeChange}>
|
||||
<Select value={timeRange === 'custom' ? '' : timeRange} onValueChange={handleTimeRangeChange}>
|
||||
<SelectTrigger
|
||||
id="monitoring-filter-time"
|
||||
className="h-9 w-full min-w-0"
|
||||
className="h-9 w-full min-w-0 gap-2 [&>svg]:shrink-0"
|
||||
title={timeRange === 'custom' && customDateRange ? `${customDateRange.from.toLocaleString()} – ${customDateRange.to.toLocaleString()}` : undefined}
|
||||
>
|
||||
<SelectValue />
|
||||
{timeRange === 'custom' && customDateRange ? (
|
||||
<span className="min-w-0 truncate text-xs tabular-nums">
|
||||
{customDateRange.from.toLocaleDateString(undefined, { month: '2-digit', day: '2-digit' })}
|
||||
{' – '}
|
||||
{customDateRange.to.toLocaleDateString(undefined, { month: '2-digit', day: '2-digit' })}
|
||||
</span>
|
||||
) : <SelectValue />}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectContent onCloseAutoFocus={(event) => { if (customPending.current) { event.preventDefault(); customPending.current = false; openCustom(); } }}>
|
||||
<SelectItem value="lastHour">
|
||||
{t('monitoring.filters.lastHour')}
|
||||
</SelectItem>
|
||||
@@ -354,11 +376,17 @@ export default function MonitoringFilters({
|
||||
<SelectItem value="last7Days">
|
||||
{t('monitoring.filters.last7Days')}
|
||||
</SelectItem>
|
||||
<SelectItem value="last30Days">
|
||||
{t('monitoring.filters.last30Days')}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{[30, 90, 180, 365].map((days) => <SelectItem key={days} value={`last${days}Days`}>{t('monitoring.rangeDays', { days })}</SelectItem>)}
|
||||
{onCustomDateRangeChange && <SelectItem value="custom">{t('monitoring.customRange')}</SelectItem>} </SelectContent>
|
||||
</Select> {onCustomDateRangeChange && <>
|
||||
<Dialog open={dateOpen} onOpenChange={setDateOpen}><DialogContent className="sm:max-w-md space-y-3">
|
||||
<DialogHeader><DialogTitle>{t('monitoring.customRange')}</DialogTitle><DialogDescription>{t('monitoring.rangeHint')}</DialogDescription></DialogHeader>
|
||||
<label className="block space-y-1 text-xs">{t('monitoring.rangeStart')}<Input type="datetime-local" value={fromDate} max={toDate} onChange={(event) => setFromDate(event.target.value)} /></label>
|
||||
<label className="block space-y-1 text-xs">{t('monitoring.rangeEnd')}<Input type="datetime-local" value={toDate} min={fromDate} max={localDate(new Date())} onChange={(event) => setToDate(event.target.value)} /></label>
|
||||
|
||||
<Button size="sm" className="w-full" disabled={!validDates} onClick={() => { onCustomDateRangeChange({ from, to }); onTimeRangeChange('custom'); setDateOpen(false); }}>{t('common.confirm')}</Button>
|
||||
</DialogContent></Dialog>
|
||||
</>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { BarChart3 } from 'lucide-react';
|
||||
import { BarChart3, ChevronDown } from 'lucide-react';
|
||||
import {
|
||||
AreaChart,
|
||||
Area,
|
||||
@@ -9,9 +9,9 @@ import {
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
ResponsiveContainer,
|
||||
Legend,
|
||||
} from 'recharts';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { MonitoringData } from '../../types/monitoring';
|
||||
|
||||
@@ -20,7 +20,40 @@ interface TrafficChartProps {
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
export default function TrafficChart({ traffic, loading }: TrafficChartProps) {
|
||||
export default function TrafficChart(props: TrafficChartProps) {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(() => {
|
||||
try { return localStorage.getItem('langbot-traffic-chart-open') !== 'false'; }
|
||||
catch { return true; }
|
||||
});
|
||||
return (
|
||||
<Collapsible open={open} onOpenChange={(value) => {
|
||||
setOpen(value);
|
||||
try { localStorage.setItem('langbot-traffic-chart-open', String(value)); } catch { /* Storage may be unavailable. */ }
|
||||
}} asChild>
|
||||
<Card className="gap-0 overflow-hidden py-0">
|
||||
<CollapsibleTrigger asChild>
|
||||
<button type="button" className="group flex w-full items-center gap-3 px-5 py-4 text-left transition-colors hover:bg-muted/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring">
|
||||
<span className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-blue-500/10 text-blue-600 dark:text-blue-400"><BarChart3 className="size-4" /></span>
|
||||
<span className="flex-1 text-sm font-semibold">{t('monitoring.trafficChart.title')}</span>
|
||||
<span className="hidden items-center gap-4 text-xs text-muted-foreground sm:flex">
|
||||
<span className="flex items-center gap-1.5"><span className="size-1.5 rounded-full bg-blue-500" />{t('monitoring.trafficChart.messages')}</span>
|
||||
<span className="flex items-center gap-1.5"><span className="size-1.5 rounded-full bg-violet-500" />{t('monitoring.trafficChart.llmCalls')}</span>
|
||||
</span>
|
||||
<ChevronDown className={`ml-2 size-4 text-muted-foreground transition-transform motion-reduce:transition-none ${open ? 'rotate-180' : ''}`} />
|
||||
</button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div className="border-t px-3 pb-4 pt-4 sm:px-5">
|
||||
<TrafficChartContent {...props} />
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Card>
|
||||
</Collapsible>
|
||||
);
|
||||
}
|
||||
|
||||
function TrafficChartContent({ traffic, loading }: TrafficChartProps) {
|
||||
const { t } = useTranslation();
|
||||
const chartData = useMemo(
|
||||
() =>
|
||||
@@ -43,27 +76,17 @@ export default function TrafficChart({ traffic, loading }: TrafficChartProps) {
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<Skeleton className="h-5 w-32 rounded" />
|
||||
<div className="flex gap-4">
|
||||
<Skeleton className="h-4 w-24 rounded" />
|
||||
<Skeleton className="h-4 w-24 rounded" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="h-[300px] flex items-center justify-center">
|
||||
<Skeleton className="w-full h-full rounded" />
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (chartData.length === 0) {
|
||||
return (
|
||||
<Card className="p-6">
|
||||
<h3 className="text-base font-semibold text-foreground mb-4">
|
||||
{t('monitoring.trafficChart.title')}
|
||||
</h3>
|
||||
<div>
|
||||
<div className="h-[300px] flex flex-col items-center justify-center text-muted-foreground gap-2">
|
||||
<BarChart3 className="h-[3rem] w-[3rem]" />
|
||||
<div className="text-sm">
|
||||
@@ -74,15 +97,12 @@ export default function TrafficChart({ traffic, loading }: TrafficChartProps) {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="p-6 transition-shadow duration-300">
|
||||
<h3 className="text-base font-semibold text-foreground mb-6">
|
||||
{t('monitoring.trafficChart.title')}
|
||||
</h3>
|
||||
<div>
|
||||
{traffic?.truncated && (
|
||||
<p role="status" className="text-sm text-muted-foreground mb-3">
|
||||
{t('monitoring.trafficChart.truncated')}
|
||||
@@ -141,15 +161,6 @@ export default function TrafficChart({ traffic, loading }: TrafficChartProps) {
|
||||
}}
|
||||
itemStyle={{ padding: '4px 0' }}
|
||||
/>
|
||||
<Legend
|
||||
wrapperStyle={{
|
||||
fontSize: '13px',
|
||||
paddingTop: '16px',
|
||||
fontWeight: 500,
|
||||
}}
|
||||
iconType="circle"
|
||||
iconSize={10}
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="messages"
|
||||
@@ -175,6 +186,6 @@ export default function TrafficChart({ traffic, loading }: TrafficChartProps) {
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,9 @@ const timeRanges = [
|
||||
'last24Hours',
|
||||
'last7Days',
|
||||
'last30Days',
|
||||
'last90Days',
|
||||
'last180Days',
|
||||
'last365Days',
|
||||
'custom',
|
||||
];
|
||||
const statuses = [
|
||||
@@ -52,7 +55,7 @@ function restore(key: string): FilterState {
|
||||
if (
|
||||
Number.isFinite(from.getTime()) &&
|
||||
Number.isFinite(to.getTime()) &&
|
||||
from <= to
|
||||
from < to && to.getTime() - from.getTime() <= 365 * 86400000
|
||||
)
|
||||
state.customDateRange = { from, to };
|
||||
}
|
||||
|
||||
@@ -4,10 +4,12 @@ import React, {
|
||||
useEffect,
|
||||
useMemo,
|
||||
useCallback,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger } from '@/components/ui/select';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@@ -53,6 +55,7 @@ function MonitoringPageContent() {
|
||||
setSelectedBots,
|
||||
setSelectedPipelines,
|
||||
setTimeRange,
|
||||
setCustomDateRange,
|
||||
setExecutionMode,
|
||||
setExecutionStatus,
|
||||
resetFilters,
|
||||
@@ -134,13 +137,46 @@ function MonitoringPageContent() {
|
||||
limit: 50,
|
||||
});
|
||||
|
||||
const countdown = useRef(0);
|
||||
const intervalRef = useRef(0);
|
||||
const [remainingSeconds, setRemainingSeconds] = useState(0);
|
||||
// Combined refresh handler for both monitoring and feedback data
|
||||
const handleRefresh = useCallback(() => {
|
||||
countdown.current = intervalRef.current;
|
||||
setRemainingSeconds(countdown.current);
|
||||
refetch();
|
||||
refetchExecutions();
|
||||
setFeedbackRefreshKey((k) => k + 1);
|
||||
}, [refetch, refetchExecutions]);
|
||||
|
||||
const [refreshInterval, setRefreshInterval] = useState(() => {
|
||||
try {
|
||||
const saved = Number(localStorage.getItem('langbot-dashboard-refresh-interval'));
|
||||
return [5, 15, 30, 60, 300].includes(saved) ? saved : 0;
|
||||
} catch { return 0; }
|
||||
});
|
||||
const refreshState = useRef({ handleRefresh, busy: false });
|
||||
useEffect(() => {
|
||||
refreshState.current = { handleRefresh, busy: loading || executionLoading || feedbackLoading };
|
||||
}, [handleRefresh, loading, executionLoading, feedbackLoading]);
|
||||
useEffect(() => {
|
||||
intervalRef.current = refreshInterval;
|
||||
countdown.current = refreshInterval;
|
||||
setRemainingSeconds(refreshInterval);
|
||||
if (!refreshInterval) return;
|
||||
const timer = window.setInterval(() => {
|
||||
if (document.visibilityState === 'visible' && !refreshState.current.busy) {
|
||||
countdown.current = Math.max(0, countdown.current - 1);
|
||||
setRemainingSeconds(countdown.current);
|
||||
if (countdown.current === 0) {
|
||||
refreshState.current.busy = true;
|
||||
refreshState.current.handleRefresh();
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
return () => window.clearInterval(timer);
|
||||
}, [refreshInterval]);
|
||||
|
||||
// State for expanded errors
|
||||
const [expandedErrorId, setExpandedErrorId] = useState<string | null>(null);
|
||||
|
||||
@@ -174,6 +210,8 @@ function MonitoringPageContent() {
|
||||
onBotsChange={setSelectedBots}
|
||||
onPipelinesChange={setSelectedPipelines}
|
||||
onTimeRangeChange={setTimeRange}
|
||||
customDateRange={filterState.customDateRange}
|
||||
onCustomDateRangeChange={setCustomDateRange}
|
||||
mode={executionMode}
|
||||
onModeChange={setExecutionMode}
|
||||
statusGroup={executionStatus}
|
||||
@@ -185,15 +223,38 @@ function MonitoringPageContent() {
|
||||
{t('monitoring.filters.reset')}
|
||||
</Button>
|
||||
{canExport && <ExportDropdown filterState={filterState} />}
|
||||
<div className="relative flex items-center overflow-hidden rounded-lg border bg-background shadow-xs">
|
||||
<Button
|
||||
variant="outline"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleRefresh}
|
||||
className="shadow-sm"
|
||||
disabled={loading || executionLoading || feedbackLoading}
|
||||
className="gap-2 rounded-none px-3"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
<RefreshCw className={cn('size-3.5', (loading || executionLoading || feedbackLoading) && 'animate-spin')} />
|
||||
{t('monitoring.refreshData')}
|
||||
{refreshInterval > 0 && <span className="min-w-8 text-right text-xs tabular-nums text-muted-foreground">{remainingSeconds}s</span>}
|
||||
</Button>
|
||||
<Select value={String(refreshInterval)} onValueChange={(value) => {
|
||||
setRefreshInterval(Number(value));
|
||||
try { localStorage.setItem('langbot-dashboard-refresh-interval', value); } catch { /* Storage may be unavailable. */ }
|
||||
}}>
|
||||
<SelectTrigger className="w-11 justify-center rounded-l-none border-0 border-l px-2 shadow-none [&>svg:last-child]:hidden" aria-label={t('monitoring.autoRefreshLabel')} title={refreshInterval ? t('monitoring.autoRefreshEvery', { seconds: refreshInterval }) : t('monitoring.autoRefreshOff')}>
|
||||
<span aria-hidden="true"><ChevronDown className="size-3.5" /></span>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="0">{t('monitoring.autoRefreshOff')}</SelectItem>
|
||||
{[5, 15, 30, 60, 300].map((seconds) => (
|
||||
<SelectItem key={seconds} value={String(seconds)}>
|
||||
{t('monitoring.autoRefreshEvery', { seconds })}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{refreshInterval > 0 && <div className="pointer-events-none absolute inset-x-0 bottom-0 h-0.5 bg-blue-500/10" aria-hidden="true">
|
||||
<div className="h-full origin-left bg-blue-500/70 transition-transform duration-700 ease-linear motion-reduce:transition-none" style={{ transform: `scaleX(${remainingSeconds / refreshInterval})` }} />
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -178,6 +178,9 @@ export type TimeRangeOption =
|
||||
| 'last24Hours'
|
||||
| 'last7Days'
|
||||
| 'last30Days'
|
||||
| 'last90Days'
|
||||
| 'last180Days'
|
||||
| 'last365Days'
|
||||
| 'custom';
|
||||
|
||||
export interface DateRange {
|
||||
|
||||
@@ -22,8 +22,11 @@ export function getPresetDateRange(option: TimeRangeOption): DateRange | null {
|
||||
case 'last7Days':
|
||||
from.setDate(now.getDate() - 7);
|
||||
break;
|
||||
case 'last90Days':
|
||||
case 'last180Days':
|
||||
case 'last365Days':
|
||||
case 'last30Days':
|
||||
from.setDate(now.getDate() - 30);
|
||||
from.setTime(now.getTime() - Number(option.match(/\d+/)?.[0] ?? 30) * 86400000);
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
@@ -143,8 +146,11 @@ export function resolveMonitoringWindow(
|
||||
case 'last7Days':
|
||||
startTime = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
||||
break;
|
||||
case 'last90Days':
|
||||
case 'last180Days':
|
||||
case 'last365Days':
|
||||
case 'last30Days':
|
||||
startTime = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
||||
startTime = new Date(now.getTime() - Number(timeRange.match(/\d+/)?.[0] ?? 30) * 86400000);
|
||||
break;
|
||||
case 'custom':
|
||||
if (customDateRange) {
|
||||
|
||||
@@ -57,7 +57,7 @@ export default function PipelineMonitoringTab({
|
||||
const [activeTab, setActiveTab] = useState<string>('messages');
|
||||
|
||||
const toggleTurnExpand = (turnId: string) => {
|
||||
setExpandedTurnId((current) => (current === turnId ? null : turnId));
|
||||
setExpandedTurnId(turnId);
|
||||
};
|
||||
|
||||
const toggleErrorExpand = (errorId: string) => {
|
||||
@@ -83,7 +83,7 @@ export default function PipelineMonitoringTab({
|
||||
return (
|
||||
<div className="w-full h-full flex flex-col">
|
||||
{/* Header with refresh button */}
|
||||
<div className="flex items-center justify-between mb-4 pb-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="mb-4 flex flex-wrap items-center justify-between gap-3 border-b pb-4">
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{t('pipelines.monitoring.description')}
|
||||
</p>
|
||||
@@ -113,8 +113,8 @@ export default function PipelineMonitoringTab({
|
||||
|
||||
{/* Overview Stats */}
|
||||
{data && (
|
||||
<div className="grid grid-cols-3 gap-4 mb-6">
|
||||
<div className="bg-white dark:bg-[#2a2a2e] rounded-lg border border-gray-200 dark:border-gray-700 p-4">
|
||||
<div className="mb-4 grid grid-cols-3 divide-x rounded-xl border bg-card">
|
||||
<div className="p-3">
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{t('monitoring.totalMessages')}
|
||||
</div>
|
||||
@@ -122,7 +122,7 @@ export default function PipelineMonitoringTab({
|
||||
{data.overview.totalMessages}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white dark:bg-[#2a2a2e] rounded-lg border border-gray-200 dark:border-gray-700 p-4">
|
||||
<div className="p-3">
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{t('monitoring.successRate')}
|
||||
</div>
|
||||
@@ -130,7 +130,7 @@ export default function PipelineMonitoringTab({
|
||||
{data.overview.successRate.toFixed(1)}%
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white dark:bg-[#2a2a2e] rounded-lg border border-gray-200 dark:border-gray-700 p-4">
|
||||
<div className="p-3">
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{t('monitoring.tabs.errors')}
|
||||
</div>
|
||||
|
||||
@@ -824,7 +824,7 @@ export default function WizardPage() {
|
||||
|
||||
if (!previewPipelineUuid) {
|
||||
const pipelineResp = await httpClient.createPipeline({
|
||||
name: `${botName} Pipeline`,
|
||||
name: `${botName} ${t('agents.pipelineType')}`,
|
||||
description: botDescription || '',
|
||||
config: {},
|
||||
});
|
||||
|
||||
@@ -76,6 +76,14 @@ const enUS = {
|
||||
sidebarGuide: {
|
||||
steps: {
|
||||
monitoring: {
|
||||
rangeDays: 'Last {{days}} days',
|
||||
customRange: 'Custom time range',
|
||||
rangeStart: 'Start time',
|
||||
rangeEnd: 'End time',
|
||||
rangeHint: 'Start must precede end. Maximum range: 365 days.',
|
||||
autoRefreshLabel: 'Auto refresh',
|
||||
autoRefreshOff: 'Auto refresh: off',
|
||||
autoRefreshEvery: 'Refresh every {{seconds}} s',
|
||||
title: 'Review activity',
|
||||
description:
|
||||
'Review bot activity, model usage, message volume, and system performance at a glance.',
|
||||
@@ -157,6 +165,14 @@ const enUS = {
|
||||
'Send test messages to check triggers, AI replies, and output processing. Any configuration changes are saved before testing.',
|
||||
},
|
||||
monitoring: {
|
||||
rangeDays: 'Last {{days}} days',
|
||||
customRange: 'Custom time range',
|
||||
rangeStart: 'Start time',
|
||||
rangeEnd: 'End time',
|
||||
rangeHint: 'Start must precede end. Maximum range: 365 days.',
|
||||
autoRefreshLabel: 'Auto refresh',
|
||||
autoRefreshOff: 'Auto refresh: off',
|
||||
autoRefreshEvery: 'Refresh every {{seconds}} s',
|
||||
title: 'Review run history',
|
||||
description:
|
||||
'Use the tab beside the title to review past conversations, processing steps, and errors when replies are missing or unexpected.',
|
||||
@@ -787,7 +803,7 @@ const enUS = {
|
||||
legacyAdapters: 'Legacy adapters',
|
||||
legacyAdapterBadge: 'Legacy',
|
||||
legacyAdaptersHint:
|
||||
'These adapters have newer event-driven counterparts.\nThey are kept only for existing configurations and are not recommended for new bots.',
|
||||
'Legacy adapters mainly send and receive messages and remain available for existing bot configurations.\nNew adapters connect to the same platform using events, including messages and supported friend requests or group changes, routed to processors.\nPrefer the new adapter for new bots. Existing bots can keep using legacy adapters. When switching, review connection settings and event routes; this does not upgrade your platform account.',
|
||||
adapterConfig: 'Adapter Configuration',
|
||||
viewAdapterDocs: 'View Docs',
|
||||
bindPipeline: 'Bind Pipeline',
|
||||
@@ -808,6 +824,25 @@ const enUS = {
|
||||
routingConnection: 'Routing & Connection',
|
||||
routingConnectionDescription:
|
||||
'Bind the pipeline that processes messages for this bot',
|
||||
routingHelp: {
|
||||
title: 'What is event routing?',
|
||||
intro: 'Bots receive more than messages: friend requests, group invitations and other events too.',
|
||||
events: 'Incoming events',
|
||||
agents: 'Different agents',
|
||||
message: 'Message received',
|
||||
friend: 'Friend request',
|
||||
group: 'Joined a group',
|
||||
messageAction: 'Reply agent → Answer',
|
||||
friendAction: 'Review agent → Review',
|
||||
groupAction: 'Welcome agent → Greet',
|
||||
rules: 'Your routing rules · Example',
|
||||
instructions: 'Agents follow your instructions and use tools to reply, review requests and more.',
|
||||
plugin: 'For advanced workflows, plugin processors register events in bulk and handle them with code. Add a plugin processor configuration below.',
|
||||
docs: 'Develop your own plugin processor',
|
||||
runnerDocs: 'Develop your own Agent runner',
|
||||
pause: 'Pause',
|
||||
play: 'Play',
|
||||
},
|
||||
eventRouting: 'Event Routing',
|
||||
eventRoutingDescription: 'Choose which processor handles each event.',
|
||||
eventBindings: 'Event Routes',
|
||||
@@ -870,6 +905,8 @@ const enUS = {
|
||||
routeFallbackCatchAll: '{{route}} is the catch-all route.',
|
||||
routeFallbackIgnored:
|
||||
'Events that match no route are ignored. Add a catch-all route only when every event needs an explicit outcome.',
|
||||
matchRecords: 'Match records',
|
||||
matchRecordsDescription: 'Latest result for each rule and recent unmatched events, from bot memory logs. Cleared on restart.',
|
||||
testRoute: 'Check route',
|
||||
adapterEventDebugAction: 'Test listener',
|
||||
adapterEventDebugTitle: 'Platform event debugging',
|
||||
@@ -1128,7 +1165,16 @@ const enUS = {
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
apiToolsSelectAll: 'Select all',
|
||||
monitoring: {
|
||||
rangeDays: 'Last {{days}} days',
|
||||
customRange: 'Custom time range',
|
||||
rangeStart: 'Start time',
|
||||
rangeEnd: 'End time',
|
||||
rangeHint: 'Start must precede end. Maximum range: 365 days.',
|
||||
autoRefreshLabel: 'Auto refresh',
|
||||
autoRefreshOff: 'Auto refresh: off',
|
||||
autoRefreshEvery: 'Refresh every {{seconds}} s',
|
||||
runStatus: "Run status",
|
||||
toolCalls: "Tool calls",
|
||||
delivery: "Message delivery",
|
||||
@@ -1243,6 +1289,9 @@ const enUS = {
|
||||
'Handle message events only, with AI generating replies directly and practical features such as knowledge bases and plugins.',
|
||||
allEvents: 'Supports all events',
|
||||
messageEventsOnly: 'Message events only',
|
||||
selectAgentRunner: 'Select Agent runner',
|
||||
selectProcessorPlugin: 'Select processor plugin',
|
||||
createBasicInfoDescription: 'Set a name, icon and plugin',
|
||||
chooseType: 'Choose how it works',
|
||||
chooseTypeDescription:
|
||||
'Choose the processor model first. You can configure it further after creation.',
|
||||
@@ -2061,6 +2110,14 @@ const enUS = {
|
||||
uploading: 'Uploading...',
|
||||
},
|
||||
monitoring: {
|
||||
rangeDays: 'Last {{days}} days',
|
||||
customRange: 'Custom time range',
|
||||
rangeStart: 'Start time',
|
||||
rangeEnd: 'End time',
|
||||
rangeHint: 'Start must precede end. Maximum range: 365 days.',
|
||||
autoRefreshLabel: 'Auto refresh',
|
||||
autoRefreshOff: 'Auto refresh: off',
|
||||
autoRefreshEvery: 'Refresh every {{seconds}} s',
|
||||
title: 'Run logs',
|
||||
workbench: 'Configure & debug',
|
||||
description:
|
||||
@@ -2490,6 +2547,14 @@ const enUS = {
|
||||
},
|
||||
},
|
||||
monitoring: {
|
||||
rangeDays: 'Last {{days}} days',
|
||||
customRange: 'Custom time range',
|
||||
rangeStart: 'Start time',
|
||||
rangeEnd: 'End time',
|
||||
rangeHint: 'Start must precede end. Maximum range: 365 days.',
|
||||
autoRefreshLabel: 'Auto refresh',
|
||||
autoRefreshOff: 'Auto refresh: off',
|
||||
autoRefreshEvery: 'Refresh every {{seconds}} s',
|
||||
unified: {
|
||||
inputs: 'Event / input',
|
||||
outputs: 'Generated results',
|
||||
@@ -2643,6 +2708,15 @@ const enUS = {
|
||||
sessions: 'Session Analysis',
|
||||
errors: 'Error Logs',
|
||||
},
|
||||
pipelineTrace: {
|
||||
received: 'Received message',
|
||||
processing: 'Processing',
|
||||
replies: 'Replies',
|
||||
noSteps: 'No model or tool calls recorded',
|
||||
success: 'Success',
|
||||
error: 'Failed',
|
||||
pending: 'Pending',
|
||||
},
|
||||
messageList: {
|
||||
timestamp: 'Timestamp',
|
||||
bot: 'Bot',
|
||||
|
||||
@@ -79,6 +79,14 @@ const esES = {
|
||||
sidebarGuide: {
|
||||
steps: {
|
||||
monitoring: {
|
||||
rangeDays: 'Últimos {{days}} días',
|
||||
customRange: 'Periodo personalizado',
|
||||
rangeStart: 'Inicio',
|
||||
rangeEnd: 'Fin',
|
||||
rangeHint: 'El inicio debe preceder al fin. Máximo: 365 días.',
|
||||
autoRefreshLabel: 'Actualización automática',
|
||||
autoRefreshOff: 'Actualización: desactivada',
|
||||
autoRefreshEvery: 'Actualizar cada {{seconds}} s',
|
||||
title: 'Revisar la actividad',
|
||||
description:
|
||||
'Consulta la actividad de los bots, el uso de modelos, los mensajes y el rendimiento del sistema.',
|
||||
@@ -160,6 +168,14 @@ const esES = {
|
||||
'Envía mensajes de prueba para comprobar los activadores, las respuestas de IA y el procesamiento de salida. Los cambios se guardan antes de la prueba.',
|
||||
},
|
||||
monitoring: {
|
||||
rangeDays: 'Últimos {{days}} días',
|
||||
customRange: 'Periodo personalizado',
|
||||
rangeStart: 'Inicio',
|
||||
rangeEnd: 'Fin',
|
||||
rangeHint: 'El inicio debe preceder al fin. Máximo: 365 días.',
|
||||
autoRefreshLabel: 'Actualización automática',
|
||||
autoRefreshOff: 'Actualización: desactivada',
|
||||
autoRefreshEvery: 'Actualizar cada {{seconds}} s',
|
||||
title: 'Revisar el historial',
|
||||
description:
|
||||
'Usa la pestaña junto al título para revisar conversaciones, pasos de procesamiento y errores cuando falten respuestas o sean inesperadas.',
|
||||
@@ -803,7 +819,7 @@ const esES = {
|
||||
legacyAdapters: 'Adaptadores heredados',
|
||||
legacyAdapterBadge: 'Heredado',
|
||||
legacyAdaptersHint:
|
||||
'Estos adaptadores tienen versiones nuevas orientadas a eventos.\nSe mantienen solo para configuraciones existentes y no se recomiendan para Bots nuevos.',
|
||||
'Los adaptadores antiguos se centran en mensajes y conservan la compatibilidad con bots existentes.\nLos nuevos conectan la misma plataforma mediante eventos, incluidos mensajes y solicitudes de amistad o cambios de grupo compatibles.\nUsa los nuevos para bots nuevos. Los existentes pueden seguir con los antiguos. Al cambiar, revisa la conexión y las rutas; no se actualiza la cuenta de la plataforma.',
|
||||
adapterConfig: 'Configuración del adaptador',
|
||||
viewAdapterDocs: 'Ver documentación',
|
||||
bindPipeline: 'Vincular Pipeline',
|
||||
@@ -824,6 +840,25 @@ const esES = {
|
||||
routingConnection: 'Enrutamiento y conexión',
|
||||
routingConnectionDescription:
|
||||
'Vincula el Pipeline que procesa los mensajes de este Bot',
|
||||
routingHelp: {
|
||||
title: '¿Qué es el enrutamiento de eventos?',
|
||||
intro: 'Los bots reciben mensajes, solicitudes de amistad y eventos como unirse a un grupo.',
|
||||
events: 'Eventos recibidos',
|
||||
agents: 'Distintos agentes',
|
||||
message: 'Mensaje recibido',
|
||||
friend: 'Solicitud de amistad',
|
||||
group: 'Entrada a un grupo',
|
||||
messageAction: 'Agente de respuestas',
|
||||
friendAction: 'Agente de revisión',
|
||||
groupAction: 'Agente de bienvenida',
|
||||
rules: 'Según tus reglas · Ejemplo',
|
||||
instructions: 'Los agentes siguen tus instrucciones y usan herramientas para responder o revisar solicitudes.',
|
||||
plugin: 'Para casos avanzados, los procesadores de plugins registran eventos en lote y los gestionan con código. Añade su configuración abajo.',
|
||||
docs: 'Desarrolla tu propio procesador de plugins',
|
||||
runnerDocs: 'Desarrolla tu propio ejecutor de Agent',
|
||||
pause: 'Pausar',
|
||||
play: 'Reproducir',
|
||||
},
|
||||
eventRouting: 'Enrutamiento de eventos',
|
||||
eventRoutingDescription:
|
||||
'Elige qué procesador maneja cada evento recibido por este Bot. Edita la lógica en la configuración del Agent o Pipeline correspondiente. Los Pipelines solo admiten eventos de mensaje.',
|
||||
@@ -888,6 +923,8 @@ const esES = {
|
||||
routeFallbackCatchAll: '{{route}} es la ruta general.',
|
||||
routeFallbackIgnored:
|
||||
'Se ignoran los eventos sin ruta coincidente. Añade una ruta general solo si todos los eventos necesitan un resultado explícito.',
|
||||
matchRecords: 'Registros de coincidencias',
|
||||
matchRecordsDescription: 'Último resultado de cada regla y eventos recientes sin coincidencia. Los registros en memoria se borran al reiniciar.',
|
||||
testRoute: 'Comprobar ruta',
|
||||
adapterEventDebugAction: 'Probar escucha',
|
||||
adapterEventDebugTitle: 'Depuración de eventos de plataforma',
|
||||
@@ -1150,7 +1187,16 @@ const esES = {
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
apiToolsSelectAll: 'Seleccionar todo',
|
||||
monitoring: {
|
||||
rangeDays: 'Últimos {{days}} días',
|
||||
customRange: 'Periodo personalizado',
|
||||
rangeStart: 'Inicio',
|
||||
rangeEnd: 'Fin',
|
||||
rangeHint: 'El inicio debe preceder al fin. Máximo: 365 días.',
|
||||
autoRefreshLabel: 'Actualización automática',
|
||||
autoRefreshOff: 'Actualización: desactivada',
|
||||
autoRefreshEvery: 'Actualizar cada {{seconds}} s',
|
||||
runStatus: "Estado de ejecución",
|
||||
toolCalls: "Llamadas a herramientas",
|
||||
delivery: "Envío de mensajes",
|
||||
@@ -1267,6 +1313,9 @@ const esES = {
|
||||
'Sigue un flujo fijo: recibir un mensaje, consultar a la IA y responder al usuario, con bases de conocimiento y plugins configurables. Solo procesa eventos de mensaje, para tareas con pasos claros y control del proceso.',
|
||||
allEvents: 'Compatible con todos los eventos',
|
||||
messageEventsOnly: 'Solo eventos de mensaje',
|
||||
selectAgentRunner: 'Seleccionar ejecutor de Agent',
|
||||
selectProcessorPlugin: 'Seleccionar plugin de procesamiento',
|
||||
createBasicInfoDescription: 'Configura el nombre, icono y plugin',
|
||||
chooseType: 'Elegir cómo funciona',
|
||||
chooseTypeDescription:
|
||||
'Elige primero el tipo de procesador. Podrás configurarlo después de crearlo.',
|
||||
@@ -2098,6 +2147,14 @@ const esES = {
|
||||
uploading: 'Subiendo...',
|
||||
},
|
||||
monitoring: {
|
||||
rangeDays: 'Últimos {{days}} días',
|
||||
customRange: 'Periodo personalizado',
|
||||
rangeStart: 'Inicio',
|
||||
rangeEnd: 'Fin',
|
||||
rangeHint: 'El inicio debe preceder al fin. Máximo: 365 días.',
|
||||
autoRefreshLabel: 'Actualización automática',
|
||||
autoRefreshOff: 'Actualización: desactivada',
|
||||
autoRefreshEvery: 'Actualizar cada {{seconds}} s',
|
||||
title: 'Registros de ejecución',
|
||||
workbench: 'Configurar y depurar',
|
||||
description:
|
||||
@@ -2543,6 +2600,14 @@ const esES = {
|
||||
},
|
||||
},
|
||||
monitoring: {
|
||||
rangeDays: 'Últimos {{days}} días',
|
||||
customRange: 'Periodo personalizado',
|
||||
rangeStart: 'Inicio',
|
||||
rangeEnd: 'Fin',
|
||||
rangeHint: 'El inicio debe preceder al fin. Máximo: 365 días.',
|
||||
autoRefreshLabel: 'Actualización automática',
|
||||
autoRefreshOff: 'Actualización: desactivada',
|
||||
autoRefreshEvery: 'Actualizar cada {{seconds}} s',
|
||||
unified: {
|
||||
inputs: 'Evento y entrada',
|
||||
outputs: 'Resultados generados',
|
||||
@@ -2697,6 +2762,15 @@ const esES = {
|
||||
sessions: 'Análisis de sesiones',
|
||||
errors: 'Registros de errores',
|
||||
},
|
||||
pipelineTrace: {
|
||||
received: 'Mensaje recibido',
|
||||
processing: 'Procesamiento',
|
||||
replies: 'Respuestas',
|
||||
noSteps: 'No se registraron llamadas a modelos o herramientas',
|
||||
success: 'Correcto',
|
||||
error: 'Error',
|
||||
pending: 'Pendiente',
|
||||
},
|
||||
messageList: {
|
||||
timestamp: 'Marca de tiempo',
|
||||
bot: 'Bot',
|
||||
|
||||
@@ -77,6 +77,14 @@ const jaJP = {
|
||||
sidebarGuide: {
|
||||
steps: {
|
||||
monitoring: {
|
||||
rangeDays: '過去 {{days}} 日間',
|
||||
customRange: '期間を指定',
|
||||
rangeStart: '開始日時',
|
||||
rangeEnd: '終了日時',
|
||||
rangeHint: '開始は終了より前、期間は最大365日です。',
|
||||
autoRefreshLabel: '自動更新',
|
||||
autoRefreshOff: '自動更新:オフ',
|
||||
autoRefreshEvery: '{{seconds}} 秒ごとに更新',
|
||||
title: '稼働状況を確認する',
|
||||
description:
|
||||
'ボットの稼働状況、モデルの使用量、メッセージ数、システムの状態をまとめて確認できます。',
|
||||
@@ -158,6 +166,14 @@ const jaJP = {
|
||||
'テストメッセージで起動条件、AI の返信、出力処理を確認します。設定の変更はテスト前に保存されます。',
|
||||
},
|
||||
monitoring: {
|
||||
rangeDays: '過去 {{days}} 日間',
|
||||
customRange: '期間を指定',
|
||||
rangeStart: '開始日時',
|
||||
rangeEnd: '終了日時',
|
||||
rangeHint: '開始は終了より前、期間は最大365日です。',
|
||||
autoRefreshLabel: '自動更新',
|
||||
autoRefreshOff: '自動更新:オフ',
|
||||
autoRefreshEvery: '{{seconds}} 秒ごとに更新',
|
||||
title: '実行履歴を確認する',
|
||||
description:
|
||||
'タイトル横のタブで過去の会話、処理過程、エラーを確認し、返信がない場合や想定外の回答を調べます。',
|
||||
@@ -790,7 +806,7 @@ const jaJP = {
|
||||
legacyAdapters: '旧式アダプター',
|
||||
legacyAdapterBadge: '旧式',
|
||||
legacyAdaptersHint:
|
||||
'これらのアダプターには新しいイベント駆動の接続方式があります。\n既存設定との互換性のために残しており、新規ボットには推奨されません。',
|
||||
'旧版は主にメッセージ送受信用で、既存の設定との互換性のために残されています。\n新版は同じプラットフォームにイベント方式で接続し、対応する友達申請やグループの変化も処理器へ振り分けます。\n新規ボットには新版を推奨します。既存ボットは旧版を継続利用できます。切り替え時は接続設定とイベントルートを確認してください。アカウントの更新ではありません。',
|
||||
adapterConfig: 'アダプター設定',
|
||||
viewAdapterDocs: 'ドキュメントを見る',
|
||||
bindPipeline: 'パイプラインを紐付け',
|
||||
@@ -811,6 +827,25 @@ const jaJP = {
|
||||
routingConnection: 'ルーティングと接続',
|
||||
routingConnectionDescription:
|
||||
'このボットのメッセージを処理するパイプラインを紐付け',
|
||||
routingHelp: {
|
||||
title: 'イベントルーティングとは?',
|
||||
intro: 'ボットはメッセージのほか、友達リクエストやグループ参加などのイベントも受け取ります。',
|
||||
events: '受信イベント',
|
||||
agents: '各 Agent へ',
|
||||
message: 'メッセージ受信',
|
||||
friend: '友達リクエスト',
|
||||
group: 'グループ参加',
|
||||
messageAction: '応答 Agent → 返信',
|
||||
friendAction: '審査 Agent → 確認',
|
||||
groupAction: '歓迎 Agent → 挨拶',
|
||||
rules: '設定したルールで振り分け · 例',
|
||||
instructions: 'Agent は指示に従ってツールを使い、返信や申請の確認を行います。',
|
||||
plugin: '高度な用途にはプラグイン処理器を利用できます。イベントを一括登録してコードで処理し、下の欄で設定を追加します。',
|
||||
docs: '独自のプラグイン処理器を開発',
|
||||
runnerDocs: '独自の Agent ランナーを開発',
|
||||
pause: '一時停止',
|
||||
play: '再生',
|
||||
},
|
||||
eventRouting: 'イベントルーティング',
|
||||
eventRoutingDescription: 'イベントごとの処理先を設定します。',
|
||||
eventBindings: 'イベントルート',
|
||||
@@ -875,6 +910,8 @@ const jaJP = {
|
||||
routeFallbackCatchAll: '{{route}} はフォールバックルートです。',
|
||||
routeFallbackIgnored:
|
||||
'どのルートにも一致しないイベントは無視されます。すべてのイベントに明示的な結果が必要な場合のみ、フォールバックを追加してください。',
|
||||
matchRecords: 'マッチ記録',
|
||||
matchRecordsDescription: '各ルールの最新結果と最近の未一致イベントを表示します。ボットのメモリ内ログは再起動時に消去されます。',
|
||||
testRoute: 'ルートを確認',
|
||||
adapterEventDebugAction: 'イベント監視テスト',
|
||||
adapterEventDebugTitle: 'プラットフォームイベントのデバッグ',
|
||||
@@ -1138,7 +1175,16 @@ const jaJP = {
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
apiToolsSelectAll: 'すべて選択',
|
||||
monitoring: {
|
||||
rangeDays: '過去 {{days}} 日間',
|
||||
customRange: '期間を指定',
|
||||
rangeStart: '開始日時',
|
||||
rangeEnd: '終了日時',
|
||||
rangeHint: '開始は終了より前、期間は最大365日です。',
|
||||
autoRefreshLabel: '自動更新',
|
||||
autoRefreshOff: '自動更新:オフ',
|
||||
autoRefreshEvery: '{{seconds}} 秒ごとに更新',
|
||||
runStatus: "実行状態",
|
||||
toolCalls: "ツール呼び出し",
|
||||
delivery: "メッセージ送信",
|
||||
@@ -1255,6 +1301,9 @@ const jaJP = {
|
||||
'メッセージイベントのみを処理し、AI が直接返信を生成します。ナレッジベースやプラグインなどの便利な機能も利用できます。',
|
||||
allEvents: 'すべてのイベントに対応',
|
||||
messageEventsOnly: 'メッセージイベントのみ',
|
||||
selectAgentRunner: 'Agent ランナーを選択',
|
||||
selectProcessorPlugin: '処理プラグインを選択',
|
||||
createBasicInfoDescription: '名前・アイコン・プラグインを設定',
|
||||
chooseType: '処理方法を選択',
|
||||
chooseTypeDescription:
|
||||
'最初にプロセッサーの動作方式を選びます。作成後に詳しく設定できます。',
|
||||
@@ -2076,6 +2125,14 @@ const jaJP = {
|
||||
uploading: 'アップロード中...',
|
||||
},
|
||||
monitoring: {
|
||||
rangeDays: '過去 {{days}} 日間',
|
||||
customRange: '期間を指定',
|
||||
rangeStart: '開始日時',
|
||||
rangeEnd: '終了日時',
|
||||
rangeHint: '開始は終了より前、期間は最大365日です。',
|
||||
autoRefreshLabel: '自動更新',
|
||||
autoRefreshOff: '自動更新:オフ',
|
||||
autoRefreshEvery: '{{seconds}} 秒ごとに更新',
|
||||
title: '実行ログ',
|
||||
workbench: '設定とデバッグ',
|
||||
description: 'このパイプラインの実行ログとエラー情報を表示(過去24時間)',
|
||||
@@ -2507,6 +2564,14 @@ const jaJP = {
|
||||
},
|
||||
},
|
||||
monitoring: {
|
||||
rangeDays: '過去 {{days}} 日間',
|
||||
customRange: '期間を指定',
|
||||
rangeStart: '開始日時',
|
||||
rangeEnd: '終了日時',
|
||||
rangeHint: '開始は終了より前、期間は最大365日です。',
|
||||
autoRefreshLabel: '自動更新',
|
||||
autoRefreshOff: '自動更新:オフ',
|
||||
autoRefreshEvery: '{{seconds}} 秒ごとに更新',
|
||||
unified: {
|
||||
inputs: 'イベントと入力',
|
||||
outputs: '生成結果',
|
||||
@@ -2660,6 +2725,15 @@ const jaJP = {
|
||||
sessions: 'セッション分析',
|
||||
errors: 'エラーログ',
|
||||
},
|
||||
pipelineTrace: {
|
||||
received: '受信メッセージ',
|
||||
processing: '処理の流れ',
|
||||
replies: '返信',
|
||||
noSteps: 'モデル・ツールの呼び出し記録はありません',
|
||||
success: '成功',
|
||||
error: '失敗',
|
||||
pending: '処理待ち',
|
||||
},
|
||||
messageList: {
|
||||
timestamp: 'タイムスタンプ',
|
||||
bot: 'ボット',
|
||||
|
||||
@@ -77,6 +77,14 @@ const ruRU = {
|
||||
sidebarGuide: {
|
||||
steps: {
|
||||
monitoring: {
|
||||
rangeDays: 'Последние {{days}} дней',
|
||||
customRange: 'Свой период',
|
||||
rangeStart: 'Начало',
|
||||
rangeEnd: 'Конец',
|
||||
rangeHint: 'Начало должно быть раньше конца. Максимум 365 дней.',
|
||||
autoRefreshLabel: 'Автообновление',
|
||||
autoRefreshOff: 'Автообновление: выкл.',
|
||||
autoRefreshEvery: 'Обновлять каждые {{seconds}} с',
|
||||
title: 'Проверьте активность',
|
||||
description:
|
||||
'Просматривайте активность ботов, использование моделей, объём сообщений и производительность системы.',
|
||||
@@ -158,6 +166,14 @@ const ruRU = {
|
||||
'Отправьте тестовые сообщения, чтобы проверить условия запуска, ответы ИИ и обработку вывода. Изменения сохраняются перед тестом.',
|
||||
},
|
||||
monitoring: {
|
||||
rangeDays: 'Последние {{days}} дней',
|
||||
customRange: 'Свой период',
|
||||
rangeStart: 'Начало',
|
||||
rangeEnd: 'Конец',
|
||||
rangeHint: 'Начало должно быть раньше конца. Максимум 365 дней.',
|
||||
autoRefreshLabel: 'Автообновление',
|
||||
autoRefreshOff: 'Автообновление: выкл.',
|
||||
autoRefreshEvery: 'Обновлять каждые {{seconds}} с',
|
||||
title: 'Посмотрите историю запусков',
|
||||
description:
|
||||
'На вкладке рядом с заголовком доступны прошлые диалоги, этапы обработки и ошибки для диагностики отсутствующих или неожиданных ответов.',
|
||||
@@ -798,7 +814,7 @@ const ruRU = {
|
||||
legacyAdapters: 'Устаревшие адаптеры',
|
||||
legacyAdapterBadge: 'Устаревший',
|
||||
legacyAdaptersHint:
|
||||
'У этих адаптеров есть новые событийные версии.\nОни сохранены только для существующих конфигураций и не рекомендуются для новых ботов.',
|
||||
'Старые адаптеры в основном обмениваются сообщениями и сохраняются для совместимости.\nНовые подключают ту же платформу через события: сообщения, поддерживаемые запросы в друзья и изменения в группах, направляя их обработчикам.\nДля новых ботов выбирайте новые адаптеры. Старые боты могут работать как прежде. При переходе проверьте подключение и маршруты; аккаунт платформы не обновляется.',
|
||||
adapterConfig: 'Настройка адаптера',
|
||||
viewAdapterDocs: 'Документация',
|
||||
bindPipeline: 'Привязать конвейер',
|
||||
@@ -819,6 +835,25 @@ const ruRU = {
|
||||
routingConnection: 'Маршрутизация и подключение',
|
||||
routingConnectionDescription:
|
||||
'Привяжите конвейер, обрабатывающий сообщения для этого бота',
|
||||
routingHelp: {
|
||||
title: 'Что такое маршрутизация событий?',
|
||||
intro: 'Бот получает сообщения, запросы в друзья и события вступления в группу.',
|
||||
events: 'Входящие события',
|
||||
agents: 'Разные агенты',
|
||||
message: 'Сообщение',
|
||||
friend: 'Запрос в друзья',
|
||||
group: 'Вступление в группу',
|
||||
messageAction: 'Агент ответов',
|
||||
friendAction: 'Агент проверки',
|
||||
groupAction: 'Агент приветствия',
|
||||
rules: 'По вашим правилам · Пример',
|
||||
instructions: 'Агенты следуют вашим инструкциям и вызывают инструменты для ответов и обработки запросов.',
|
||||
plugin: 'Для сложных задач обработчики плагинов регистрируют события пакетно и обрабатывают их кодом. Добавьте конфигурацию обработчика ниже.',
|
||||
docs: 'Разработать свой обработчик плагина',
|
||||
runnerDocs: 'Разработать свой исполнитель Agent',
|
||||
pause: 'Пауза',
|
||||
play: 'Воспроизвести',
|
||||
},
|
||||
eventRouting: 'Маршрутизация событий',
|
||||
eventRoutingDescription:
|
||||
'Выберите обработчик для каждого события бота. Изменяйте логику в настройках соответствующего Agent или Pipeline. Pipeline поддерживает только события сообщений.',
|
||||
@@ -883,6 +918,8 @@ const ruRU = {
|
||||
routeFallbackCatchAll: '{{route}} — маршрут для всех событий.',
|
||||
routeFallbackIgnored:
|
||||
'События без подходящего маршрута игнорируются. Добавляйте общий маршрут, только если каждое событие требует явного результата.',
|
||||
matchRecords: 'История совпадений',
|
||||
matchRecordsDescription: 'Последний результат каждого правила и недавние события без совпадений. Журнал в памяти очищается при перезапуске.',
|
||||
testRoute: 'Проверить маршрут',
|
||||
adapterEventDebugAction: 'Тест прослушивания',
|
||||
adapterEventDebugTitle: 'Отладка событий платформы',
|
||||
@@ -1148,7 +1185,16 @@ const ruRU = {
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
apiToolsSelectAll: 'Выбрать все',
|
||||
monitoring: {
|
||||
rangeDays: 'Последние {{days}} дней',
|
||||
customRange: 'Свой период',
|
||||
rangeStart: 'Начало',
|
||||
rangeEnd: 'Конец',
|
||||
rangeHint: 'Начало должно быть раньше конца. Максимум 365 дней.',
|
||||
autoRefreshLabel: 'Автообновление',
|
||||
autoRefreshOff: 'Автообновление: выкл.',
|
||||
autoRefreshEvery: 'Обновлять каждые {{seconds}} с',
|
||||
runStatus: "Статус выполнения",
|
||||
toolCalls: "Вызовы инструментов",
|
||||
delivery: "Отправка сообщений",
|
||||
@@ -1263,6 +1309,9 @@ const ruRU = {
|
||||
'Работает по заданному процессу: получить сообщение, вызвать AI и ответить пользователю. Поддерживает настройку баз знаний и плагинов; обрабатывает только события сообщений и подходит для задач с четкими шагами и контролем обработки.',
|
||||
allEvents: 'Поддерживает все события',
|
||||
messageEventsOnly: 'Только события сообщений',
|
||||
selectAgentRunner: 'Выберите исполнитель Agent',
|
||||
selectProcessorPlugin: 'Выберите плагин обработки',
|
||||
createBasicInfoDescription: 'Задайте имя, значок и плагин',
|
||||
chooseType: 'Выберите способ работы',
|
||||
chooseTypeDescription:
|
||||
'Сначала выберите тип обработчика. Остальные настройки доступны после создания.',
|
||||
@@ -2088,6 +2137,14 @@ const ruRU = {
|
||||
uploading: 'Загрузка...',
|
||||
},
|
||||
monitoring: {
|
||||
rangeDays: 'Последние {{days}} дней',
|
||||
customRange: 'Свой период',
|
||||
rangeStart: 'Начало',
|
||||
rangeEnd: 'Конец',
|
||||
rangeHint: 'Начало должно быть раньше конца. Максимум 365 дней.',
|
||||
autoRefreshLabel: 'Автообновление',
|
||||
autoRefreshOff: 'Автообновление: выкл.',
|
||||
autoRefreshEvery: 'Обновлять каждые {{seconds}} с',
|
||||
title: 'Журнал выполнения',
|
||||
workbench: 'Настройка и отладка',
|
||||
description:
|
||||
@@ -2515,6 +2572,14 @@ const ruRU = {
|
||||
},
|
||||
},
|
||||
monitoring: {
|
||||
rangeDays: 'Последние {{days}} дней',
|
||||
customRange: 'Свой период',
|
||||
rangeStart: 'Начало',
|
||||
rangeEnd: 'Конец',
|
||||
rangeHint: 'Начало должно быть раньше конца. Максимум 365 дней.',
|
||||
autoRefreshLabel: 'Автообновление',
|
||||
autoRefreshOff: 'Автообновление: выкл.',
|
||||
autoRefreshEvery: 'Обновлять каждые {{seconds}} с',
|
||||
unified: {
|
||||
inputs: 'Событие и ввод',
|
||||
outputs: 'Сгенерированные результаты',
|
||||
@@ -2668,6 +2733,15 @@ const ruRU = {
|
||||
sessions: 'Анализ сессий',
|
||||
errors: 'Журнал ошибок',
|
||||
},
|
||||
pipelineTrace: {
|
||||
received: 'Входящее сообщение',
|
||||
processing: 'Обработка',
|
||||
replies: 'Ответы',
|
||||
noSteps: 'Вызовы моделей и инструментов не зарегистрированы',
|
||||
success: 'Успешно',
|
||||
error: 'Ошибка',
|
||||
pending: 'Ожидание',
|
||||
},
|
||||
messageList: {
|
||||
timestamp: 'Время',
|
||||
bot: 'Бот',
|
||||
|
||||
@@ -75,6 +75,14 @@ const thTH = {
|
||||
sidebarGuide: {
|
||||
steps: {
|
||||
monitoring: {
|
||||
rangeDays: '{{days}} วันที่ผ่านมา',
|
||||
customRange: 'กำหนดช่วงเวลาเอง',
|
||||
rangeStart: 'เวลาเริ่มต้น',
|
||||
rangeEnd: 'เวลาสิ้นสุด',
|
||||
rangeHint: 'เวลาเริ่มต้องก่อนเวลาสิ้นสุด สูงสุด 365 วัน',
|
||||
autoRefreshLabel: 'รีเฟรชอัตโนมัติ',
|
||||
autoRefreshOff: 'รีเฟรชอัตโนมัติ: ปิด',
|
||||
autoRefreshEvery: 'รีเฟรชทุก {{seconds}} วินาที',
|
||||
title: 'ดูภาพรวมการทำงาน',
|
||||
description:
|
||||
'ดูการทำงานของบอท การใช้โมเดล ปริมาณข้อความ และประสิทธิภาพระบบ',
|
||||
@@ -155,6 +163,14 @@ const thTH = {
|
||||
'ส่งข้อความทดสอบเพื่อตรวจเงื่อนไข คำตอบ AI และการประมวลผลผลลัพธ์ การเปลี่ยนแปลงจะถูกบันทึกก่อนทดสอบ',
|
||||
},
|
||||
monitoring: {
|
||||
rangeDays: '{{days}} วันที่ผ่านมา',
|
||||
customRange: 'กำหนดช่วงเวลาเอง',
|
||||
rangeStart: 'เวลาเริ่มต้น',
|
||||
rangeEnd: 'เวลาสิ้นสุด',
|
||||
rangeHint: 'เวลาเริ่มต้องก่อนเวลาสิ้นสุด สูงสุด 365 วัน',
|
||||
autoRefreshLabel: 'รีเฟรชอัตโนมัติ',
|
||||
autoRefreshOff: 'รีเฟรชอัตโนมัติ: ปิด',
|
||||
autoRefreshEvery: 'รีเฟรชทุก {{seconds}} วินาที',
|
||||
title: 'ดูประวัติการทำงาน',
|
||||
description:
|
||||
'ใช้แท็บข้างชื่อเพื่อดูบทสนทนา ขั้นตอนการประมวลผล และข้อผิดพลาด เมื่อไม่มีคำตอบหรือได้คำตอบที่ไม่คาดคิด',
|
||||
@@ -774,7 +790,7 @@ const thTH = {
|
||||
legacyAdapters: 'อะแดปเตอร์รุ่นเก่า',
|
||||
legacyAdapterBadge: 'เก่า',
|
||||
legacyAdaptersHint:
|
||||
'อะแดปเตอร์เหล่านี้มีรุ่นใหม่แบบขับเคลื่อนด้วยเหตุการณ์แล้ว\nและเก็บไว้เพื่อความเข้ากันได้กับการตั้งค่าเดิมเท่านั้น ไม่แนะนำสำหรับ Bot ใหม่',
|
||||
'อะแดปเตอร์รุ่นเก่าเน้นรับส่งข้อความ และคงไว้เพื่อรองรับการตั้งค่าบอทเดิม\nรุ่นใหม่เชื่อมต่อแพลตฟอร์มเดียวกันผ่านเหตุการณ์ ทั้งข้อความ คำขอเป็นเพื่อนหรือการเปลี่ยนแปลงกลุ่มที่รองรับ แล้วส่งให้ตัวประมวลผล\nแนะนำรุ่นใหม่สำหรับบอทใหม่ บอทเดิมยังใช้รุ่นเก่าได้ เมื่อเปลี่ยนให้ตรวจการเชื่อมต่อและเส้นทางเหตุการณ์ ไม่ใช่การอัปเกรดบัญชีแพลตฟอร์ม',
|
||||
adapterConfig: 'การกำหนดค่าอะแดปเตอร์',
|
||||
viewAdapterDocs: 'ดูเอกสาร',
|
||||
bindPipeline: 'ผูก Pipeline',
|
||||
@@ -795,6 +811,25 @@ const thTH = {
|
||||
routingConnection: 'การกำหนดเส้นทางและการเชื่อมต่อ',
|
||||
routingConnectionDescription:
|
||||
'ผูก Pipeline ที่ประมวลผลข้อความสำหรับ Bot นี้',
|
||||
routingHelp: {
|
||||
title: 'การกำหนดเส้นทางเหตุการณ์คืออะไร?',
|
||||
intro: 'บอทรับได้ทั้งข้อความ คำขอเป็นเพื่อน และเหตุการณ์เข้าร่วมกลุ่ม',
|
||||
events: 'เหตุการณ์ที่ได้รับ',
|
||||
agents: 'Agent แต่ละตัว',
|
||||
message: 'ได้รับข้อความ',
|
||||
friend: 'คำขอเป็นเพื่อน',
|
||||
group: 'เข้าร่วมกลุ่ม',
|
||||
messageAction: 'Agent ตอบคำถาม',
|
||||
friendAction: 'Agent ตรวจคำขอ',
|
||||
groupAction: 'Agent ต้อนรับ',
|
||||
rules: 'ตามกฎที่คุณตั้ง · ตัวอย่าง',
|
||||
instructions: 'Agent ทำตามคำสั่งของคุณและใช้เครื่องมือเพื่อตอบข้อความหรือตรวจคำขอ',
|
||||
plugin: 'สำหรับงานขั้นสูง ตัวประมวลผลปลั๊กอินลงทะเบียนหลายเหตุการณ์และจัดการด้วยโค้ดได้ เพิ่มการตั้งค่าตัวประมวลผลปลั๊กอินด้านล่าง',
|
||||
docs: 'พัฒนาตัวประมวลผลปลั๊กอินของคุณเอง',
|
||||
runnerDocs: 'พัฒนารันเนอร์ Agent ของคุณเอง',
|
||||
pause: 'หยุดชั่วคราว',
|
||||
play: 'เล่น',
|
||||
},
|
||||
eventRouting: 'การกำหนดเส้นทางเหตุการณ์',
|
||||
eventRoutingDescription:
|
||||
'เลือกตัวประมวลผลสำหรับแต่ละเหตุการณ์ของ Bot และแก้ไขตรรกะในการตั้งค่า Agent หรือ Pipeline ที่เกี่ยวข้อง โดย Pipeline รองรับเฉพาะเหตุการณ์ข้อความ',
|
||||
@@ -857,6 +892,8 @@ const thTH = {
|
||||
routeFallbackCatchAll: '{{route}} เป็นเส้นทางรับทุกเหตุการณ์',
|
||||
routeFallbackIgnored:
|
||||
'เหตุการณ์ที่ไม่ตรงกับเส้นทางจะถูกละเว้น เพิ่มเส้นทางรับทั้งหมดเฉพาะเมื่อต้องกำหนดผลลัพธ์ให้ทุกเหตุการณ์',
|
||||
matchRecords: 'บันทึกการจับคู่',
|
||||
matchRecordsDescription: 'แสดงผลล่าสุดของแต่ละกฎและเหตุการณ์ล่าสุดที่ไม่ตรงกัน บันทึกในหน่วยความจำจะถูกล้างเมื่อรีสตาร์ท',
|
||||
testRoute: 'ตรวจเส้นทาง',
|
||||
adapterEventDebugAction: 'ทดสอบการรับเหตุการณ์',
|
||||
adapterEventDebugTitle: 'ดีบักเหตุการณ์แพลตฟอร์ม',
|
||||
@@ -1108,7 +1145,16 @@ const thTH = {
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
apiToolsSelectAll: 'เลือกทั้งหมด',
|
||||
monitoring: {
|
||||
rangeDays: '{{days}} วันที่ผ่านมา',
|
||||
customRange: 'กำหนดช่วงเวลาเอง',
|
||||
rangeStart: 'เวลาเริ่มต้น',
|
||||
rangeEnd: 'เวลาสิ้นสุด',
|
||||
rangeHint: 'เวลาเริ่มต้องก่อนเวลาสิ้นสุด สูงสุด 365 วัน',
|
||||
autoRefreshLabel: 'รีเฟรชอัตโนมัติ',
|
||||
autoRefreshOff: 'รีเฟรชอัตโนมัติ: ปิด',
|
||||
autoRefreshEvery: 'รีเฟรชทุก {{seconds}} วินาที',
|
||||
runStatus: "สถานะการทำงาน",
|
||||
toolCalls: "การเรียกเครื่องมือ",
|
||||
delivery: "การส่งข้อความ",
|
||||
@@ -1220,6 +1266,9 @@ const thTH = {
|
||||
'ทำงานตามขั้นตอนที่กำหนด: รับข้อความ เรียก AI และตอบกลับผู้ใช้ พร้อมตั้งค่าฐานความรู้และปลั๊กอินได้ รองรับเฉพาะเหตุการณ์ข้อความ เหมาะกับงานที่มีขั้นตอนชัดเจนและต้องการควบคุมกระบวนการประมวลผล',
|
||||
allEvents: 'รองรับทุกเหตุการณ์',
|
||||
messageEventsOnly: 'เฉพาะเหตุการณ์ข้อความ',
|
||||
selectAgentRunner: 'เลือกรันเนอร์ Agent',
|
||||
selectProcessorPlugin: 'เลือกปลั๊กอินประมวลผล',
|
||||
createBasicInfoDescription: 'ตั้งชื่อ ไอคอน และเลือกปลั๊กอิน',
|
||||
chooseType: 'เลือกวิธีทำงาน',
|
||||
chooseTypeDescription:
|
||||
'เลือกประเภทตัวประมวลผลก่อน แล้วตั้งค่าเพิ่มเติมหลังสร้าง',
|
||||
@@ -2023,6 +2072,14 @@ const thTH = {
|
||||
uploading: 'กำลังอัปโหลด...',
|
||||
},
|
||||
monitoring: {
|
||||
rangeDays: '{{days}} วันที่ผ่านมา',
|
||||
customRange: 'กำหนดช่วงเวลาเอง',
|
||||
rangeStart: 'เวลาเริ่มต้น',
|
||||
rangeEnd: 'เวลาสิ้นสุด',
|
||||
rangeHint: 'เวลาเริ่มต้องก่อนเวลาสิ้นสุด สูงสุด 365 วัน',
|
||||
autoRefreshLabel: 'รีเฟรชอัตโนมัติ',
|
||||
autoRefreshOff: 'รีเฟรชอัตโนมัติ: ปิด',
|
||||
autoRefreshEvery: 'รีเฟรชทุก {{seconds}} วินาที',
|
||||
title: 'บันทึกการทำงาน',
|
||||
workbench: 'กำหนดค่าและดีบัก',
|
||||
description:
|
||||
@@ -2440,6 +2497,14 @@ const thTH = {
|
||||
},
|
||||
},
|
||||
monitoring: {
|
||||
rangeDays: '{{days}} วันที่ผ่านมา',
|
||||
customRange: 'กำหนดช่วงเวลาเอง',
|
||||
rangeStart: 'เวลาเริ่มต้น',
|
||||
rangeEnd: 'เวลาสิ้นสุด',
|
||||
rangeHint: 'เวลาเริ่มต้องก่อนเวลาสิ้นสุด สูงสุด 365 วัน',
|
||||
autoRefreshLabel: 'รีเฟรชอัตโนมัติ',
|
||||
autoRefreshOff: 'รีเฟรชอัตโนมัติ: ปิด',
|
||||
autoRefreshEvery: 'รีเฟรชทุก {{seconds}} วินาที',
|
||||
unified: {
|
||||
inputs: 'เหตุการณ์และข้อมูลเข้า',
|
||||
outputs: 'ผลลัพธ์ที่สร้าง',
|
||||
@@ -2593,6 +2658,15 @@ const thTH = {
|
||||
sessions: 'การวิเคราะห์เซสชัน',
|
||||
errors: 'บันทึกข้อผิดพลาด',
|
||||
},
|
||||
pipelineTrace: {
|
||||
received: 'ข้อความที่ได้รับ',
|
||||
processing: 'ขั้นตอนการประมวลผล',
|
||||
replies: 'ข้อความตอบกลับ',
|
||||
noSteps: 'ไม่มีบันทึกการเรียกโมเดลหรือเครื่องมือ',
|
||||
success: 'สำเร็จ',
|
||||
error: 'ล้มเหลว',
|
||||
pending: 'รอดำเนินการ',
|
||||
},
|
||||
messageList: {
|
||||
timestamp: 'เวลา',
|
||||
bot: 'Bot',
|
||||
|
||||
@@ -77,6 +77,14 @@ const viVN = {
|
||||
sidebarGuide: {
|
||||
steps: {
|
||||
monitoring: {
|
||||
rangeDays: '{{days}} ngày qua',
|
||||
customRange: 'Khoảng thời gian tùy chỉnh',
|
||||
rangeStart: 'Bắt đầu',
|
||||
rangeEnd: 'Kết thúc',
|
||||
rangeHint: 'Bắt đầu phải trước kết thúc. Tối đa 365 ngày.',
|
||||
autoRefreshLabel: 'Tự động làm mới',
|
||||
autoRefreshOff: 'Tự động làm mới: tắt',
|
||||
autoRefreshEvery: 'Làm mới mỗi {{seconds}} giây',
|
||||
title: 'Xem hoạt động',
|
||||
description:
|
||||
'Xem hoạt động bot, mức sử dụng mô hình, lượng tin nhắn và hiệu suất hệ thống.',
|
||||
@@ -157,6 +165,14 @@ const viVN = {
|
||||
'Gửi tin nhắn thử để kiểm tra điều kiện kích hoạt, câu trả lời AI và xử lý đầu ra. Thay đổi cấu hình được lưu trước khi thử.',
|
||||
},
|
||||
monitoring: {
|
||||
rangeDays: '{{days}} ngày qua',
|
||||
customRange: 'Khoảng thời gian tùy chỉnh',
|
||||
rangeStart: 'Bắt đầu',
|
||||
rangeEnd: 'Kết thúc',
|
||||
rangeHint: 'Bắt đầu phải trước kết thúc. Tối đa 365 ngày.',
|
||||
autoRefreshLabel: 'Tự động làm mới',
|
||||
autoRefreshOff: 'Tự động làm mới: tắt',
|
||||
autoRefreshEvery: 'Làm mới mỗi {{seconds}} giây',
|
||||
title: 'Xem lịch sử chạy',
|
||||
description:
|
||||
'Dùng thẻ cạnh tiêu đề để xem cuộc trò chuyện, các bước xử lý và lỗi khi không có phản hồi hoặc phản hồi không đúng dự kiến.',
|
||||
@@ -788,7 +804,7 @@ const viVN = {
|
||||
legacyAdapters: 'Adapter cũ',
|
||||
legacyAdapterBadge: 'Cũ',
|
||||
legacyAdaptersHint:
|
||||
'Các adapter này đã có phiên bản hướng sự kiện mới hơn.\nChúng chỉ được giữ lại để tương thích với cấu hình hiện có và không được khuyến nghị cho Bot mới.',
|
||||
'Adapter cũ chủ yếu gửi và nhận tin nhắn, được giữ để tương thích với cấu hình bot hiện có.\nAdapter mới kết nối cùng nền tảng qua sự kiện, gồm tin nhắn và các yêu cầu kết bạn hoặc thay đổi nhóm được hỗ trợ, rồi chuyển đến bộ xử lý.\nƯu tiên bản mới khi tạo bot. Bot hiện có vẫn dùng được bản cũ. Khi chuyển đổi, kiểm tra kết nối và tuyến sự kiện; tài khoản nền tảng không được nâng cấp.',
|
||||
adapterConfig: 'Cấu hình Adapter',
|
||||
viewAdapterDocs: 'Xem tài liệu',
|
||||
bindPipeline: 'Liên kết Pipeline',
|
||||
@@ -809,6 +825,25 @@ const viVN = {
|
||||
routingConnection: 'Định tuyến & Kết nối',
|
||||
routingConnectionDescription:
|
||||
'Liên kết Pipeline xử lý tin nhắn cho Bot này',
|
||||
routingHelp: {
|
||||
title: 'Định tuyến sự kiện là gì?',
|
||||
intro: 'Bot nhận tin nhắn, lời mời kết bạn và các sự kiện như tham gia nhóm.',
|
||||
events: 'Sự kiện đến',
|
||||
agents: 'Các Agent khác nhau',
|
||||
message: 'Nhận tin nhắn',
|
||||
friend: 'Lời mời kết bạn',
|
||||
group: 'Tham gia nhóm',
|
||||
messageAction: 'Agent trả lời',
|
||||
friendAction: 'Agent xét duyệt',
|
||||
groupAction: 'Agent chào mừng',
|
||||
rules: 'Theo quy tắc của bạn · Ví dụ',
|
||||
instructions: 'Agent làm theo chỉ dẫn của bạn và gọi công cụ để trả lời, xét duyệt yêu cầu và thực hiện thao tác khác.',
|
||||
plugin: 'Với nhu cầu nâng cao, bộ xử lý plugin đăng ký hàng loạt sự kiện và xử lý bằng mã. Thêm cấu hình bộ xử lý plugin bên dưới.',
|
||||
docs: 'Phát triển bộ xử lý plugin của riêng bạn',
|
||||
runnerDocs: 'Phát triển trình chạy Agent của riêng bạn',
|
||||
pause: 'Tạm dừng',
|
||||
play: 'Phát',
|
||||
},
|
||||
eventRouting: 'Định tuyến sự kiện',
|
||||
eventRoutingDescription:
|
||||
'Chọn bộ xử lý cho từng sự kiện của Bot. Chỉnh sửa logic trong cấu hình Agent hoặc Pipeline tương ứng. Pipeline chỉ hỗ trợ sự kiện tin nhắn.',
|
||||
@@ -872,6 +907,8 @@ const viVN = {
|
||||
routeFallbackCatchAll: '{{route}} là tuyến nhận mọi sự kiện.',
|
||||
routeFallbackIgnored:
|
||||
'Sự kiện không khớp tuyến nào sẽ bị bỏ qua. Chỉ thêm tuyến nhận tất cả khi mọi sự kiện cần kết quả rõ ràng.',
|
||||
matchRecords: 'Lịch sử khớp',
|
||||
matchRecordsDescription: 'Kết quả gần nhất của mỗi quy tắc và các sự kiện không khớp gần đây. Nhật ký trong bộ nhớ được xóa khi khởi động lại.',
|
||||
testRoute: 'Kiểm tra tuyến',
|
||||
adapterEventDebugAction: 'Kiểm tra lắng nghe',
|
||||
adapterEventDebugTitle: 'Gỡ lỗi sự kiện nền tảng',
|
||||
@@ -1126,7 +1163,16 @@ const viVN = {
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
apiToolsSelectAll: 'Chọn tất cả',
|
||||
monitoring: {
|
||||
rangeDays: '{{days}} ngày qua',
|
||||
customRange: 'Khoảng thời gian tùy chỉnh',
|
||||
rangeStart: 'Bắt đầu',
|
||||
rangeEnd: 'Kết thúc',
|
||||
rangeHint: 'Bắt đầu phải trước kết thúc. Tối đa 365 ngày.',
|
||||
autoRefreshLabel: 'Tự động làm mới',
|
||||
autoRefreshOff: 'Tự động làm mới: tắt',
|
||||
autoRefreshEvery: 'Làm mới mỗi {{seconds}} giây',
|
||||
runStatus: "Trạng thái thực thi",
|
||||
toolCalls: "Lượt gọi công cụ",
|
||||
delivery: "Gửi tin nhắn",
|
||||
@@ -1239,6 +1285,9 @@ const viVN = {
|
||||
'Chạy theo quy trình cố định: nhận tin nhắn, gọi AI và trả lời người dùng, với cơ sở tri thức và plugin có thể cấu hình. Chỉ xử lý sự kiện tin nhắn, phù hợp với tác vụ có các bước rõ ràng và cần kiểm soát quá trình xử lý.',
|
||||
allEvents: 'Hỗ trợ tất cả sự kiện',
|
||||
messageEventsOnly: 'Chỉ sự kiện tin nhắn',
|
||||
selectAgentRunner: 'Chọn trình chạy Agent',
|
||||
selectProcessorPlugin: 'Chọn plugin xử lý',
|
||||
createBasicInfoDescription: 'Đặt tên, biểu tượng và chọn plugin',
|
||||
chooseType: 'Chọn cách hoạt động',
|
||||
chooseTypeDescription:
|
||||
'Chọn loại bộ xử lý trước. Bạn có thể cấu hình thêm sau khi tạo.',
|
||||
@@ -2053,6 +2102,14 @@ const viVN = {
|
||||
uploading: 'Đang tải lên...',
|
||||
},
|
||||
monitoring: {
|
||||
rangeDays: '{{days}} ngày qua',
|
||||
customRange: 'Khoảng thời gian tùy chỉnh',
|
||||
rangeStart: 'Bắt đầu',
|
||||
rangeEnd: 'Kết thúc',
|
||||
rangeHint: 'Bắt đầu phải trước kết thúc. Tối đa 365 ngày.',
|
||||
autoRefreshLabel: 'Tự động làm mới',
|
||||
autoRefreshOff: 'Tự động làm mới: tắt',
|
||||
autoRefreshEvery: 'Làm mới mỗi {{seconds}} giây',
|
||||
title: 'Nhật ký chạy',
|
||||
workbench: 'Cấu hình và gỡ lỗi',
|
||||
description: 'Xem nhật ký thực thi và lỗi của Pipeline này (24 giờ qua)',
|
||||
@@ -2476,6 +2533,14 @@ const viVN = {
|
||||
},
|
||||
},
|
||||
monitoring: {
|
||||
rangeDays: '{{days}} ngày qua',
|
||||
customRange: 'Khoảng thời gian tùy chỉnh',
|
||||
rangeStart: 'Bắt đầu',
|
||||
rangeEnd: 'Kết thúc',
|
||||
rangeHint: 'Bắt đầu phải trước kết thúc. Tối đa 365 ngày.',
|
||||
autoRefreshLabel: 'Tự động làm mới',
|
||||
autoRefreshOff: 'Tự động làm mới: tắt',
|
||||
autoRefreshEvery: 'Làm mới mỗi {{seconds}} giây',
|
||||
unified: {
|
||||
inputs: 'Sự kiện và đầu vào',
|
||||
outputs: 'Kết quả tạo ra',
|
||||
@@ -2629,6 +2694,15 @@ const viVN = {
|
||||
sessions: 'Phân tích phiên',
|
||||
errors: 'Nhật ký lỗi',
|
||||
},
|
||||
pipelineTrace: {
|
||||
received: 'Tin nhắn nhận được',
|
||||
processing: 'Quá trình xử lý',
|
||||
replies: 'Phản hồi',
|
||||
noSteps: 'Không có bản ghi gọi mô hình hoặc công cụ',
|
||||
success: 'Thành công',
|
||||
error: 'Thất bại',
|
||||
pending: 'Đang chờ',
|
||||
},
|
||||
messageList: {
|
||||
timestamp: 'Thời gian',
|
||||
bot: 'Bot',
|
||||
|
||||
@@ -32,17 +32,17 @@ const zhHans = {
|
||||
sendUnconfirmed: '发送状态未确认,请检查连接后重新打开会话。',
|
||||
draftPlaceholder: '可以先写下一条,回复完成后发送…',
|
||||
operations: {
|
||||
create_pipeline: '创建 Pipeline',
|
||||
configure_pipeline: '配置 Pipeline',
|
||||
create_pipeline: '创建 流水线',
|
||||
configure_pipeline: '配置 流水线',
|
||||
create_knowledge_base: '创建知识库',
|
||||
get_pipeline: '查看 Pipeline',
|
||||
get_pipeline: '查看 流水线',
|
||||
get_knowledge_schema: '查看知识引擎配置',
|
||||
list_operation_logs: '查询操作日志',
|
||||
},
|
||||
resources: {
|
||||
models: '查询聊天模型',
|
||||
embedding_models: '查询嵌入模型',
|
||||
pipelines: '查询 Pipeline',
|
||||
pipelines: '查询 流水线',
|
||||
knowledge_bases: '查询知识库',
|
||||
knowledge_engines: '查询知识引擎',
|
||||
},
|
||||
@@ -51,8 +51,8 @@ const zhHans = {
|
||||
newChat: '新对话',
|
||||
close: '关闭',
|
||||
welcome:
|
||||
'告诉我你想构建什么应用。我可以查看当前工作区,为机器人、Agent、知识库和 Pipeline 等提供配置方案,由你确认后执行。',
|
||||
discover: '当前有哪些可用的模型、知识库和 Pipeline?',
|
||||
'告诉我你想构建什么应用。我可以查看当前工作区,为机器人、Agent、知识库和 流水线 等提供配置方案,由你确认后执行。',
|
||||
discover: '当前有哪些可用的模型、知识库和 流水线?',
|
||||
build: '帮我创建一个知识库问答应用。',
|
||||
toolResult: '操作结果',
|
||||
review: '请检查具体操作及参数,确认后执行',
|
||||
@@ -71,6 +71,14 @@ const zhHans = {
|
||||
sidebarGuide: {
|
||||
steps: {
|
||||
monitoring: {
|
||||
rangeDays: '最近 {{days}} 天',
|
||||
customRange: '自定义时间范围',
|
||||
rangeStart: '开始时间',
|
||||
rangeEnd: '结束时间',
|
||||
rangeHint: '开始时间需早于结束时间,最多 365 天。',
|
||||
autoRefreshLabel: '定时刷新',
|
||||
autoRefreshOff: '定时刷新:关闭',
|
||||
autoRefreshEvery: '每 {{seconds}} 秒刷新',
|
||||
title: '查看运行概况',
|
||||
description: '集中查看机器人活动、模型调用、消息量和系统运行情况。',
|
||||
},
|
||||
@@ -146,6 +154,14 @@ const zhHans = {
|
||||
'发送测试消息,检查触发规则、AI 回复和输出处理是否符合预期;修改配置后,测试前会先保存。',
|
||||
},
|
||||
monitoring: {
|
||||
rangeDays: '最近 {{days}} 天',
|
||||
customRange: '自定义时间范围',
|
||||
rangeStart: '开始时间',
|
||||
rangeEnd: '结束时间',
|
||||
rangeHint: '开始时间需早于结束时间,最多 365 天。',
|
||||
autoRefreshLabel: '定时刷新',
|
||||
autoRefreshOff: '定时刷新:关闭',
|
||||
autoRefreshEvery: '每 {{seconds}} 秒刷新',
|
||||
title: '查看运行日志',
|
||||
description:
|
||||
'打开标题旁的“运行日志”,查看历史对话、处理过程和错误,排查回复异常。',
|
||||
@@ -740,7 +756,7 @@ const zhHans = {
|
||||
legacyAdapters: '旧版适配器',
|
||||
legacyAdapterBadge: '过时',
|
||||
legacyAdaptersHint:
|
||||
'这些适配器已有对应的事件驱动接入版本,\n仅为兼容存量配置保留,不建议新建机器人时使用。',
|
||||
'旧版主要用于消息收发,保留用于兼容已有机器人配置。\n新版是同一平台的事件驱动接入,除消息外,还可处理该平台支持的好友请求、群成员变化等事件,并通过事件路由交给处理器。\n新建机器人优先选择新版。已有机器人可继续使用旧版;切换时请按新版接入要求重新检查连接参数和事件路由,并非直接升级平台账号。',
|
||||
adapterConfig: '适配器配置',
|
||||
viewAdapterDocs: '查看文档',
|
||||
bindPipeline: '绑定流水线',
|
||||
@@ -760,6 +776,25 @@ const zhHans = {
|
||||
basicInfoDescription: '设置机器人名称和描述',
|
||||
routingConnection: '路由与连接',
|
||||
routingConnectionDescription: '绑定处理此机器人消息的流水线',
|
||||
routingHelp: {
|
||||
title: '什么是事件路由?',
|
||||
intro: '机器人不只会收到消息,也会收到好友请求、加入群聊等事件。',
|
||||
events: '机器人收到事件',
|
||||
agents: '交给不同 Agent',
|
||||
message: '收到消息',
|
||||
friend: '好友请求',
|
||||
group: '加入群聊',
|
||||
messageAction: '问答 Agent → 回复',
|
||||
friendAction: '审核 Agent → 处理申请',
|
||||
groupAction: '欢迎 Agent → 打招呼',
|
||||
rules: '按您设置的规则分配 · 示例',
|
||||
instructions: 'Agent 按您的指令调用工具,完成回复、审核等操作。',
|
||||
plugin: '专业场景可用插件处理器:批量注册事件,用代码处理,再在下方添加插件处理器配置。',
|
||||
docs: '开发自己的插件处理器',
|
||||
runnerDocs: '开发自己的 Agent 运行器',
|
||||
pause: '暂停',
|
||||
play: '播放',
|
||||
},
|
||||
eventRouting: '事件路由',
|
||||
eventRoutingDescription: '设置收到事件后交给哪个处理器。',
|
||||
eventBindings: '事件路由',
|
||||
@@ -784,7 +819,7 @@ const zhHans = {
|
||||
targetType: '目标类型',
|
||||
target: '处理器',
|
||||
targetAgent: 'Agent',
|
||||
targetPipeline: 'Pipeline',
|
||||
targetPipeline: '流水线',
|
||||
targetDiscard: '丢弃',
|
||||
selectTarget: '选择处理器',
|
||||
searchTarget: '搜索处理器…',
|
||||
@@ -793,7 +828,7 @@ const zhHans = {
|
||||
enabled: '启用',
|
||||
eventBindingDescriptionPlaceholder: '规则说明',
|
||||
noEventBindings: '暂无事件路由',
|
||||
unsupportedPipelineEvent: 'Pipeline 仅可用于 message.* 事件',
|
||||
unsupportedPipelineEvent: '流水线 仅可用于 message.* 事件',
|
||||
disable: '禁用',
|
||||
enable: '启用',
|
||||
disabledBindings: '已禁用',
|
||||
@@ -817,6 +852,8 @@ const zhHans = {
|
||||
routeFallbackCatchAll: '{{route}} 是兜底路由。',
|
||||
routeFallbackIgnored:
|
||||
'未命中任何路由的事件会被忽略。只有需要为每个事件指定结果时,才添加全局兜底路由。',
|
||||
matchRecords: '匹配记录',
|
||||
matchRecordsDescription: '查看各规则最近一次匹配结果及近期未匹配事件。记录来自机器人内存日志,重启后清空。',
|
||||
testRoute: '检查路由',
|
||||
adapterEventDebugAction: '测试监听',
|
||||
adapterEventDebugTitle: '平台事件调试',
|
||||
@@ -1062,7 +1099,16 @@ const zhHans = {
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
apiToolsSelectAll: '全选',
|
||||
monitoring: {
|
||||
rangeDays: '最近 {{days}} 天',
|
||||
customRange: '自定义时间范围',
|
||||
rangeStart: '开始时间',
|
||||
rangeEnd: '结束时间',
|
||||
rangeHint: '开始时间需早于结束时间,最多 365 天。',
|
||||
autoRefreshLabel: '定时刷新',
|
||||
autoRefreshOff: '定时刷新:关闭',
|
||||
autoRefreshEvery: '每 {{seconds}} 秒刷新',
|
||||
runStatus: "执行状态",
|
||||
toolCalls: "工具调用",
|
||||
delivery: "消息发送",
|
||||
@@ -1172,15 +1218,18 @@ const zhHans = {
|
||||
'只处理消息事件,由 AI 直接生成回复,并提供知识库、插件等实用功能。',
|
||||
allEvents: '支持全部事件',
|
||||
messageEventsOnly: '仅支持消息事件',
|
||||
selectAgentRunner: '选择 Agent 运行器',
|
||||
selectProcessorPlugin: '选择处理器插件',
|
||||
createBasicInfoDescription: '设置名称、图标并选择插件',
|
||||
chooseType: '选择处理方式',
|
||||
chooseTypeDescription: '先选择处理器的工作方式,创建后可继续完善配置。',
|
||||
diagramHint: '工作方式示意',
|
||||
agentDiagramTitle: 'Agent:自主判断并行动',
|
||||
agentDiagramDescription:
|
||||
'Agent 可以接收多种平台事件,结合模型和工具理解当前情况,并自主决定下一步动作。',
|
||||
pipelineDiagramTitle: 'Pipeline:按流程逐步处理',
|
||||
pipelineDiagramTitle: '流水线:按流程逐步处理',
|
||||
pipelineDiagramDescription:
|
||||
'Pipeline 只处理消息事件,并严格按照你编排的固定步骤依次执行。',
|
||||
'流水线 只处理消息事件,并严格按照你编排的固定步骤依次执行。',
|
||||
pipelineDiagramFlow: '消息沿固定流程依次处理',
|
||||
diagramEvents: '平台事件',
|
||||
diagramAgentCanUse: 'Agent 可使用',
|
||||
@@ -1950,6 +1999,14 @@ const zhHans = {
|
||||
uploading: '上传中...',
|
||||
},
|
||||
monitoring: {
|
||||
rangeDays: '最近 {{days}} 天',
|
||||
customRange: '自定义时间范围',
|
||||
rangeStart: '开始时间',
|
||||
rangeEnd: '结束时间',
|
||||
rangeHint: '开始时间需早于结束时间,最多 365 天。',
|
||||
autoRefreshLabel: '定时刷新',
|
||||
autoRefreshOff: '定时刷新:关闭',
|
||||
autoRefreshEvery: '每 {{seconds}} 秒刷新',
|
||||
title: '运行日志',
|
||||
workbench: '配置与调试',
|
||||
description: '查看此流水线的运行记录和错误信息(最近24小时)',
|
||||
@@ -2339,6 +2396,14 @@ const zhHans = {
|
||||
},
|
||||
},
|
||||
monitoring: {
|
||||
rangeDays: '最近 {{days}} 天',
|
||||
customRange: '自定义时间范围',
|
||||
rangeStart: '开始时间',
|
||||
rangeEnd: '结束时间',
|
||||
rangeHint: '开始时间需早于结束时间,最多 365 天。',
|
||||
autoRefreshLabel: '定时刷新',
|
||||
autoRefreshOff: '定时刷新:关闭',
|
||||
autoRefreshEvery: '每 {{seconds}} 秒刷新',
|
||||
unified: {
|
||||
inputs: '事件与输入',
|
||||
outputs: '生成结果',
|
||||
@@ -2364,13 +2429,13 @@ const zhHans = {
|
||||
},
|
||||
execution: {
|
||||
title: '执行记录',
|
||||
subtitle: '每条入站事件及其触发的执行,覆盖 Agent 与 Pipeline。',
|
||||
subtitle: '每条入站事件及其触发的执行,覆盖 Agent 与 流水线。',
|
||||
tabs: { executions: '执行记录' },
|
||||
filters: {
|
||||
source: '类型',
|
||||
sourceAll: '全部类型',
|
||||
sourceAgent: 'Agent',
|
||||
sourcePipeline: 'Pipeline',
|
||||
sourcePipeline: '流水线',
|
||||
mode: '流量',
|
||||
modeAll: '真实 + 调试',
|
||||
modeLive: '仅真实',
|
||||
@@ -2389,7 +2454,7 @@ const zhHans = {
|
||||
// prettier-ignore
|
||||
kind: {
|
||||
agent: 'Agent',
|
||||
pipeline: 'Pipeline',
|
||||
pipeline: '流水线',
|
||||
processor: '处理器',
|
||||
},
|
||||
columns: {
|
||||
@@ -2409,7 +2474,7 @@ const zhHans = {
|
||||
tokensHint:
|
||||
'仅汇总已记录的用量;部分模型未返回用量时,总数可能不完整。',
|
||||
totalExecutions: '执行总数',
|
||||
bySource: 'Agent {{agent}} · Pipeline {{pipeline}}',
|
||||
bySource: 'Agent {{agent}} · 流水线 {{pipeline}}',
|
||||
successRate: '执行成功率',
|
||||
successRateHint:
|
||||
'成功次数 ÷(成功 + 失败)。不计进行中、已忽略和已取消的执行。',
|
||||
@@ -2490,6 +2555,15 @@ const zhHans = {
|
||||
sessions: '会话分析',
|
||||
errors: '错误日志',
|
||||
},
|
||||
pipelineTrace: {
|
||||
received: '收到消息',
|
||||
processing: '处理过程',
|
||||
replies: '回复消息',
|
||||
noSteps: '未记录模型或工具调用',
|
||||
success: '成功',
|
||||
error: '失败',
|
||||
pending: '处理中',
|
||||
},
|
||||
messageList: {
|
||||
timestamp: '时间戳',
|
||||
bot: '机器人',
|
||||
@@ -2785,7 +2859,7 @@ const zhHans = {
|
||||
adapter: '适配器',
|
||||
model_provider: '模型提供商',
|
||||
llm_model: '模型',
|
||||
pipeline: '管道',
|
||||
pipeline: '流水线',
|
||||
user: '用户',
|
||||
workspace: '工作区',
|
||||
monitoring: '监控',
|
||||
@@ -2843,7 +2917,7 @@ const zhHans = {
|
||||
codex_view: '查看 Codex 授权',
|
||||
codex_authorize: '授权 Codex',
|
||||
embed: '嵌入式会话',
|
||||
pipeline_extensions_update: '修改管道扩展绑定',
|
||||
pipeline_extensions_update: '修改流水线扩展绑定',
|
||||
export: '导出数据',
|
||||
execute: '执行任务',
|
||||
debug: '调试任务',
|
||||
|
||||
+119
-45
@@ -33,17 +33,17 @@ const zhHant = {
|
||||
sendUnconfirmed: '傳送狀態未確認,請檢查連線後重新開啟對話。',
|
||||
draftPlaceholder: '可以先寫下一條,回覆完成後傳送…',
|
||||
operations: {
|
||||
create_pipeline: '建立 Pipeline',
|
||||
configure_pipeline: '設定 Pipeline',
|
||||
create_pipeline: '建立 流水線',
|
||||
configure_pipeline: '設定 流水線',
|
||||
create_knowledge_base: '建立知識庫',
|
||||
get_pipeline: '查看 Pipeline',
|
||||
get_pipeline: '查看 流水線',
|
||||
get_knowledge_schema: '查看知識引擎設定',
|
||||
list_operation_logs: '查詢操作日誌',
|
||||
},
|
||||
resources: {
|
||||
models: '查詢聊天模型',
|
||||
embedding_models: '查詢嵌入模型',
|
||||
pipelines: '查詢 Pipeline',
|
||||
pipelines: '查詢 流水線',
|
||||
knowledge_bases: '查詢知識庫',
|
||||
knowledge_engines: '查詢知識引擎',
|
||||
},
|
||||
@@ -52,8 +52,8 @@ const zhHant = {
|
||||
newChat: '新對話',
|
||||
close: '關閉',
|
||||
welcome:
|
||||
'告訴我你想建構什麼應用。我可以查看目前工作區,為機器人、Agent、知識庫和 Pipeline 等提供設定方案,由你確認後執行。',
|
||||
discover: '目前有哪些可用的模型、知識庫和 Pipeline?',
|
||||
'告訴我你想建構什麼應用。我可以查看目前工作區,為機器人、Agent、知識庫和 流水線 等提供設定方案,由你確認後執行。',
|
||||
discover: '目前有哪些可用的模型、知識庫和 流水線?',
|
||||
build: '幫我建立一個知識庫問答應用。',
|
||||
toolResult: '操作結果',
|
||||
review: '請檢查具體操作及參數,確認後執行',
|
||||
@@ -72,6 +72,14 @@ const zhHant = {
|
||||
sidebarGuide: {
|
||||
steps: {
|
||||
monitoring: {
|
||||
rangeDays: '最近 {{days}} 天',
|
||||
customRange: '自訂時間範圍',
|
||||
rangeStart: '開始時間',
|
||||
rangeEnd: '結束時間',
|
||||
rangeHint: '開始時間須早於結束時間,最多 365 天。',
|
||||
autoRefreshLabel: '定時重新整理',
|
||||
autoRefreshOff: '定時重新整理:關閉',
|
||||
autoRefreshEvery: '每 {{seconds}} 秒重新整理',
|
||||
title: '查看執行概況',
|
||||
description: '集中查看機器人活動、模型呼叫、訊息量和系統運行情況。',
|
||||
},
|
||||
@@ -147,6 +155,14 @@ const zhHant = {
|
||||
'傳送測試訊息,檢查觸發規則、AI 回覆和輸出處理是否符合預期;修改設定後,測試前會先儲存。',
|
||||
},
|
||||
monitoring: {
|
||||
rangeDays: '最近 {{days}} 天',
|
||||
customRange: '自訂時間範圍',
|
||||
rangeStart: '開始時間',
|
||||
rangeEnd: '結束時間',
|
||||
rangeHint: '開始時間須早於結束時間,最多 365 天。',
|
||||
autoRefreshLabel: '定時重新整理',
|
||||
autoRefreshOff: '定時重新整理:關閉',
|
||||
autoRefreshEvery: '每 {{seconds}} 秒重新整理',
|
||||
title: '查看執行日誌',
|
||||
description:
|
||||
'開啟標題旁的「執行日誌」,查看歷史對話、處理過程和錯誤,排查回覆異常。',
|
||||
@@ -518,7 +534,7 @@ const zhHant = {
|
||||
confirmDisconnect: '確認中斷',
|
||||
},
|
||||
title: '模型設定',
|
||||
description: '設定和管理可在流程線中使用的模型',
|
||||
description: '設定和管理可在流水線中使用的模型',
|
||||
createModel: '建立模型',
|
||||
editModel: '編輯模型',
|
||||
getModelListError: '取得模型清單失敗:',
|
||||
@@ -738,11 +754,11 @@ const zhHant = {
|
||||
legacyAdapters: '舊版適配器',
|
||||
legacyAdapterBadge: '過時',
|
||||
legacyAdaptersHint:
|
||||
'這些適配器已有對應的事件驅動接入版本,\n僅為相容既有設定保留,不建議新建機器人時使用。',
|
||||
'舊版主要用於訊息收發,保留以相容現有機器人設定。\n新版是同一平台的事件驅動接入,除訊息外,也可處理平台支援的好友請求、群成員變化等事件,透過事件路由交給處理器。\n新建機器人優先選新版。現有機器人可繼續使用舊版;切換時請依新版要求檢查連線參數和事件路由,並非直接升級平台帳號。',
|
||||
adapterConfig: '適配器設定',
|
||||
viewAdapterDocs: '查看文檔',
|
||||
bindPipeline: '綁定流程線',
|
||||
selectPipeline: '選擇流程線',
|
||||
bindPipeline: '綁定流水線',
|
||||
selectPipeline: '選擇流水線',
|
||||
selectBot: '請選擇機器人',
|
||||
botLogTitle: '機器人日誌',
|
||||
enableAutoRefresh: '開啟自動重新整理',
|
||||
@@ -757,10 +773,29 @@ const zhHant = {
|
||||
basicInfo: '基礎資訊',
|
||||
basicInfoDescription: '設定機器人名稱和描述',
|
||||
routingConnection: '路由與連接',
|
||||
routingConnectionDescription: '綁定處理此機器人訊息的流程線',
|
||||
routingConnectionDescription: '綁定處理此機器人訊息的流水線',
|
||||
routingHelp: {
|
||||
title: '什麼是事件路由?',
|
||||
intro: '機器人不只會收到訊息,也會收到好友請求、加入群聊等事件。',
|
||||
events: '機器人收到事件',
|
||||
agents: '交給不同 Agent',
|
||||
message: '收到訊息',
|
||||
friend: '好友請求',
|
||||
group: '加入群聊',
|
||||
messageAction: '問答 Agent → 回覆',
|
||||
friendAction: '審核 Agent → 處理申請',
|
||||
groupAction: '歡迎 Agent → 打招呼',
|
||||
rules: '依您設定的規則分配 · 範例',
|
||||
instructions: 'Agent 依您的指令呼叫工具,完成回覆、審核等操作。',
|
||||
plugin: '專業場景可用插件處理器:批次註冊事件,用程式碼處理,再於下方新增插件處理器設定。',
|
||||
docs: '開發自己的插件處理器',
|
||||
runnerDocs: '開發自己的 Agent 運行器',
|
||||
pause: '暫停',
|
||||
play: '播放',
|
||||
},
|
||||
eventRouting: '事件路由',
|
||||
eventRoutingDescription:
|
||||
'選擇此機器人收到不同事件時交給哪個處理器。在對應的 Agent 或 Pipeline 設定中編輯處理邏輯;Pipeline 僅支援訊息事件。',
|
||||
'選擇此機器人收到不同事件時交給哪個處理器。在對應的 Agent 或 流水線 設定中編輯處理邏輯;流水線 僅支援訊息事件。',
|
||||
eventBindings: '事件路由',
|
||||
addEventBinding: '添加路由',
|
||||
addBehavior: '添加行爲',
|
||||
@@ -783,7 +818,7 @@ const zhHant = {
|
||||
targetType: '目標類型',
|
||||
target: '處理器',
|
||||
targetAgent: 'Agent',
|
||||
targetPipeline: 'Pipeline',
|
||||
targetPipeline: '流水線',
|
||||
targetDiscard: '丟棄',
|
||||
selectTarget: '選擇處理器',
|
||||
searchTarget: '搜索處理器…',
|
||||
@@ -792,7 +827,7 @@ const zhHant = {
|
||||
enabled: '啟用',
|
||||
eventBindingDescriptionPlaceholder: '規則說明',
|
||||
noEventBindings: '暫無事件路由',
|
||||
unsupportedPipelineEvent: 'Pipeline 僅可用於 message.* 事件',
|
||||
unsupportedPipelineEvent: '流水線 僅可用於 message.* 事件',
|
||||
disable: '禁用',
|
||||
enable: '啟用',
|
||||
disabledBindings: '已禁用',
|
||||
@@ -816,6 +851,8 @@ const zhHant = {
|
||||
routeFallbackCatchAll: '{{route}} 是兜底路由。',
|
||||
routeFallbackIgnored:
|
||||
'未命中任何路由的事件會被忽略。只有需要爲每個事件指定結果時,才添加全局兜底路由。',
|
||||
matchRecords: '匹配記錄',
|
||||
matchRecordsDescription: '查看各規則最近一次匹配結果及近期未匹配事件。記錄來自機器人記憶體日誌,重新啟動後清空。',
|
||||
testRoute: '檢查路由',
|
||||
adapterEventDebugAction: '測試監聽',
|
||||
adapterEventDebugTitle: '平臺事件調試',
|
||||
@@ -955,7 +992,7 @@ const zhHant = {
|
||||
operator_regex: '正則匹配',
|
||||
routingRules: '條件路由規則',
|
||||
routingRulesDescription:
|
||||
'按順序匹配,命中第一條規則後路由到對應流程線;都不匹配時使用上方預設流程線',
|
||||
'按順序匹配,命中第一條規則後路由到對應流水線;都不匹配時使用上方預設流水線',
|
||||
addRoutingRule: '新增規則',
|
||||
ruleTypeLauncherType: '會話類型',
|
||||
ruleTypeLauncherId: '會話 ID',
|
||||
@@ -1061,7 +1098,16 @@ const zhHant = {
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
apiToolsSelectAll: '全選',
|
||||
monitoring: {
|
||||
rangeDays: '最近 {{days}} 天',
|
||||
customRange: '自訂時間範圍',
|
||||
rangeStart: '開始時間',
|
||||
rangeEnd: '結束時間',
|
||||
rangeHint: '開始時間須早於結束時間,最多 365 天。',
|
||||
autoRefreshLabel: '定時重新整理',
|
||||
autoRefreshOff: '定時重新整理:關閉',
|
||||
autoRefreshEvery: '每 {{seconds}} 秒重新整理',
|
||||
runStatus: "執行狀態",
|
||||
toolCalls: "工具呼叫",
|
||||
delivery: "訊息傳送",
|
||||
@@ -1161,7 +1207,7 @@ const zhHant = {
|
||||
selectFromSidebar: '從側邊欄選擇一個處理器',
|
||||
agentType: 'Agent',
|
||||
agentTypeDescription: '透過執行器處理訊息、群成員、好友、回饋等平台事件。',
|
||||
pipelineType: '流程線',
|
||||
pipelineType: '流水線',
|
||||
kindBadgeAgent: 'Agent',
|
||||
kindBadgePipeline: '流水線',
|
||||
groupByKind: '依類型分組',
|
||||
@@ -1170,15 +1216,18 @@ const zhHant = {
|
||||
'依「接收訊息、呼叫 AI、回覆使用者」的固定流程執行,可設定知識庫與外掛擴充。僅處理訊息事件,適合步驟明確、需要控制處理過程的場景。',
|
||||
allEvents: '支援全部事件',
|
||||
messageEventsOnly: '僅支援訊息事件',
|
||||
selectAgentRunner: '選擇 Agent 運行器',
|
||||
selectProcessorPlugin: '選擇處理器插件',
|
||||
createBasicInfoDescription: '設定名稱、圖示並選擇插件',
|
||||
chooseType: '選擇處理方式',
|
||||
chooseTypeDescription: '先選擇處理器的工作方式,創建後可繼續完善配置。',
|
||||
diagramHint: '工作方式示意',
|
||||
agentDiagramTitle: 'Agent:自主判斷並行動',
|
||||
agentDiagramDescription:
|
||||
'Agent 可以接收多種平臺事件,結合模型和工具理解當前情況,並自主決定下一步動作。',
|
||||
pipelineDiagramTitle: 'Pipeline:按流程逐步處理',
|
||||
pipelineDiagramTitle: '流水線:按流程逐步處理',
|
||||
pipelineDiagramDescription:
|
||||
'Pipeline 只處理訊息事件,並嚴格按照你編排的固定步驟依次執行。',
|
||||
'流水線 只處理訊息事件,並嚴格按照你編排的固定步驟依次執行。',
|
||||
pipelineDiagramFlow: '訊息沿固定流程依次處理',
|
||||
diagramEvents: '平臺事件',
|
||||
diagramAgentCanUse: 'Agent 可使用',
|
||||
@@ -1354,7 +1403,7 @@ const zhHant = {
|
||||
},
|
||||
plugins: {
|
||||
title: '外掛擴展',
|
||||
description: '安裝和設定用於擴展功能的外掛,請在流程線配置中選用',
|
||||
description: '安裝和設定用於擴展功能的外掛,請在流水線配置中選用',
|
||||
createPlugin: '建立外掛',
|
||||
editPlugin: '編輯外掛',
|
||||
installed: '已安裝',
|
||||
@@ -1795,15 +1844,15 @@ const zhHant = {
|
||||
deleteMCPHint: '刪除後,此 MCP 伺服器設定將無法恢復。',
|
||||
},
|
||||
pipelines: {
|
||||
title: '流程線',
|
||||
description: '流程線定義了對訊息事件的處理流程,用於綁定到機器人',
|
||||
createPipeline: '建立流程線',
|
||||
selectFromSidebar: '從側邊欄選擇一個流程線',
|
||||
editPipeline: '編輯流程線',
|
||||
title: '流水線',
|
||||
description: '流水線定義了對訊息事件的處理流程,用於綁定到機器人',
|
||||
createPipeline: '建立流水線',
|
||||
selectFromSidebar: '從側邊欄選擇一個流水線',
|
||||
editPipeline: '編輯流水線',
|
||||
chat: '對話',
|
||||
configuration: '設定',
|
||||
debugChat: '對話除錯',
|
||||
getPipelineListError: '取得流程線清單失敗:',
|
||||
getPipelineListError: '取得流水線清單失敗:',
|
||||
daysAgo: '天前',
|
||||
today: '今天',
|
||||
updateTime: '更新於',
|
||||
@@ -1814,7 +1863,7 @@ const zhHant = {
|
||||
recentlyEdited: '最近編輯',
|
||||
earliestEdited: '最早編輯',
|
||||
basicInfo: '基本資訊',
|
||||
basicInfoDescription: '設定流程線名稱、圖示和描述',
|
||||
basicInfoDescription: '設定流水線名稱、圖示和描述',
|
||||
managementDescription: '複製或刪除此流水線。',
|
||||
aiCapabilities: 'AI 能力',
|
||||
triggerConditions: '觸發條件',
|
||||
@@ -1822,26 +1871,26 @@ const zhHant = {
|
||||
outputProcessing: '輸出處理',
|
||||
nameRequired: '名稱不能為空',
|
||||
descriptionRequired: '描述不能為空',
|
||||
createSuccess: '建立成功 請編輯流程線詳細參數',
|
||||
createSuccess: '建立成功 請編輯流水線詳細參數',
|
||||
createError: '建立失敗:',
|
||||
saveSuccess: '儲存成功',
|
||||
saveError: '儲存失敗:',
|
||||
copySuffix: ' Copy',
|
||||
deleteConfirmation:
|
||||
'您確定要刪除這個流程線嗎?已綁定此流程線的機器人將無法使用。',
|
||||
defaultPipelineCannotDelete: '預設流程線不可刪除',
|
||||
'您確定要刪除這個流水線嗎?已綁定此流水線的機器人將無法使用。',
|
||||
defaultPipelineCannotDelete: '預設流水線不可刪除',
|
||||
deleteSuccess: '刪除成功',
|
||||
deleteError: '刪除失敗:',
|
||||
copyConfirmTitle: '確認複製',
|
||||
copyConfirmation:
|
||||
'確定要複製這個流程線嗎?複製將創建一個包含完整配置的新流程線。',
|
||||
'確定要複製這個流水線嗎?複製將創建一個包含完整配置的新流水線。',
|
||||
unsavedChanges: '有未儲存的變更',
|
||||
dangerZone: '危險區域',
|
||||
dangerZoneDescription: '不可逆的操作',
|
||||
deletePipelineAction: '刪除此流程線',
|
||||
deletePipelineHint: '刪除後,綁定此流程線的機器人將無法正常運作。',
|
||||
copyPipelineAction: '複製此流程線',
|
||||
copyPipelineHint: '建立一條新的流程線,並複製所有設定。',
|
||||
deletePipelineAction: '刪除此流水線',
|
||||
deletePipelineHint: '刪除後,綁定此流水線的機器人將無法正常運作。',
|
||||
copyPipelineAction: '複製此流水線',
|
||||
copyPipelineHint: '建立一條新的流水線,並複製所有設定。',
|
||||
extensions: {
|
||||
title: '擴展集成',
|
||||
loadError: '載入插件清單失敗',
|
||||
@@ -1876,7 +1925,7 @@ const zhHant = {
|
||||
selectSkills: '選擇技能',
|
||||
noSkillsAvailable: '暫無可用技能',
|
||||
mcpServersScopeTooltip:
|
||||
'這裡僅控制此流程線綁定哪些 MCP 伺服器;具體 MCP 工具和資源在 AI 能力的內建 Agent 表單中選擇。',
|
||||
'這裡僅控制此流水線綁定哪些 MCP 伺服器;具體 MCP 工具和資源在 AI 能力的內建 Agent 表單中選擇。',
|
||||
enableAllMCPServersTooltip:
|
||||
'啟用後,所有已配置且啟用的 MCP 伺服器都會進入 AI 能力中的 MCP 工具和資源候選範圍。',
|
||||
},
|
||||
@@ -1911,8 +1960,8 @@ const zhHant = {
|
||||
selectKnowledgeBases: '選擇知識庫',
|
||||
},
|
||||
debugDialog: {
|
||||
title: '流程線對話',
|
||||
selectPipeline: '選擇流程線',
|
||||
title: '流水線對話',
|
||||
selectPipeline: '選擇流水線',
|
||||
sessionType: '對話類型',
|
||||
privateChat: '私聊',
|
||||
groupChat: '群聊',
|
||||
@@ -1927,7 +1976,7 @@ const zhHant = {
|
||||
resetSuccess: '對話已重設',
|
||||
resetFailed: '重設失敗',
|
||||
loadMessagesFailed: '載入訊息失敗',
|
||||
loadPipelinesFailed: '載入流程線失敗',
|
||||
loadPipelinesFailed: '載入流水線失敗',
|
||||
atTips: '提及機器人',
|
||||
streaming: '串流傳輸',
|
||||
streamOutput: '串流',
|
||||
@@ -1948,9 +1997,17 @@ const zhHant = {
|
||||
uploading: '上傳中...',
|
||||
},
|
||||
monitoring: {
|
||||
rangeDays: '最近 {{days}} 天',
|
||||
customRange: '自訂時間範圍',
|
||||
rangeStart: '開始時間',
|
||||
rangeEnd: '結束時間',
|
||||
rangeHint: '開始時間須早於結束時間,最多 365 天。',
|
||||
autoRefreshLabel: '定時重新整理',
|
||||
autoRefreshOff: '定時重新整理:關閉',
|
||||
autoRefreshEvery: '每 {{seconds}} 秒重新整理',
|
||||
title: '執行日誌',
|
||||
workbench: '設定與除錯',
|
||||
description: '檢視此流程線的執行記錄和錯誤資訊(最近24小時)',
|
||||
description: '檢視此流水線的執行記錄和錯誤資訊(最近24小時)',
|
||||
detailedLogs: '詳細日誌',
|
||||
},
|
||||
},
|
||||
@@ -2341,6 +2398,14 @@ const zhHant = {
|
||||
},
|
||||
},
|
||||
monitoring: {
|
||||
rangeDays: '最近 {{days}} 天',
|
||||
customRange: '自訂時間範圍',
|
||||
rangeStart: '開始時間',
|
||||
rangeEnd: '結束時間',
|
||||
rangeHint: '開始時間須早於結束時間,最多 365 天。',
|
||||
autoRefreshLabel: '定時重新整理',
|
||||
autoRefreshOff: '定時重新整理:關閉',
|
||||
autoRefreshEvery: '每 {{seconds}} 秒重新整理',
|
||||
unified: {
|
||||
inputs: '事件與輸入',
|
||||
outputs: '生成結果',
|
||||
@@ -2366,13 +2431,13 @@ const zhHant = {
|
||||
},
|
||||
execution: {
|
||||
title: '執行記錄',
|
||||
subtitle: '每條入站事件及其觸發的執行,涵蓋 Agent 與 Pipeline。',
|
||||
subtitle: '每條入站事件及其觸發的執行,涵蓋 Agent 與 流水線。',
|
||||
tabs: { executions: '執行記錄' },
|
||||
filters: {
|
||||
source: '類型',
|
||||
sourceAll: '全部類型',
|
||||
sourceAgent: 'Agent',
|
||||
sourcePipeline: 'Pipeline',
|
||||
sourcePipeline: '流水線',
|
||||
mode: '流量',
|
||||
modeAll: '真實 + 偵錯',
|
||||
modeLive: '僅真實',
|
||||
@@ -2391,7 +2456,7 @@ const zhHant = {
|
||||
// prettier-ignore
|
||||
kind: {
|
||||
agent: 'Agent',
|
||||
pipeline: 'Pipeline',
|
||||
pipeline: '流水線',
|
||||
processor: '處理器',
|
||||
},
|
||||
columns: {
|
||||
@@ -2411,7 +2476,7 @@ const zhHant = {
|
||||
tokensHint:
|
||||
'僅加總已記錄的用量;部分模型未回傳用量時,總數可能不完整。',
|
||||
totalExecutions: '執行總數',
|
||||
bySource: 'Agent {{agent}} · Pipeline {{pipeline}}',
|
||||
bySource: 'Agent {{agent}} · 流水線 {{pipeline}}',
|
||||
successRate: '執行成功率',
|
||||
successRateHint:
|
||||
'成功次數 ÷(成功 + 失敗)。不計進行中、已忽略及已取消的執行。',
|
||||
@@ -2492,6 +2557,15 @@ const zhHant = {
|
||||
sessions: '會話分析',
|
||||
errors: '錯誤日誌',
|
||||
},
|
||||
pipelineTrace: {
|
||||
received: '收到訊息',
|
||||
processing: '處理過程',
|
||||
replies: '回覆訊息',
|
||||
noSteps: '未記錄模型或工具呼叫',
|
||||
success: '成功',
|
||||
error: '失敗',
|
||||
pending: '處理中',
|
||||
},
|
||||
messageList: {
|
||||
timestamp: '時間戳記',
|
||||
bot: '機器人',
|
||||
@@ -2787,7 +2861,7 @@ const zhHant = {
|
||||
adapter: '適配器',
|
||||
model_provider: '模型提供商',
|
||||
llm_model: '模型',
|
||||
pipeline: '管道',
|
||||
pipeline: '流水線',
|
||||
user: '使用者',
|
||||
workspace: '工作區',
|
||||
monitoring: '監控',
|
||||
@@ -2845,7 +2919,7 @@ const zhHant = {
|
||||
codex_view: '查看 Codex 授權',
|
||||
codex_authorize: '授權 Codex',
|
||||
embed: '嵌入式工作階段',
|
||||
pipeline_extensions_update: '修改管道擴展綁定',
|
||||
pipeline_extensions_update: '修改流水線擴展綁定',
|
||||
export: '匯出資料',
|
||||
execute: '執行任務',
|
||||
debug: '偵錯任務',
|
||||
|
||||
Reference in New Issue
Block a user