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);
await ensureEvidence(paths);
const mobileScreenshot = paths.screenshot.replace(/\.png$/, "-mobile.png");
const scenarioMenuScreenshot = paths.screenshot.replace(
/\.png$/,
"-scenario-menu.png",
);
const startedAt = new Date();
const frontendUrl = process.env.LANGBOT_FRONTEND_URL || "";
@@ -50,6 +54,7 @@ const result = {
network_log: paths.networkLog,
screenshot: paths.screenshot,
mobile_screenshot: mobileScreenshot,
scenario_menu_screenshot: scenarioMenuScreenshot,
automation_result_json: paths.automationResultJson,
result_json: paths.resultJson,
},
@@ -93,10 +98,23 @@ try {
.getByText(/Event Routing|事件路由|イベントルーティング/)
.first()
.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
.getByRole("button", { name: /Add Route|添加路由|ルートを追加/ })
.getByText(/Message received|收到消息|メッセージを受信/)
.first()
.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", {
method: "POST",
@@ -27,7 +27,7 @@ automation_env:
preconditions:
- "The target is a local test instance where a temporary HTTP Bot may be created and deleted."
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."
- "Open the Bot configuration in the WebUI."
- "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."
- "Close the dialog and confirm the route card shows the latest discarded status."
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: 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."
@@ -18,10 +18,15 @@ import {
GripVertical,
Info,
ListChecks,
MessageSquare,
Plus,
Play,
RefreshCw,
Shield,
Trash2,
UserCheck,
UserMinus,
UserPlus,
Workflow,
XCircle,
} from 'lucide-react';
@@ -56,6 +61,13 @@ import {
DialogTitle,
} from '@/components/ui/dialog';
import { Alert, AlertDescription } from '@/components/ui/alert';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import {
DndContext,
DragOverlay,
@@ -145,6 +157,39 @@ const ELEMENTS = [
// only emit message.received, so that's the sole fallback option.
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 ───────────────────────────────────────────────────────────────────
function isMessageEventPattern(p: string) {
@@ -1451,6 +1496,13 @@ export default function EventBindingsEditor({
() => findCatchAllRouteIndex(bindings),
[bindings],
);
const behaviorPresets = useMemo(
() =>
BEHAVIOR_PRESETS.filter((preset) =>
dryRunEventOptions.includes(preset.eventType),
),
[dryRunEventOptions],
);
const refreshRouteStatuses = useCallback(async () => {
if (!botId) {
@@ -1479,11 +1531,11 @@ export default function EventBindingsEditor({
form.setValue('event_bindings', next, { shouldDirty: true });
}
function addBinding() {
function addBinding(eventPattern = 'message.received') {
updateBindings([
...bindings,
{
event_pattern: 'message.received',
event_pattern: eventPattern,
target_type: 'agent',
target_uuid: '',
priority: 0,
@@ -1673,10 +1725,48 @@ export default function EventBindingsEditor({
</DndContext>
<div className="flex flex-wrap gap-2">
<Button type="button" variant="outline" size="sm" onClick={addBinding}>
<Plus className="h-4 w-4 mr-1" />
{t('bots.addEventBinding')}
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button type="button" variant="outline" size="sm">
<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
botId={botId}
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.',
eventBindings: 'Event Routes',
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',
eventPatternPlaceholder: 'Select event',
targetType: 'Target Type',
+19
View File
@@ -373,6 +373,25 @@ const jaJP = {
'このボットが受信した各イベントをどのプロセッサーに渡すかを選択します。処理ロジックは Agent ページで編集します。Pipeline はメッセージイベントのみ対応します。',
eventBindings: 'イベントルート',
addEventBinding: 'ルートを追加',
addBehavior: '動作を追加',
behaviorReplyMessages: '受信メッセージに返信',
behaviorReplyMessagesDescription:
'受信メッセージを Agent または Pipeline で処理します。',
behaviorWelcomeMembers: '新しいメンバーを歓迎',
behaviorWelcomeMembersDescription:
'メンバーがグループに参加したときに Agent を実行します。',
behaviorHandleDepartures: 'メンバーの退出を処理',
behaviorHandleDeparturesDescription:
'メンバーが退出または削除されたときに Agent を実行します。',
behaviorReviewFriendRequests: '友だち申請を確認',
behaviorReviewFriendRequestsDescription:
'新しい申請の処理方法を Agent に判断させます。',
behaviorHandleModeration: 'モデレーションイベントを処理',
behaviorHandleModerationDescription:
'グループメンバーが制限されたときに Agent を実行します。',
behaviorCustom: '別のイベントを設定',
behaviorCustomDescription:
'ルートを追加し、このアダプターが対応する全イベントから選択します。',
eventPattern: 'イベント',
eventPatternPlaceholder: 'イベントを選択',
targetType: 'ターゲットタイプ',
+15
View File
@@ -352,6 +352,21 @@ const zhHans = {
'选择此机器人收到不同事件时交给哪个处理器。具体处理逻辑在 Agent 页面编辑,Pipeline 仅支持消息事件。',
eventBindings: '事件路由',
addEventBinding: '添加路由',
addBehavior: '添加行为',
behaviorReplyMessages: '回复收到的消息',
behaviorReplyMessagesDescription:
'把收到的消息交给 Agent 或 Pipeline 处理。',
behaviorWelcomeMembers: '欢迎新成员',
behaviorWelcomeMembersDescription: '有人加入群组时运行 Agent。',
behaviorHandleDepartures: '处理成员离群',
behaviorHandleDeparturesDescription: '有人离开或被移出群组时运行 Agent。',
behaviorReviewFriendRequests: '审核好友请求',
behaviorReviewFriendRequestsDescription:
'让 Agent 决定如何处理新的好友请求。',
behaviorHandleModeration: '处理群管理事件',
behaviorHandleModerationDescription: '群成员受到限制时运行 Agent。',
behaviorCustom: '配置其他事件',
behaviorCustomDescription: '添加路由,并从此适配器支持的全部事件中选择。',
eventPattern: '事件',
eventPatternPlaceholder: '选择事件',
targetType: '目标类型',