fix: bot routing setup and busy-session response checks (#2728)

Apply group response rules before busy-session steering. Initialize usable private-message and group-mention routes for new bots, including Wizard-created pipelines, and preserve member-configured rules across saves and retries.

Fix empty pipeline configuration, refresh the processor sidebar after automatic creation, and show creation feedback. Align routing regressions and use Node 24 for the browser CI job.

Correct the task's erroneous agent attribution while preserving every code tree and the original human contribution.

Co-authored-by: multica-agent <[email protected]>
This commit is contained in:
RockChinQ
2026-10-20 21:22:53 +08:00
committed by GitHub
co-authored by multica-agent
parent f8e32ef14f
commit d13ae56ea5
35 changed files with 1064 additions and 81 deletions
@@ -23,6 +23,7 @@ import { UUID } from 'uuidjs';
import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent';
import { httpClient } from '@/app/infra/http/HttpClient';
import { systemInfo } from '@/app/infra/http';
import { useSidebarData } from '@/app/home/components/home-sidebar/SidebarDataContext';
import { Agent, Bot } from '@/app/infra/entities/api';
import { getAdapterDocUrl } from '@/app/infra/entities/adapter-docs';
import {
@@ -220,6 +221,7 @@ const BotForm = forwardRef<BotFormHandle, BotFormProps>(function BotForm(
ref,
) {
const { t } = useTranslation();
const { refreshPipelines } = useSidebarData();
const formSchema = getFormSchema(t);
const form = useForm<z.infer<typeof formSchema>>({
@@ -668,8 +670,30 @@ const BotForm = forwardRef<BotFormHandle, BotFormProps>(function BotForm(
};
try {
// A failed metadata refresh must not prevent creating the bot.
const before = await httpClient.getPipelines().catch(() => null);
const res = await httpClient.createBot(newBot);
toast.success(t('bots.createSuccess'));
const [created] = await Promise.all([
httpClient.getBot(res.uuid).catch(() => null),
refreshPipelines(),
]);
const automaticallyCreatedPipeline =
before !== null &&
created?.bot.event_bindings?.some(
(binding) =>
binding.event_pattern === 'message.received' &&
binding.target_type === 'pipeline' &&
!before.pipelines.some(
(pipeline) => pipeline.uuid === binding.target_uuid,
),
);
toast.success(
t(
automaticallyCreatedPipeline
? 'bots.autoCreatedPipeline'
: 'bots.createSuccess',
),
);
onNewBotCreated(res.uuid);
} catch (error) {
const err = error as CustomApiError & {
@@ -12,6 +12,7 @@ import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import { cn } from '@/lib/utils';
import styles from './EventBindingsEditor.module.css';
import { appendMessageRoutingTemplate } from './message-routing-template';
import type { TFunction } from 'i18next';
import { UseFormReturn } from 'react-hook-form';
import {
@@ -144,17 +145,22 @@ interface EventBindingsEditorProps {
}
type FilterField =
'chat_type' | 'chat_id' | 'message_text' | 'message_element_types';
| 'chat_type'
| 'chat_id'
| 'message_text'
| 'message_element_types'
| 'is_at_bot';
type FilterOperator =
'eq' | 'neq' | 'contains' | 'not_contains' | 'starts_with' | 'regex';
interface FilterRow {
field: FilterField;
operator: FilterOperator;
value: string;
value: string | boolean;
}
const OPERATORS_BY_FIELD: Record<FilterField, FilterOperator[]> = {
is_at_bot: ['eq', 'neq'],
chat_type: ['eq', 'neq'],
chat_id: ['eq', 'neq', 'contains', 'not_contains', 'regex'],
message_text: [
@@ -443,7 +449,7 @@ function samplePayloadForEvent(eventType: string): Record<string, unknown> {
if (eventType === 'message.received') {
return {
message_text: 'Hello',
chat_type: 'private',
chat_type: 'person',
chat_id: 'test-user',
user_id: 'test-user',
};
@@ -727,7 +733,7 @@ function FilterConditionsPanel({
next[i] = { ...next[i], ...patch };
if ('field' in patch) {
next[i].operator = OPERATORS_BY_FIELD[patch.field as FilterField][0];
next[i].value = '';
next[i].value = patch.field === 'is_at_bot' ? true : '';
}
onChange(next);
}
@@ -755,6 +761,9 @@ function FilterConditionsPanel({
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="is_at_bot">
{t('bots.filterIsAtBot')}
</SelectItem>
<SelectItem value="chat_type">
{t('bots.filterChatType')}
</SelectItem>
@@ -784,9 +793,22 @@ function FilterConditionsPanel({
</SelectContent>
</Select>
{f.field === 'chat_type' ? (
{f.field === 'is_at_bot' ? (
<Select
value={f.value}
value={String(f.value)}
onValueChange={(v) => update(i, { value: v === 'true' })}
>
<SelectTrigger className="h-8 w-[90px] text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="true">{t('bots.mentionYes')}</SelectItem>
<SelectItem value="false">{t('bots.mentionNo')}</SelectItem>
</SelectContent>
</Select>
) : f.field === 'chat_type' ? (
<Select
value={String(f.value)}
onValueChange={(v) => update(i, { value: v })}
>
<SelectTrigger className="h-8 w-[90px] text-xs">
@@ -803,7 +825,7 @@ function FilterConditionsPanel({
</Select>
) : f.field === 'message_element_types' ? (
<Select
value={f.value}
value={String(f.value)}
onValueChange={(v) => update(i, { value: v })}
>
<SelectTrigger className="h-8 w-[100px] text-xs">
@@ -825,7 +847,7 @@ function FilterConditionsPanel({
? t('bots.ruleValueLauncherIdPlaceholder')
: t('bots.ruleValueMessagePlaceholder')
}
value={f.value}
value={String(f.value)}
onChange={(e) => update(i, { value: e.target.value })}
/>
)}
@@ -1600,6 +1622,18 @@ export default function EventBindingsEditor({
}
function addBinding(eventPattern = 'message.received') {
if (eventPattern === 'message.received') {
const target =
agentOptions.find((agent) => agent.kind === 'pipeline') ??
agentOptions.find((agent) => agent.kind === 'agent');
updateBindings(
appendMessageRoutingTemplate(bindings, {
target_type: target?.kind === 'pipeline' ? 'pipeline' : 'agent',
target_uuid: target?.uuid ?? '',
}),
);
return;
}
updateBindings([
...bindings,
{
@@ -0,0 +1,53 @@
import type { EventBinding } from '@/app/infra/entities/api';
/** Add explicit private/group routes, preserving all existing user routes. */
export function appendMessageRoutingTemplate(
bindings: EventBinding[],
target: Pick<EventBinding, 'target_type' | 'target_uuid'>,
): EventBinding[] {
const routes: EventBinding[] = [
{
event_pattern: 'message.received',
...target,
priority: 0,
enabled: true,
filters: [{ field: 'chat_type', operator: 'eq', value: 'person' }],
},
{
event_pattern: 'message.received',
...target,
priority: 0,
enabled: true,
filters: [
{ field: 'chat_type', operator: 'eq', value: 'group' },
{ field: 'is_at_bot', operator: 'eq', value: true },
],
},
];
const sameFilters = (a: EventBinding, b: EventBinding) => {
const left = a.filters ?? [];
const right = b.filters ?? [];
return (
left.length === right.length &&
left.every((filter) =>
right.some(
(other) =>
filter.field === other.field &&
filter.operator === other.operator &&
filter.value === other.value,
),
)
);
};
return [
...bindings,
...routes.filter(
(route) =>
!bindings.some(
(binding) =>
binding.event_pattern === route.event_pattern &&
sameFilters(binding, route),
),
),
];
}
+64
View File
@@ -0,0 +1,64 @@
import type { Bot } from '@/app/infra/entities/api';
import { appendMessageRoutingTemplate } from '@/app/home/bots/components/bot-form/message-routing-template';
export interface WizardDraft {
adapter: string;
botUuid: string | null;
pipelineUuid: string | null;
}
interface SetupClient {
getBot(uuid: string): Promise<{ bot: Bot }>;
createBot(bot: Bot): Promise<{ uuid: string }>;
createPipeline(pipeline: {
name: string;
description: string;
config: Record<string, unknown>;
}): Promise<{ uuid: string }>;
}
/** Keep successful resources in the draft so a retry never recreates them. */
export async function prepareWizardBot(
client: SetupClient,
draft: WizardDraft,
bot: Bot,
pipelineName: string,
) {
if (draft.botUuid) {
return {
uuid: draft.botUuid,
bot: (await client.getBot(draft.botUuid)).bot,
};
}
if (!draft.pipelineUuid) {
draft.pipelineUuid = (
await client.createPipeline({
name: pipelineName,
description: bot.description ?? '',
config: {},
})
).uuid;
}
const configuredBot: Bot = {
...bot,
event_bindings: appendMessageRoutingTemplate([], {
target_type: 'pipeline',
target_uuid: draft.pipelineUuid,
}),
};
const { uuid } = await client.createBot(configuredBot);
draft.botUuid = uuid;
return { uuid, bot: configuredBot };
}
/** Saving adapter settings must not replace or re-enable a member's routes. */
export function wizardBindingsIfMissing(bot: Bot, pipelineUuid: string) {
return bot.event_bindings?.length
? {}
: {
event_bindings: appendMessageRoutingTemplate([], {
target_type: 'pipeline',
target_uuid: pipelineUuid,
}),
};
}
+48 -19
View File
@@ -81,6 +81,11 @@ import {
import OwnModelSetup, {
type OwnModelSelection,
} from '@/app/wizard/components/OwnModelSetup';
import {
prepareWizardBot,
wizardBindingsIfMissing,
type WizardDraft,
} from '@/app/wizard/bot-setup';
type RunnerInstallProgress = { stage: InstallStage; percent: number };
@@ -153,6 +158,8 @@ export default function WizardPage() {
);
const [runnerConfig, setRunnerConfig] = useState<Record<string, unknown>>({});
const [createdBotUuid, setCreatedBotUuid] = useState<string | null>(null);
const botDraft = useRef<WizardDraft | null>(null);
const botOperationPending = useRef(false);
const [createdPipelineUuid, setCreatedPipelineUuid] = useState<string | null>(
null,
);
@@ -699,10 +706,20 @@ export default function WizardPage() {
// Creates a disabled bot using the adapter label as name.
const handleCreateBot = useCallback(async () => {
if (!selectedAdapter) return;
if (!selectedAdapter || botOperationPending.current) return;
botOperationPending.current = true;
setIsCreatingBot(true);
try {
if (createdBotUuid) {
const existing = (await httpClient.getBot(createdBotUuid)).bot;
if (existing.adapter === selectedAdapter) {
// Returning to the platform step must retain unsaved form values too.
setCurrentStep(1);
saveProgress({ step: 1 });
return;
}
}
// Use adapter label as default bot name
const adapter = adapters.find((a) => a.name === selectedAdapter);
const defaultName = adapter
@@ -726,9 +743,25 @@ export default function WizardPage() {
adapter_config: initialConfig,
enable: false,
};
const resp = await httpClient.createBot(bot);
if (!botDraft.current || botDraft.current.adapter !== selectedAdapter) {
botDraft.current = {
adapter: selectedAdapter,
botUuid: null,
pipelineUuid: null,
};
}
const resp = await prepareWizardBot(
httpClient,
botDraft.current,
bot,
`${defaultName} ${t('agents.pipelineType')}`,
);
setBotName(resp.bot.name);
setAdapterConfig(
(resp.bot.adapter_config ?? initialConfig) as Record<string, unknown>,
);
setCreatedBotUuid(resp.uuid);
setCreatedPipelineUuid(null);
setCreatedPipelineUuid(botDraft.current.pipelineUuid);
// Fetch runtime info to get webhook URL(s)
try {
@@ -752,7 +785,7 @@ export default function WizardPage() {
selected_scenario: null,
selected_adapter: selectedAdapter,
created_bot_uuid: resp.uuid,
created_pipeline_uuid: null,
created_pipeline_uuid: botDraft.current.pipelineUuid,
bot_saved: false,
message_received: false,
selected_runner: null,
@@ -763,15 +796,18 @@ export default function WizardPage() {
t('wizard.createError') + (apiErr?.msg ? `: ${apiErr.msg}` : ''),
);
} finally {
botOperationPending.current = false;
setIsCreatingBot(false);
}
}, [selectedAdapter, adapters, t, saveProgress]);
}, [selectedAdapter, adapters, createdBotUuid, t, saveProgress]);
// ---- Save Bot Config & Enable (Step 1) ----
// Updates the bot's adapter config and enables it.
const handleSaveBot = useCallback(async () => {
if (!createdBotUuid || !selectedAdapter) return;
if (!createdBotUuid || !selectedAdapter || botOperationPending.current)
return;
botOperationPending.current = true;
setIsSavingBot(true);
setBotSaved(false);
let previewPipelineUuid = createdPipelineUuid;
@@ -871,19 +907,11 @@ export default function WizardPage() {
adapter_config: configToSave,
enable: true,
};
if (previewPipelineUuid) {
botUpdate.event_bindings = [
{
event_pattern: 'message.received',
target_type: 'pipeline',
target_uuid: previewPipelineUuid,
filters: [],
priority: 0,
enabled: true,
description: '',
},
];
}
const currentBot = (await httpClient.getBot(createdBotUuid)).bot;
Object.assign(
botUpdate,
wizardBindingsIfMissing(currentBot, previewPipelineUuid),
);
await httpClient.updateBot(createdBotUuid, botUpdate);
setDefaultPreparationProgress(100);
@@ -929,6 +957,7 @@ export default function WizardPage() {
}
toast.error(`${t('wizard.createError')}: ${getErrorMessage(err)}`);
} finally {
botOperationPending.current = false;
setPreparingDefault(false);
setIsSavingBot(false);
}