refactor: extract RoutingRulesEditor component, revert log levels to debug

- Extract ~250 lines of inline routing rules UI from BotForm into
  a dedicated RoutingRulesEditor component
- Revert stage interrupt and event prevented-default log levels
  from warning back to debug (these are normal flow, not errors)
- Remove message content from log lines to avoid leaking user data
This commit is contained in:
RockChinQ
2026-04-02 22:19:28 +08:00
parent eb633f8849
commit 98ccbf0f99
4 changed files with 273 additions and 311 deletions

View File

@@ -247,8 +247,8 @@ class RuntimePipeline:
await self._check_output(query, result) await self._check_output(query, result)
if result.result_type == pipeline_entities.ResultType.INTERRUPT: if result.result_type == pipeline_entities.ResultType.INTERRUPT:
self.ap.logger.warning( self.ap.logger.debug(
f'Stage {stage_container.inst_name} interrupted query {query.query_id}, message: {str(query.message_chain)[:100]}' f'Stage {stage_container.inst_name} interrupted query {query.query_id}'
) )
break break
elif result.result_type == pipeline_entities.ResultType.CONTINUE: elif result.result_type == pipeline_entities.ResultType.CONTINUE:
@@ -263,8 +263,8 @@ class RuntimePipeline:
await self._check_output(query, sub_result) await self._check_output(query, sub_result)
if sub_result.result_type == pipeline_entities.ResultType.INTERRUPT: if sub_result.result_type == pipeline_entities.ResultType.INTERRUPT:
self.ap.logger.warning( self.ap.logger.debug(
f'Stage {stage_container.inst_name} interrupted query {query.query_id}, message: {str(query.message_chain)[:100]}' f'Stage {stage_container.inst_name} interrupted query {query.query_id}'
) )
break break
elif sub_result.result_type == pipeline_entities.ResultType.CONTINUE: elif sub_result.result_type == pipeline_entities.ResultType.CONTINUE:
@@ -327,8 +327,8 @@ class RuntimePipeline:
event_ctx = await self.ap.plugin_connector.emit_event(event_obj, bound_plugins) event_ctx = await self.ap.plugin_connector.emit_event(event_obj, bound_plugins)
if event_ctx.is_prevented_default(): if event_ctx.is_prevented_default():
self.ap.logger.warning( self.ap.logger.debug(
f'MessageReceived event prevented default for query {query.query_id}, pipeline={pipeline_name}, message: {str(query.message_chain)[:100]}' f'MessageReceived event prevented default for query {query.query_id}, pipeline={pipeline_name}'
) )
return return

View File

@@ -61,8 +61,8 @@ class ChatMessageHandler(handler.MessageHandler):
yield entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) yield entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query)
else: else:
self.ap.logger.warning( self.ap.logger.debug(
f'NormalMessageReceived event prevented default for query {query.query_id} without reply, message: {str(query.message_chain)[:100]}' f'NormalMessageReceived event prevented default for query {query.query_id} without reply'
) )
yield entities.StageProcessResult(result_type=entities.ResultType.INTERRUPT, new_query=query) yield entities.StageProcessResult(result_type=entities.ResultType.INTERRUPT, new_query=query)
else: else:

View File

@@ -13,13 +13,10 @@ import { IDynamicFormItemSchema } from '@/app/infra/entities/form/dynamic';
import { UUID } from 'uuidjs'; import { UUID } from 'uuidjs';
import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent'; import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent';
import { httpClient } from '@/app/infra/http/HttpClient'; import { httpClient } from '@/app/infra/http/HttpClient';
import { import { Bot } from '@/app/infra/entities/api';
Bot,
PipelineRoutingRule,
RoutingRuleOperator,
} from '@/app/infra/entities/api';
import { getAdapterDocUrl } from '@/app/infra/entities/adapter-docs'; import { getAdapterDocUrl } from '@/app/infra/entities/adapter-docs';
import { ExternalLink, Plus, Trash2 } from 'lucide-react'; import { ExternalLink } from 'lucide-react';
import RoutingRulesEditor from './RoutingRulesEditor';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
@@ -492,303 +489,10 @@ export default function BotForm({
/> />
{/* Pipeline Routing Rules */} {/* Pipeline Routing Rules */}
<div className="mt-6"> <RoutingRulesEditor
<div className="flex items-center justify-between mb-2"> form={form}
<div> pipelineNameList={pipelineNameList}
<FormLabel>{t('bots.routingRules')}</FormLabel>
<p className="text-sm text-muted-foreground mt-1">
{t('bots.routingRulesDescription')}
</p>
</div>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => {
const rules =
form.getValues('pipeline_routing_rules') || [];
form.setValue(
'pipeline_routing_rules',
[
...rules,
{
type: 'launcher_type' as const,
operator: 'eq' as const,
value: '',
pipeline_uuid: '',
},
],
{ shouldDirty: true },
);
}}
>
<Plus className="h-4 w-4 mr-1" />
{t('bots.addRoutingRule')}
</Button>
</div>
{(form.watch('pipeline_routing_rules') || []).map(
(rule, index) => {
// Determine which operators are available for the current type
const operatorsForType: {
value: RoutingRuleOperator;
labelKey: string;
}[] =
rule.type === 'launcher_type'
? [
{ value: 'eq', labelKey: 'bots.operatorEq' },
{ value: 'neq', labelKey: 'bots.operatorNeq' },
]
: rule.type === 'launcher_id'
? [
{ value: 'eq', labelKey: 'bots.operatorEq' },
{ value: 'neq', labelKey: 'bots.operatorNeq' },
{
value: 'contains',
labelKey: 'bots.operatorContains',
},
{
value: 'not_contains',
labelKey: 'bots.operatorNotContains',
},
{
value: 'regex',
labelKey: 'bots.operatorRegex',
},
]
: [
{ value: 'eq', labelKey: 'bots.operatorEq' },
{ value: 'neq', labelKey: 'bots.operatorNeq' },
{
value: 'contains',
labelKey: 'bots.operatorContains',
},
{
value: 'not_contains',
labelKey: 'bots.operatorNotContains',
},
{
value: 'starts_with',
labelKey: 'bots.operatorStartsWith',
},
{
value: 'regex',
labelKey: 'bots.operatorRegex',
},
];
return (
<div
key={index}
className="flex items-center gap-2 mt-2 p-3 border rounded-md bg-muted/30"
>
{/* Field selector */}
<Select
value={rule.type}
onValueChange={(val) => {
const rules = [
...(form.getValues('pipeline_routing_rules') ||
[]),
];
const newType = val as PipelineRoutingRule['type'];
// Reset operator to 'eq' when switching type
rules[index] = {
...rules[index],
type: newType,
operator: 'eq',
value: '',
};
form.setValue('pipeline_routing_rules', rules, {
shouldDirty: true,
});
}}
>
<SelectTrigger className="w-[130px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="launcher_type">
{t('bots.ruleTypeLauncherType')}
</SelectItem>
<SelectItem value="launcher_id">
{t('bots.ruleTypeLauncherId')}
</SelectItem>
<SelectItem value="message_content">
{t('bots.ruleTypeMessageContent')}
</SelectItem>
</SelectContent>
</Select>
{/* Operator selector */}
<Select
value={rule.operator || 'eq'}
onValueChange={(val) => {
const rules = [
...(form.getValues('pipeline_routing_rules') ||
[]),
];
rules[index] = {
...rules[index],
operator: val as RoutingRuleOperator,
};
form.setValue('pipeline_routing_rules', rules, {
shouldDirty: true,
});
}}
>
<SelectTrigger className="w-[120px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
{operatorsForType.map((op) => (
<SelectItem key={op.value} value={op.value}>
{t(op.labelKey)}
</SelectItem>
))}
</SelectContent>
</Select>
{/* Value input */}
{rule.type === 'launcher_type' ? (
<Select
value={rule.value}
onValueChange={(val) => {
const rules = [
...(form.getValues('pipeline_routing_rules') ||
[]),
];
rules[index] = { ...rules[index], value: val };
form.setValue('pipeline_routing_rules', rules, {
shouldDirty: true,
});
}}
>
<SelectTrigger className="w-[100px]">
<SelectValue
placeholder={t('bots.ruleValuePlaceholder')}
/> />
</SelectTrigger>
<SelectContent>
<SelectItem value="person">
{t('bots.sessionTypePerson')}
</SelectItem>
<SelectItem value="group">
{t('bots.sessionTypeGroup')}
</SelectItem>
</SelectContent>
</Select>
) : (
<Input
className="flex-1"
placeholder={
rule.type === 'launcher_id'
? t('bots.ruleValueLauncherIdPlaceholder')
: rule.operator === 'regex'
? t('bots.ruleValueRegexpPlaceholder')
: t('bots.ruleValueMessagePlaceholder')
}
value={rule.value}
onChange={(e) => {
const rules = [
...(form.getValues('pipeline_routing_rules') ||
[]),
];
rules[index] = {
...rules[index],
value: e.target.value,
};
form.setValue('pipeline_routing_rules', rules, {
shouldDirty: true,
});
}}
/>
)}
<span className="text-sm text-muted-foreground shrink-0">
</span>
{/* Pipeline selector */}
<Select
value={rule.pipeline_uuid}
onValueChange={(val) => {
const rules = [
...(form.getValues('pipeline_routing_rules') ||
[]),
];
rules[index] = {
...rules[index],
pipeline_uuid: val,
};
form.setValue('pipeline_routing_rules', rules, {
shouldDirty: true,
});
}}
>
<SelectTrigger className="w-[200px]">
{rule.pipeline_uuid ? (
(() => {
const p = pipelineNameList.find(
(p) => p.value === rule.pipeline_uuid,
);
return (
<div className="flex items-center gap-2">
{p?.emoji && (
<span className="text-sm shrink-0">
{p.emoji}
</span>
)}
<span>
{p?.label ?? rule.pipeline_uuid}
</span>
</div>
);
})()
) : (
<SelectValue
placeholder={t('bots.selectPipeline')}
/>
)}
</SelectTrigger>
<SelectContent>
{pipelineNameList.map((item) => (
<SelectItem key={item.value} value={item.value}>
<div className="flex items-center gap-2">
{item.emoji && (
<span className="text-sm shrink-0">
{item.emoji}
</span>
)}
<span>{item.label}</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
<Button
type="button"
variant="ghost"
size="icon"
className="shrink-0"
onClick={() => {
const rules = [
...(form.getValues('pipeline_routing_rules') ||
[]),
];
rules.splice(index, 1);
form.setValue('pipeline_routing_rules', rules, {
shouldDirty: true,
});
}}
>
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
</div>
);
},
)}
</div>
</CardContent> </CardContent>
</Card> </Card>
)} )}

View File

@@ -0,0 +1,258 @@
'use client';
import { useTranslation } from 'react-i18next';
import { UseFormReturn } from 'react-hook-form';
import {
PipelineRoutingRule,
RoutingRuleOperator,
} from '@/app/infra/entities/api';
import { Plus, Trash2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { FormLabel } from '@/components/ui/form';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
interface PipelineOption {
value: string;
label: string;
emoji?: string;
}
interface RoutingRulesEditorProps {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
form: UseFormReturn<any>;
pipelineNameList: PipelineOption[];
}
const OPERATORS_BY_TYPE: Record<
PipelineRoutingRule['type'],
{ value: RoutingRuleOperator; labelKey: string }[]
> = {
launcher_type: [
{ value: 'eq', labelKey: 'bots.operatorEq' },
{ value: 'neq', labelKey: 'bots.operatorNeq' },
],
launcher_id: [
{ value: 'eq', labelKey: 'bots.operatorEq' },
{ value: 'neq', labelKey: 'bots.operatorNeq' },
{ value: 'contains', labelKey: 'bots.operatorContains' },
{ value: 'not_contains', labelKey: 'bots.operatorNotContains' },
{ value: 'regex', labelKey: 'bots.operatorRegex' },
],
message_content: [
{ value: 'eq', labelKey: 'bots.operatorEq' },
{ value: 'neq', labelKey: 'bots.operatorNeq' },
{ value: 'contains', labelKey: 'bots.operatorContains' },
{ value: 'not_contains', labelKey: 'bots.operatorNotContains' },
{ value: 'starts_with', labelKey: 'bots.operatorStartsWith' },
{ value: 'regex', labelKey: 'bots.operatorRegex' },
],
};
function getValuePlaceholder(
t: (key: string) => string,
rule: PipelineRoutingRule,
): string {
if (rule.type === 'launcher_id') return t('bots.ruleValueLauncherIdPlaceholder');
if (rule.operator === 'regex') return t('bots.ruleValueRegexpPlaceholder');
return t('bots.ruleValueMessagePlaceholder');
}
export default function RoutingRulesEditor({
form,
pipelineNameList,
}: RoutingRulesEditorProps) {
const { t } = useTranslation();
const rules: PipelineRoutingRule[] =
form.watch('pipeline_routing_rules') || [];
const updateRules = (newRules: PipelineRoutingRule[]) => {
form.setValue('pipeline_routing_rules', newRules, { shouldDirty: true });
};
const addRule = () => {
updateRules([
...rules,
{
type: 'launcher_type',
operator: 'eq',
value: '',
pipeline_uuid: '',
},
]);
};
const updateRule = (index: number, patch: Partial<PipelineRoutingRule>) => {
const updated = [...rules];
updated[index] = { ...updated[index], ...patch };
updateRules(updated);
};
const removeRule = (index: number) => {
const updated = [...rules];
updated.splice(index, 1);
updateRules(updated);
};
return (
<div className="mt-6">
<div className="flex items-center justify-between mb-2">
<div>
<FormLabel>{t('bots.routingRules')}</FormLabel>
<p className="text-sm text-muted-foreground mt-1">
{t('bots.routingRulesDescription')}
</p>
</div>
<Button type="button" variant="outline" size="sm" onClick={addRule}>
<Plus className="h-4 w-4 mr-1" />
{t('bots.addRoutingRule')}
</Button>
</div>
{rules.map((rule, index) => {
const operatorsForType = OPERATORS_BY_TYPE[rule.type] || OPERATORS_BY_TYPE.message_content;
return (
<div
key={index}
className="flex items-center gap-2 mt-2 p-3 border rounded-md bg-muted/30"
>
{/* Field selector */}
<Select
value={rule.type}
onValueChange={(val) => {
updateRule(index, {
type: val as PipelineRoutingRule['type'],
operator: 'eq',
value: '',
});
}}
>
<SelectTrigger className="w-[130px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="launcher_type">
{t('bots.ruleTypeLauncherType')}
</SelectItem>
<SelectItem value="launcher_id">
{t('bots.ruleTypeLauncherId')}
</SelectItem>
<SelectItem value="message_content">
{t('bots.ruleTypeMessageContent')}
</SelectItem>
</SelectContent>
</Select>
{/* Operator selector */}
<Select
value={rule.operator || 'eq'}
onValueChange={(val) => {
updateRule(index, { operator: val as RoutingRuleOperator });
}}
>
<SelectTrigger className="w-[120px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
{operatorsForType.map((op) => (
<SelectItem key={op.value} value={op.value}>
{t(op.labelKey)}
</SelectItem>
))}
</SelectContent>
</Select>
{/* Value input */}
{rule.type === 'launcher_type' ? (
<Select
value={rule.value}
onValueChange={(val) => updateRule(index, { value: val })}
>
<SelectTrigger className="w-[100px]">
<SelectValue
placeholder={t('bots.ruleValuePlaceholder')}
/>
</SelectTrigger>
<SelectContent>
<SelectItem value="person">
{t('bots.sessionTypePerson')}
</SelectItem>
<SelectItem value="group">
{t('bots.sessionTypeGroup')}
</SelectItem>
</SelectContent>
</Select>
) : (
<Input
className="flex-1"
placeholder={getValuePlaceholder(t, rule)}
value={rule.value}
onChange={(e) => updateRule(index, { value: e.target.value })}
/>
)}
<span className="text-sm text-muted-foreground shrink-0"></span>
{/* Pipeline selector */}
<Select
value={rule.pipeline_uuid}
onValueChange={(val) =>
updateRule(index, { pipeline_uuid: val })
}
>
<SelectTrigger className="w-[200px]">
{rule.pipeline_uuid ? (
(() => {
const p = pipelineNameList.find(
(p) => p.value === rule.pipeline_uuid,
);
return (
<div className="flex items-center gap-2">
{p?.emoji && (
<span className="text-sm shrink-0">{p.emoji}</span>
)}
<span>{p?.label ?? rule.pipeline_uuid}</span>
</div>
);
})()
) : (
<SelectValue placeholder={t('bots.selectPipeline')} />
)}
</SelectTrigger>
<SelectContent>
{pipelineNameList.map((item) => (
<SelectItem key={item.value} value={item.value}>
<div className="flex items-center gap-2">
{item.emoji && (
<span className="text-sm shrink-0">{item.emoji}</span>
)}
<span>{item.label}</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
<Button
type="button"
variant="ghost"
size="icon"
className="shrink-0"
onClick={() => removeRule(index)}
>
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
</div>
);
})}
</div>
);
}