mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-21 20:06:06 +00:00
feat(agent-platform): explain route conflicts and fallback
This commit is contained in:
@@ -19,6 +19,7 @@ const caseId = "bot-event-routing-product-flow";
|
|||||||
await loadEnvFiles();
|
await loadEnvFiles();
|
||||||
const paths = evidencePaths(caseId);
|
const paths = evidencePaths(caseId);
|
||||||
await ensureEvidence(paths);
|
await ensureEvidence(paths);
|
||||||
|
const mobileScreenshot = paths.screenshot.replace(/\.png$/, "-mobile.png");
|
||||||
|
|
||||||
const startedAt = new Date();
|
const startedAt = new Date();
|
||||||
const frontendUrl = process.env.LANGBOT_FRONTEND_URL || "";
|
const frontendUrl = process.env.LANGBOT_FRONTEND_URL || "";
|
||||||
@@ -48,6 +49,7 @@ const result = {
|
|||||||
console_log: paths.consoleLog,
|
console_log: paths.consoleLog,
|
||||||
network_log: paths.networkLog,
|
network_log: paths.networkLog,
|
||||||
screenshot: paths.screenshot,
|
screenshot: paths.screenshot,
|
||||||
|
mobile_screenshot: mobileScreenshot,
|
||||||
automation_result_json: paths.automationResultJson,
|
automation_result_json: paths.automationResultJson,
|
||||||
result_json: paths.resultJson,
|
result_json: paths.resultJson,
|
||||||
},
|
},
|
||||||
@@ -125,6 +127,17 @@ try {
|
|||||||
description: "Discard the deterministic QA event",
|
description: "Discard the deterministic QA event",
|
||||||
order: 0,
|
order: 0,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "qa-message-discard-shadowed",
|
||||||
|
event_pattern: "message.received",
|
||||||
|
target_type: "discard",
|
||||||
|
target_uuid: "",
|
||||||
|
filters: [],
|
||||||
|
priority: 0,
|
||||||
|
enabled: true,
|
||||||
|
description: "Verify visible route conflict guidance",
|
||||||
|
order: 1,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -158,10 +171,22 @@ try {
|
|||||||
.getByText(/Message received|收到消息|メッセージを受信/)
|
.getByText(/Message received|收到消息|メッセージを受信/)
|
||||||
.first()
|
.first()
|
||||||
.waitFor();
|
.waitFor();
|
||||||
|
await page
|
||||||
|
.getByText(
|
||||||
|
/Some routes overlap|部分路由存在覆盖冲突|一部のルートが重複しています/,
|
||||||
|
)
|
||||||
|
.waitFor();
|
||||||
|
await page
|
||||||
|
.getByText(
|
||||||
|
/Events that match no route are ignored|未命中任何路由的事件会被忽略|どのルートにも一致しないイベントは無視されます/,
|
||||||
|
)
|
||||||
|
.waitFor();
|
||||||
result.visible_signals.push(
|
result.visible_signals.push(
|
||||||
"event-routing",
|
"event-routing",
|
||||||
"adapter-capabilities",
|
"adapter-capabilities",
|
||||||
"friendly-event-name",
|
"friendly-event-name",
|
||||||
|
"route-conflict-guidance",
|
||||||
|
"fallback-guidance",
|
||||||
);
|
);
|
||||||
|
|
||||||
await page
|
await page
|
||||||
@@ -234,6 +259,23 @@ try {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await safeScreenshot(page, paths.screenshot);
|
await safeScreenshot(page, paths.screenshot);
|
||||||
|
await page.setViewportSize({ width: 390, height: 844 });
|
||||||
|
await page.waitForTimeout(250);
|
||||||
|
const horizontalOverflow = await page.evaluate(
|
||||||
|
() => document.documentElement.scrollWidth - window.innerWidth,
|
||||||
|
);
|
||||||
|
if (horizontalOverflow > 1) {
|
||||||
|
throw new Error(
|
||||||
|
`The mobile route editor overflows horizontally by ${horizontalOverflow}px.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await page
|
||||||
|
.getByText(
|
||||||
|
/Some routes overlap|部分路由存在覆盖冲突|一部のルートが重複しています/,
|
||||||
|
)
|
||||||
|
.waitFor();
|
||||||
|
await safeScreenshot(page, mobileScreenshot);
|
||||||
|
result.visible_signals.push("mobile-layout");
|
||||||
result.diagnostics = await scanBrowserDiagnostics(paths);
|
result.diagnostics = await scanBrowserDiagnostics(paths);
|
||||||
if (result.diagnostics.status !== "pass") {
|
if (result.diagnostics.status !== "pass") {
|
||||||
throw new Error(result.diagnostics.reason);
|
throw new Error(result.diagnostics.reason);
|
||||||
|
|||||||
@@ -31,12 +31,14 @@ steps:
|
|||||||
- "Create a temporary HTTP Bot with a saved message.received route to the discard processor."
|
- "Create a temporary HTTP Bot with a saved message.received route to the discard processor."
|
||||||
- "Open the Bot configuration in the WebUI."
|
- "Open the Bot configuration in the WebUI."
|
||||||
- "Confirm the adapter capability summary, friendly event name, target, and route status are visible."
|
- "Confirm the adapter capability summary, friendly event name, target, and route status are visible."
|
||||||
|
- "Confirm overlapping routes and unmatched-event fallback behavior are explained before save."
|
||||||
- "Open Test event route and run a dry-run against the current form."
|
- "Open Test event route and run a dry-run against the current form."
|
||||||
- "Run the saved runtime route with a synthetic event."
|
- "Run the saved runtime route with a synthetic event."
|
||||||
- "Close the dialog and confirm the route card shows the latest discarded status."
|
- "Close the dialog and confirm the route card shows the latest discarded status."
|
||||||
checks:
|
checks:
|
||||||
- "UI: A user can choose a channel and add event routes during initial Bot creation."
|
- "UI: A user can choose a channel and add event routes during initial Bot creation."
|
||||||
- "UI: Event routing uses user-facing labels and does not require the raw event name in the primary route card."
|
- "UI: Event routing uses user-facing labels and does not require the raw event name in the primary route card."
|
||||||
|
- "UI: Definite route shadowing and unmatched-event fallback behavior are visible without opening raw logs."
|
||||||
- "UI: Dry-run visibly reports that the route matched the discard processor."
|
- "UI: Dry-run visibly reports that the route matched the discard processor."
|
||||||
- "UI: Saved-route execution visibly succeeds, explains its side-effect boundary, and updates route status to discarded."
|
- "UI: Saved-route execution visibly succeeds, explains its side-effect boundary, and updates route status to discarded."
|
||||||
- "Console: No unexpected frontend errors appear during the flow."
|
- "Console: No unexpected frontend errors appear during the flow."
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
ChevronRight,
|
ChevronRight,
|
||||||
ChevronsUpDown,
|
ChevronsUpDown,
|
||||||
GripVertical,
|
GripVertical,
|
||||||
|
Info,
|
||||||
ListChecks,
|
ListChecks,
|
||||||
Plus,
|
Plus,
|
||||||
Play,
|
Play,
|
||||||
@@ -161,6 +162,90 @@ function eventPatternCovers(sup: string, bind: string) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface RouteConflict {
|
||||||
|
winnerIndex: number;
|
||||||
|
shadowedIndex: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindingFilters(binding: EventBinding) {
|
||||||
|
return binding.filters ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function filtersEqual(a: EventBinding, b: EventBinding) {
|
||||||
|
return (
|
||||||
|
JSON.stringify(bindingFilters(a)) === JSON.stringify(bindingFilters(b))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function routePrecedes(
|
||||||
|
a: EventBinding,
|
||||||
|
aIndex: number,
|
||||||
|
b: EventBinding,
|
||||||
|
bIndex: number,
|
||||||
|
) {
|
||||||
|
const aPriority = Number.isFinite(a.priority) ? a.priority : 0;
|
||||||
|
const bPriority = Number.isFinite(b.priority) ? b.priority : 0;
|
||||||
|
return aPriority === bPriority ? aIndex < bIndex : aPriority > bPriority;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findRouteConflicts(bindings: EventBinding[]): RouteConflict[] {
|
||||||
|
const enabled = bindings
|
||||||
|
.map((binding, index) => ({ binding, index }))
|
||||||
|
.filter(({ binding }) => binding.enabled ?? true);
|
||||||
|
const conflicts: RouteConflict[] = [];
|
||||||
|
|
||||||
|
for (let left = 0; left < enabled.length; left += 1) {
|
||||||
|
for (let right = left + 1; right < enabled.length; right += 1) {
|
||||||
|
const a = enabled[left];
|
||||||
|
const b = enabled[right];
|
||||||
|
const [winner, shadowed] = routePrecedes(
|
||||||
|
a.binding,
|
||||||
|
a.index,
|
||||||
|
b.binding,
|
||||||
|
b.index,
|
||||||
|
)
|
||||||
|
? [a, b]
|
||||||
|
: [b, a];
|
||||||
|
|
||||||
|
if (
|
||||||
|
!eventPatternCovers(
|
||||||
|
winner.binding.event_pattern,
|
||||||
|
shadowed.binding.event_pattern,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
bindingFilters(winner.binding).length === 0 ||
|
||||||
|
filtersEqual(winner.binding, shadowed.binding)
|
||||||
|
) {
|
||||||
|
conflicts.push({
|
||||||
|
winnerIndex: winner.index,
|
||||||
|
shadowedIndex: shadowed.index,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return conflicts;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findCatchAllRouteIndex(bindings: EventBinding[]) {
|
||||||
|
const candidates = bindings
|
||||||
|
.map((binding, index) => ({ binding, index }))
|
||||||
|
.filter(
|
||||||
|
({ binding }) =>
|
||||||
|
(binding.enabled ?? true) &&
|
||||||
|
binding.event_pattern === '*' &&
|
||||||
|
bindingFilters(binding).length === 0,
|
||||||
|
);
|
||||||
|
candidates.sort((a, b) =>
|
||||||
|
routePrecedes(a.binding, a.index, b.binding, b.index) ? -1 : 1,
|
||||||
|
);
|
||||||
|
return candidates[0]?.index ?? -1;
|
||||||
|
}
|
||||||
|
|
||||||
function agentSupportsEventPattern(agent: Agent, pattern: string) {
|
function agentSupportsEventPattern(agent: Agent, pattern: string) {
|
||||||
const patterns = agent.supported_event_patterns ??
|
const patterns = agent.supported_event_patterns ??
|
||||||
agent.capability?.supported_event_patterns ?? ['*'];
|
agent.capability?.supported_event_patterns ?? ['*'];
|
||||||
@@ -1135,7 +1220,7 @@ function BindingCardContent({
|
|||||||
return (
|
return (
|
||||||
<div className="rounded-lg border bg-card">
|
<div className="rounded-lg border bg-card">
|
||||||
{/* main row */}
|
{/* main row */}
|
||||||
<div className="flex items-center gap-2 p-2.5">
|
<div className="flex flex-wrap items-center gap-2 p-2.5">
|
||||||
{isEnabled && (
|
{isEnabled && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -1146,6 +1231,14 @@ function BindingCardContent({
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
className="h-5 min-w-5 shrink-0 justify-center px-1 text-[10px]"
|
||||||
|
title={t('bots.dryRunRuleIndex', { index: globalIndex + 1 })}
|
||||||
|
>
|
||||||
|
{globalIndex + 1}
|
||||||
|
</Badge>
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
value={binding.event_pattern}
|
value={binding.event_pattern}
|
||||||
onValueChange={(eventPattern) => {
|
onValueChange={(eventPattern) => {
|
||||||
@@ -1162,7 +1255,7 @@ function BindingCardContent({
|
|||||||
onUpdate(globalIndex, patch);
|
onUpdate(globalIndex, patch);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-8 flex-1 min-w-0 text-sm">
|
<SelectTrigger className="h-8 min-w-[150px] flex-1 text-sm">
|
||||||
{binding.event_pattern ? (
|
{binding.event_pattern ? (
|
||||||
<span className="truncate">
|
<span className="truncate">
|
||||||
{eventLabel(binding.event_pattern, t)}
|
{eventLabel(binding.event_pattern, t)}
|
||||||
@@ -1217,54 +1310,37 @@ function BindingCardContent({
|
|||||||
onUpdate={(patch) => onUpdate(globalIndex, patch)}
|
onUpdate={(patch) => onUpdate(globalIndex, patch)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="hidden min-w-[132px] max-w-[220px] flex-col items-end gap-1 lg:flex">
|
|
||||||
<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="max-w-full truncate text-[11px] text-muted-foreground"
|
|
||||||
title={
|
|
||||||
statusTime ? `${statusDetail} · ${statusTime}` : statusDetail
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{statusDetail || statusTime}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!pipelineAllowed && binding.target_type === 'pipeline' && (
|
{!pipelineAllowed && binding.target_type === 'pipeline' && (
|
||||||
<span className="text-xs text-destructive shrink-0">
|
<span className="text-xs text-destructive shrink-0">
|
||||||
{t('bots.unsupportedPipelineEvent')}
|
{t('bots.unsupportedPipelineEvent')}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* disable/enable toggle */}
|
<div className="ml-auto flex shrink-0 items-center gap-1">
|
||||||
<Button
|
{/* disable/enable toggle */}
|
||||||
type="button"
|
<Button
|
||||||
variant="ghost"
|
type="button"
|
||||||
size="sm"
|
variant="ghost"
|
||||||
className="h-8 px-2 text-xs text-muted-foreground shrink-0"
|
size="sm"
|
||||||
onClick={() => onUpdate(globalIndex, { enabled: !isEnabled })}
|
className="h-8 px-2 text-xs text-muted-foreground"
|
||||||
>
|
onClick={() => onUpdate(globalIndex, { enabled: !isEnabled })}
|
||||||
{isEnabled ? t('bots.disable') : t('bots.enable')}
|
>
|
||||||
</Button>
|
{isEnabled ? t('bots.disable') : t('bots.enable')}
|
||||||
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-8 w-8 shrink-0"
|
className="h-8 w-8"
|
||||||
onClick={() => onRemove(globalIndex)}
|
onClick={() => onRemove(globalIndex)}
|
||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4 text-muted-foreground hover:text-destructive" />
|
<Trash2 className="h-4 w-4 text-muted-foreground hover:text-destructive" />
|
||||||
</Button>
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 border-t px-3 py-1.5 lg:hidden">
|
<div className="flex items-center gap-2 border-t px-3 py-1.5">
|
||||||
<Badge
|
<Badge
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className={`rounded-md px-2 py-0.5 text-[10px] font-medium ${routeStatusBadgeClass(
|
className={`rounded-md px-2 py-0.5 text-[10px] font-medium ${routeStatusBadgeClass(
|
||||||
@@ -1275,9 +1351,9 @@ function BindingCardContent({
|
|||||||
</Badge>
|
</Badge>
|
||||||
<span
|
<span
|
||||||
className="min-w-0 truncate text-[11px] text-muted-foreground"
|
className="min-w-0 truncate text-[11px] text-muted-foreground"
|
||||||
title={routeStatus?.reason || routeStatus?.message || statusTime}
|
title={statusTime ? `${statusDetail} · ${statusTime}` : statusDetail}
|
||||||
>
|
>
|
||||||
{routeStatus?.failure_code || routeStatus?.reason || statusTime}
|
{statusDetail || statusTime}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1328,7 +1404,9 @@ export default function EventBindingsEditor({
|
|||||||
agentOptions,
|
agentOptions,
|
||||||
}: EventBindingsEditorProps) {
|
}: EventBindingsEditorProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const bindings: EventBinding[] = form.watch('event_bindings') || [];
|
const watchedBindings: EventBinding[] | undefined =
|
||||||
|
form.watch('event_bindings');
|
||||||
|
const bindings = useMemo(() => watchedBindings ?? [], [watchedBindings]);
|
||||||
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
|
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
|
||||||
const [disabledSectionOpen, setDisabledSectionOpen] = useState(false);
|
const [disabledSectionOpen, setDisabledSectionOpen] = useState(false);
|
||||||
const [activeId, setActiveId] = useState<string | null>(null);
|
const [activeId, setActiveId] = useState<string | null>(null);
|
||||||
@@ -1365,6 +1443,14 @@ export default function EventBindingsEditor({
|
|||||||
});
|
});
|
||||||
return map;
|
return map;
|
||||||
}, [routeStatuses]);
|
}, [routeStatuses]);
|
||||||
|
const routeConflicts = useMemo(
|
||||||
|
() => findRouteConflicts(bindings),
|
||||||
|
[bindings],
|
||||||
|
);
|
||||||
|
const catchAllRouteIndex = useMemo(
|
||||||
|
() => findCatchAllRouteIndex(bindings),
|
||||||
|
[bindings],
|
||||||
|
);
|
||||||
|
|
||||||
const refreshRouteStatuses = useCallback(async () => {
|
const refreshRouteStatuses = useCallback(async () => {
|
||||||
if (!botId) {
|
if (!botId) {
|
||||||
@@ -1400,7 +1486,7 @@ export default function EventBindingsEditor({
|
|||||||
event_pattern: 'message.received',
|
event_pattern: 'message.received',
|
||||||
target_type: 'agent',
|
target_type: 'agent',
|
||||||
target_uuid: '',
|
target_uuid: '',
|
||||||
priority: bindings.length,
|
priority: 0,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
description: '',
|
description: '',
|
||||||
filters: [],
|
filters: [],
|
||||||
@@ -1481,6 +1567,49 @@ export default function EventBindingsEditor({
|
|||||||
eventOptions={eventOptions}
|
eventOptions={eventOptions}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{routeConflicts.length > 0 && (
|
||||||
|
<Alert className="border-amber-200 bg-amber-50/60 text-amber-900 dark:border-amber-900/50 dark:bg-amber-950/20 dark:text-amber-200">
|
||||||
|
<AlertCircle className="h-4 w-4" />
|
||||||
|
<AlertDescription>
|
||||||
|
<p className="font-medium">{t('bots.routeConflictTitle')}</p>
|
||||||
|
<ul className="mt-1 space-y-1 text-xs">
|
||||||
|
{routeConflicts.slice(0, 3).map((conflict) => (
|
||||||
|
<li key={`${conflict.winnerIndex}:${conflict.shadowedIndex}`}>
|
||||||
|
{t('bots.routeConflictShadowed', {
|
||||||
|
winner: t('bots.dryRunRuleIndex', {
|
||||||
|
index: conflict.winnerIndex + 1,
|
||||||
|
}),
|
||||||
|
shadowed: t('bots.dryRunRuleIndex', {
|
||||||
|
index: conflict.shadowedIndex + 1,
|
||||||
|
}),
|
||||||
|
})}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
{routeConflicts.length > 3 && (
|
||||||
|
<p className="mt-1 text-xs">
|
||||||
|
{t('bots.routeConflictMore', {
|
||||||
|
count: routeConflicts.length - 3,
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Alert>
|
||||||
|
<Info className="h-4 w-4" />
|
||||||
|
<AlertDescription>
|
||||||
|
{catchAllRouteIndex >= 0
|
||||||
|
? t('bots.routeFallbackCatchAll', {
|
||||||
|
route: t('bots.dryRunRuleIndex', {
|
||||||
|
index: catchAllRouteIndex + 1,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
: t('bots.routeFallbackIgnored')}
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
{/* enabled section */}
|
{/* enabled section */}
|
||||||
<DndContext
|
<DndContext
|
||||||
sensors={sensors}
|
sensors={sensors}
|
||||||
|
|||||||
@@ -391,6 +391,14 @@ const enUS = {
|
|||||||
adapterEventsMore: '{{count}} more',
|
adapterEventsMore: '{{count}} more',
|
||||||
advancedEventValues: 'Advanced event values',
|
advancedEventValues: 'Advanced event values',
|
||||||
eventGroup: 'Group',
|
eventGroup: 'Group',
|
||||||
|
routeConflictTitle: 'Some routes overlap',
|
||||||
|
routeConflictShadowed:
|
||||||
|
'{{shadowed}} may never run because {{winner}} handles the same events first.',
|
||||||
|
routeConflictMore: '{{count}} more route conflicts need attention.',
|
||||||
|
routeFallbackCatchAll:
|
||||||
|
'{{route}} is the catch-all route. Routes with higher priority run first.',
|
||||||
|
routeFallbackIgnored:
|
||||||
|
'Events that match no route are ignored. Add a catch-all route only when every event needs an explicit outcome.',
|
||||||
testRoute: 'Test route',
|
testRoute: 'Test route',
|
||||||
refreshRouteStatus: 'Refresh status',
|
refreshRouteStatus: 'Refresh status',
|
||||||
routeStatusIdle: 'No run yet',
|
routeStatusIdle: 'No run yet',
|
||||||
|
|||||||
@@ -398,6 +398,14 @@ const jaJP = {
|
|||||||
adapterEventsMore: 'ほか {{count}} 件',
|
adapterEventsMore: 'ほか {{count}} 件',
|
||||||
advancedEventValues: '高度なイベント値',
|
advancedEventValues: '高度なイベント値',
|
||||||
eventGroup: 'グループ',
|
eventGroup: 'グループ',
|
||||||
|
routeConflictTitle: '一部のルートが重複しています',
|
||||||
|
routeConflictShadowed:
|
||||||
|
'{{winner}} が同じイベントを先に処理するため、{{shadowed}} は実行されない可能性があります。',
|
||||||
|
routeConflictMore: 'ほか {{count}} 件のルート競合を確認してください。',
|
||||||
|
routeFallbackCatchAll:
|
||||||
|
'{{route}} はすべてのイベントを受けるフォールバックです。優先度の高いルートが先に実行されます。',
|
||||||
|
routeFallbackIgnored:
|
||||||
|
'どのルートにも一致しないイベントは無視されます。すべてのイベントに明示的な結果が必要な場合のみ、フォールバックを追加してください。',
|
||||||
testRoute: 'ルートをテスト',
|
testRoute: 'ルートをテスト',
|
||||||
refreshRouteStatus: '状態を更新',
|
refreshRouteStatus: '状態を更新',
|
||||||
routeStatusIdle: '実行記録なし',
|
routeStatusIdle: '実行記録なし',
|
||||||
|
|||||||
@@ -376,6 +376,14 @@ const zhHans = {
|
|||||||
adapterEventsMore: '另有 {{count}} 类',
|
adapterEventsMore: '另有 {{count}} 类',
|
||||||
advancedEventValues: '高级事件值',
|
advancedEventValues: '高级事件值',
|
||||||
eventGroup: '事件组',
|
eventGroup: '事件组',
|
||||||
|
routeConflictTitle: '部分路由存在覆盖冲突',
|
||||||
|
routeConflictShadowed:
|
||||||
|
'{{shadowed}} 可能永远不会运行,因为 {{winner}} 会先处理相同事件。',
|
||||||
|
routeConflictMore: '另有 {{count}} 个路由冲突需要处理。',
|
||||||
|
routeFallbackCatchAll:
|
||||||
|
'{{route}} 是全局兜底路由,优先级更高的路由会先运行。',
|
||||||
|
routeFallbackIgnored:
|
||||||
|
'未命中任何路由的事件会被忽略。只有需要为每个事件指定结果时,才添加全局兜底路由。',
|
||||||
testRoute: '测试路由',
|
testRoute: '测试路由',
|
||||||
refreshRouteStatus: '刷新状态',
|
refreshRouteStatus: '刷新状态',
|
||||||
routeStatusIdle: '暂无运行记录',
|
routeStatusIdle: '暂无运行记录',
|
||||||
|
|||||||
Reference in New Issue
Block a user