feat(agent-platform): add scenario-based route creation

This commit is contained in:
huanghuoguoguo
2026-07-11 11:10:46 +08:00
parent f15f004680
commit a2b30957b6
6 changed files with 171 additions and 10 deletions
@@ -20,6 +20,10 @@ 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 mobileScreenshot = paths.screenshot.replace(/\.png$/, "-mobile.png");
const scenarioMenuScreenshot = paths.screenshot.replace(
/\.png$/,
"-scenario-menu.png",
);
const startedAt = new Date(); const startedAt = new Date();
const frontendUrl = process.env.LANGBOT_FRONTEND_URL || ""; const frontendUrl = process.env.LANGBOT_FRONTEND_URL || "";
@@ -50,6 +54,7 @@ const result = {
network_log: paths.networkLog, network_log: paths.networkLog,
screenshot: paths.screenshot, screenshot: paths.screenshot,
mobile_screenshot: mobileScreenshot, mobile_screenshot: mobileScreenshot,
scenario_menu_screenshot: scenarioMenuScreenshot,
automation_result_json: paths.automationResultJson, automation_result_json: paths.automationResultJson,
result_json: paths.resultJson, result_json: paths.resultJson,
}, },
@@ -93,10 +98,23 @@ try {
.getByText(/Event Routing|事件路由|イベントルーティング/) .getByText(/Event Routing|事件路由|イベントルーティング/)
.first() .first()
.waitFor(); .waitFor();
const addBehavior = page.getByRole("button", {
name: /Add behavior|添加行为|動作を追加/,
});
await addBehavior.waitFor();
await addBehavior.click();
const messageBehavior = page.getByRole("menuitem", {
name: /Reply to messages|回复收到的消息|受信メッセージに返信/,
});
await messageBehavior.waitFor();
await page.waitForTimeout(250);
await safeScreenshot(page, scenarioMenuScreenshot);
await messageBehavior.click();
await page await page
.getByRole("button", { name: /Add Route|添加路由|ルートを追加/ }) .getByText(/Message received|收到消息|メッセージを受信/)
.first()
.waitFor(); .waitFor();
result.visible_signals.push("create-mode-routing"); result.visible_signals.push("create-mode-routing", "scenario-route-added");
const create = await apiJson(backendUrl, "/api/v1/platform/bots", { const create = await apiJson(backendUrl, "/api/v1/platform/bots", {
method: "POST", method: "POST",
@@ -27,7 +27,7 @@ automation_env:
preconditions: preconditions:
- "The target is a local test instance where a temporary HTTP Bot may be created and deleted." - "The target is a local test instance where a temporary HTTP Bot may be created and deleted."
steps: steps:
- "Open Create Bot, choose HTTP Bot, and confirm event routing can be configured before the first save." - "Open Create Bot, choose HTTP Bot, and add a message-reply behavior before the first save."
- "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."
@@ -36,7 +36,7 @@ steps:
- "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 a scenario-labeled behavior 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: 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."
@@ -18,10 +18,15 @@ import {
GripVertical, GripVertical,
Info, Info,
ListChecks, ListChecks,
MessageSquare,
Plus, Plus,
Play, Play,
RefreshCw, RefreshCw,
Shield,
Trash2, Trash2,
UserCheck,
UserMinus,
UserPlus,
Workflow, Workflow,
XCircle, XCircle,
} from 'lucide-react'; } from 'lucide-react';
@@ -56,6 +61,13 @@ import {
DialogTitle, DialogTitle,
} from '@/components/ui/dialog'; } from '@/components/ui/dialog';
import { Alert, AlertDescription } from '@/components/ui/alert'; import { Alert, AlertDescription } from '@/components/ui/alert';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { import {
DndContext, DndContext,
DragOverlay, DragOverlay,
@@ -145,6 +157,39 @@ const ELEMENTS = [
// only emit message.received, so that's the sole fallback option. // only emit message.received, so that's the sole fallback option.
const DEFAULT_EVENTS = ['message.received']; const DEFAULT_EVENTS = ['message.received'];
const BEHAVIOR_PRESETS = [
{
eventType: 'message.received',
labelKey: 'bots.behaviorReplyMessages',
descriptionKey: 'bots.behaviorReplyMessagesDescription',
icon: MessageSquare,
},
{
eventType: 'group.member_joined',
labelKey: 'bots.behaviorWelcomeMembers',
descriptionKey: 'bots.behaviorWelcomeMembersDescription',
icon: UserPlus,
},
{
eventType: 'group.member_left',
labelKey: 'bots.behaviorHandleDepartures',
descriptionKey: 'bots.behaviorHandleDeparturesDescription',
icon: UserMinus,
},
{
eventType: 'friend.request_received',
labelKey: 'bots.behaviorReviewFriendRequests',
descriptionKey: 'bots.behaviorReviewFriendRequestsDescription',
icon: UserCheck,
},
{
eventType: 'group.member_banned',
labelKey: 'bots.behaviorHandleModeration',
descriptionKey: 'bots.behaviorHandleModerationDescription',
icon: Shield,
},
];
// ── helpers ─────────────────────────────────────────────────────────────────── // ── helpers ───────────────────────────────────────────────────────────────────
function isMessageEventPattern(p: string) { function isMessageEventPattern(p: string) {
@@ -1451,6 +1496,13 @@ export default function EventBindingsEditor({
() => findCatchAllRouteIndex(bindings), () => findCatchAllRouteIndex(bindings),
[bindings], [bindings],
); );
const behaviorPresets = useMemo(
() =>
BEHAVIOR_PRESETS.filter((preset) =>
dryRunEventOptions.includes(preset.eventType),
),
[dryRunEventOptions],
);
const refreshRouteStatuses = useCallback(async () => { const refreshRouteStatuses = useCallback(async () => {
if (!botId) { if (!botId) {
@@ -1479,11 +1531,11 @@ export default function EventBindingsEditor({
form.setValue('event_bindings', next, { shouldDirty: true }); form.setValue('event_bindings', next, { shouldDirty: true });
} }
function addBinding() { function addBinding(eventPattern = 'message.received') {
updateBindings([ updateBindings([
...bindings, ...bindings,
{ {
event_pattern: 'message.received', event_pattern: eventPattern,
target_type: 'agent', target_type: 'agent',
target_uuid: '', target_uuid: '',
priority: 0, priority: 0,
@@ -1673,10 +1725,48 @@ export default function EventBindingsEditor({
</DndContext> </DndContext>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
<Button type="button" variant="outline" size="sm" onClick={addBinding}> <DropdownMenu>
<Plus className="h-4 w-4 mr-1" /> <DropdownMenuTrigger asChild>
{t('bots.addEventBinding')} <Button type="button" variant="outline" size="sm">
</Button> <Plus className="h-4 w-4 mr-1" />
{t('bots.addBehavior')}
<ChevronDown className="ml-1 h-3.5 w-3.5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-[300px] max-w-[90vw]">
{behaviorPresets.map((preset) => {
const Icon = preset.icon;
return (
<DropdownMenuItem
key={preset.eventType}
className="items-start gap-2 py-2"
onClick={() => addBinding(preset.eventType)}
>
<Icon className="mt-0.5 h-4 w-4 shrink-0" />
<span className="flex min-w-0 flex-col gap-0.5">
<span>{t(preset.labelKey)}</span>
<span className="text-xs text-muted-foreground">
{t(preset.descriptionKey)}
</span>
</span>
</DropdownMenuItem>
);
})}
<DropdownMenuSeparator />
<DropdownMenuItem
className="items-start gap-2 py-2"
onClick={() => addBinding(dryRunEventOptions[0])}
>
<Workflow className="mt-0.5 h-4 w-4 shrink-0" />
<span className="flex min-w-0 flex-col gap-0.5">
<span>{t('bots.behaviorCustom')}</span>
<span className="text-xs text-muted-foreground">
{t('bots.behaviorCustomDescription')}
</span>
</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<RouteDryRunDialog <RouteDryRunDialog
botId={botId} botId={botId}
bindings={bindings} bindings={bindings}
+19
View File
@@ -367,6 +367,25 @@ const enUS = {
'Choose which processor handles each event received by this bot. Edit the processing logic on the Agent page. Pipelines only support message events.', 'Choose which processor handles each event received by this bot. Edit the processing logic on the Agent page. Pipelines only support message events.',
eventBindings: 'Event Routes', eventBindings: 'Event Routes',
addEventBinding: 'Add Route', addEventBinding: 'Add Route',
addBehavior: 'Add behavior',
behaviorReplyMessages: 'Reply to messages',
behaviorReplyMessagesDescription:
'Send incoming messages to an Agent or Pipeline.',
behaviorWelcomeMembers: 'Welcome new members',
behaviorWelcomeMembersDescription:
'Run an Agent when someone joins a group.',
behaviorHandleDepartures: 'Handle member departures',
behaviorHandleDeparturesDescription:
'Run an Agent when someone leaves or is removed.',
behaviorReviewFriendRequests: 'Review friend requests',
behaviorReviewFriendRequestsDescription:
'Let an Agent decide how to handle a new request.',
behaviorHandleModeration: 'Handle moderation events',
behaviorHandleModerationDescription:
'Run an Agent when a group member is restricted.',
behaviorCustom: 'Configure another event',
behaviorCustomDescription:
'Add a route and choose from every event supported by this adapter.',
eventPattern: 'Event', eventPattern: 'Event',
eventPatternPlaceholder: 'Select event', eventPatternPlaceholder: 'Select event',
targetType: 'Target Type', targetType: 'Target Type',
+19
View File
@@ -373,6 +373,25 @@ const jaJP = {
'このボットが受信した各イベントをどのプロセッサーに渡すかを選択します。処理ロジックは Agent ページで編集します。Pipeline はメッセージイベントのみ対応します。', 'このボットが受信した各イベントをどのプロセッサーに渡すかを選択します。処理ロジックは Agent ページで編集します。Pipeline はメッセージイベントのみ対応します。',
eventBindings: 'イベントルート', eventBindings: 'イベントルート',
addEventBinding: 'ルートを追加', addEventBinding: 'ルートを追加',
addBehavior: '動作を追加',
behaviorReplyMessages: '受信メッセージに返信',
behaviorReplyMessagesDescription:
'受信メッセージを Agent または Pipeline で処理します。',
behaviorWelcomeMembers: '新しいメンバーを歓迎',
behaviorWelcomeMembersDescription:
'メンバーがグループに参加したときに Agent を実行します。',
behaviorHandleDepartures: 'メンバーの退出を処理',
behaviorHandleDeparturesDescription:
'メンバーが退出または削除されたときに Agent を実行します。',
behaviorReviewFriendRequests: '友だち申請を確認',
behaviorReviewFriendRequestsDescription:
'新しい申請の処理方法を Agent に判断させます。',
behaviorHandleModeration: 'モデレーションイベントを処理',
behaviorHandleModerationDescription:
'グループメンバーが制限されたときに Agent を実行します。',
behaviorCustom: '別のイベントを設定',
behaviorCustomDescription:
'ルートを追加し、このアダプターが対応する全イベントから選択します。',
eventPattern: 'イベント', eventPattern: 'イベント',
eventPatternPlaceholder: 'イベントを選択', eventPatternPlaceholder: 'イベントを選択',
targetType: 'ターゲットタイプ', targetType: 'ターゲットタイプ',
+15
View File
@@ -352,6 +352,21 @@ const zhHans = {
'选择此机器人收到不同事件时交给哪个处理器。具体处理逻辑在 Agent 页面编辑,Pipeline 仅支持消息事件。', '选择此机器人收到不同事件时交给哪个处理器。具体处理逻辑在 Agent 页面编辑,Pipeline 仅支持消息事件。',
eventBindings: '事件路由', eventBindings: '事件路由',
addEventBinding: '添加路由', addEventBinding: '添加路由',
addBehavior: '添加行为',
behaviorReplyMessages: '回复收到的消息',
behaviorReplyMessagesDescription:
'把收到的消息交给 Agent 或 Pipeline 处理。',
behaviorWelcomeMembers: '欢迎新成员',
behaviorWelcomeMembersDescription: '有人加入群组时运行 Agent。',
behaviorHandleDepartures: '处理成员离群',
behaviorHandleDeparturesDescription: '有人离开或被移出群组时运行 Agent。',
behaviorReviewFriendRequests: '审核好友请求',
behaviorReviewFriendRequestsDescription:
'让 Agent 决定如何处理新的好友请求。',
behaviorHandleModeration: '处理群管理事件',
behaviorHandleModerationDescription: '群成员受到限制时运行 Agent。',
behaviorCustom: '配置其他事件',
behaviorCustomDescription: '添加路由,并从此适配器支持的全部事件中选择。',
eventPattern: '事件', eventPattern: '事件',
eventPatternPlaceholder: '选择事件', eventPatternPlaceholder: '选择事件',
targetType: '目标类型', targetType: '目标类型',