From a2b30957b65f7deb18e208e2ae2115372111fb40 Mon Sep 17 00:00:00 2001 From: huanghuoguoguo <60681390+huanghuoguoguo@users.noreply.github.com> Date: Sat, 11 Jul 2026 11:10:46 +0800 Subject: [PATCH] feat(agent-platform): add scenario-based route creation --- .../e2e/bot-event-routing-product-flow.mjs | 22 +++- .../cases/bot-event-routing-product-flow.yaml | 4 +- .../bot-form/EventBindingsEditor.tsx | 102 ++++++++++++++++-- web/src/i18n/locales/en-US.ts | 19 ++++ web/src/i18n/locales/ja-JP.ts | 19 ++++ web/src/i18n/locales/zh-Hans.ts | 15 +++ 6 files changed, 171 insertions(+), 10 deletions(-) diff --git a/skills/scripts/e2e/bot-event-routing-product-flow.mjs b/skills/scripts/e2e/bot-event-routing-product-flow.mjs index fe971cbd1..566b89f03 100644 --- a/skills/scripts/e2e/bot-event-routing-product-flow.mjs +++ b/skills/scripts/e2e/bot-event-routing-product-flow.mjs @@ -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", diff --git a/skills/skills/langbot-testing/cases/bot-event-routing-product-flow.yaml b/skills/skills/langbot-testing/cases/bot-event-routing-product-flow.yaml index 00373b454..4e8a6dbab 100644 --- a/skills/skills/langbot-testing/cases/bot-event-routing-product-flow.yaml +++ b/skills/skills/langbot-testing/cases/bot-event-routing-product-flow.yaml @@ -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." diff --git a/web/src/app/home/bots/components/bot-form/EventBindingsEditor.tsx b/web/src/app/home/bots/components/bot-form/EventBindingsEditor.tsx index df5f02a11..c114778c8 100644 --- a/web/src/app/home/bots/components/bot-form/EventBindingsEditor.tsx +++ b/web/src/app/home/bots/components/bot-form/EventBindingsEditor.tsx @@ -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({
- + + + + + + {behaviorPresets.map((preset) => { + const Icon = preset.icon; + return ( + addBinding(preset.eventType)} + > + + + {t(preset.labelKey)} + + {t(preset.descriptionKey)} + + + + ); + })} + + addBinding(dryRunEventOptions[0])} + > + + + {t('bots.behaviorCustom')} + + {t('bots.behaviorCustomDescription')} + + + + +