mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-24 17:35:19 +08:00
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:
co-authored by
multica-agent
parent
f8e32ef14f
commit
d13ae56ea5
@@ -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),
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user