mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-17 01:46:07 +00:00
feat(agent-platform): add scenario-based route creation
This commit is contained in:
@@ -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}
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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: 'ターゲットタイプ',
|
||||||
|
|||||||
@@ -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: '目标类型',
|
||||||
|
|||||||
Reference in New Issue
Block a user