fix: stabilize dynamic forms and mcp testing

This commit is contained in:
huanghuoguoguo
2026-05-17 10:25:08 +08:00
parent cc3e9090b2
commit 03cbe8043d
6 changed files with 57 additions and 25 deletions
+9 -1
View File
@@ -262,6 +262,14 @@ class AgentRunnerRegistry:
stages = [] stages = []
for descriptor in runners: for descriptor in runners:
config_schema = []
for index, config_item in enumerate(descriptor.config_schema):
item = dict(config_item)
if not item.get('id'):
item_name = item.get('name') or str(index)
item['id'] = f'{descriptor.id}.{item_name}'
config_schema.append(item)
# Add runner option # Add runner option
options.append( options.append(
{ {
@@ -278,7 +286,7 @@ class AgentRunnerRegistry:
'name': descriptor.id, 'name': descriptor.id,
'label': descriptor.label, 'label': descriptor.label,
'description': descriptor.description, 'description': descriptor.description,
'config': descriptor.config_schema, 'config': config_schema,
} }
) )
@@ -502,12 +502,13 @@ export default function DynamicFormComponent({
}} }}
/> />
{itemConfigList.map((config) => { {itemConfigList.map((config, index) => {
// Create a normalized config with type converted to frontend format // Create a normalized config with type converted to frontend format
const normalizedConfig = { const normalizedConfig = {
...config, ...config,
type: normalizeItemType(config.type), type: normalizeItemType(config.type),
}; };
const fieldKey = config.id || config.name || `field-${index}`;
if (config.show_if) { if (config.show_if) {
const dependValue = resolveShowIfValue( const dependValue = resolveShowIfValue(
@@ -596,7 +597,7 @@ export default function DynamicFormComponent({
return ( return (
<WebhookUrlField <WebhookUrlField
key={config.id} key={fieldKey}
label={extractI18nObject(config.label)} label={extractI18nObject(config.label)}
description={ description={
config.description config.description
@@ -627,7 +628,7 @@ export default function DynamicFormComponent({
return ( return (
<EmbedCodeField <EmbedCodeField
key={config.id} key={fieldKey}
label={extractI18nObject(config.label)} label={extractI18nObject(config.label)}
description={ description={
config.description config.description
@@ -642,7 +643,7 @@ export default function DynamicFormComponent({
// QR code login button (e.g. Feishu one-click create, WeChat scan login) // QR code login button (e.g. Feishu one-click create, WeChat scan login)
if (config.type === 'qr-code-login') { if (config.type === 'qr-code-login') {
return ( return (
<FormItem key={config.id}> <FormItem key={fieldKey}>
<div <div
className="relative flex items-center gap-4 p-4 rounded-xl border-2 border-dashed cursor-pointer transition-all hover:border-solid hover:shadow-md group" className="relative flex items-center gap-4 p-4 rounded-xl border-2 border-dashed cursor-pointer transition-all hover:border-solid hover:shadow-md group"
style={{ style={{
@@ -703,7 +704,7 @@ export default function DynamicFormComponent({
if (normalizedConfig.type === 'boolean') { if (normalizedConfig.type === 'boolean') {
return ( return (
<FormField <FormField
key={config.id} key={fieldKey}
control={form.control} control={form.control}
name={config.name as keyof FormValues} name={config.name as keyof FormValues}
render={({ field }) => ( render={({ field }) => (
@@ -742,7 +743,7 @@ export default function DynamicFormComponent({
return ( return (
<FormField <FormField
key={config.id} key={fieldKey}
control={form.control} control={form.control}
name={config.name as keyof FormValues} name={config.name as keyof FormValues}
render={({ field }) => ( render={({ field }) => (
@@ -253,6 +253,7 @@ export default function DynamicFormItemComponent({
type="number" type="number"
className="w-full max-w-xs" className="w-full max-w-xs"
{...field} {...field}
value={field.value ?? ''}
onChange={(e) => field.onChange(Number(e.target.value))} onChange={(e) => field.onChange(Number(e.target.value))}
/> />
); );
@@ -261,7 +262,11 @@ export default function DynamicFormItemComponent({
if (config.options && config.options.length > 0) { if (config.options && config.options.length > 0) {
return ( return (
<div className="flex w-full max-w-md min-w-0 items-center gap-1.5"> <div className="flex w-full max-w-md min-w-0 items-center gap-1.5">
<Input className="min-w-0 flex-1" {...field} /> <Input
className="min-w-0 flex-1"
{...field}
value={field.value ?? ''}
/>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button <Button
@@ -292,12 +297,19 @@ export default function DynamicFormItemComponent({
</div> </div>
); );
} }
return <Input className="w-full max-w-md" {...field} />; return (
<Input
className="w-full max-w-md"
{...field}
value={field.value ?? ''}
/>
);
case DynamicFormItemType.TEXT: case DynamicFormItemType.TEXT:
return ( return (
<Textarea <Textarea
{...field} {...field}
value={field.value ?? ''}
className="min-h-[120px] w-full max-w-full resize-y overflow-x-hidden break-all" className="min-h-[120px] w-full max-w-full resize-y overflow-x-hidden break-all"
/> />
); );
@@ -306,13 +318,16 @@ export default function DynamicFormItemComponent({
return ( return (
<Textarea <Textarea
{...field} {...field}
value={field.value ?? ''}
className="min-h-[200px] font-mono text-sm" className="min-h-[200px] font-mono text-sm"
placeholder='{"key": "value"}' placeholder='{"key": "value"}'
/> />
); );
case DynamicFormItemType.BOOLEAN: case DynamicFormItemType.BOOLEAN:
return <Switch checked={field.value} onCheckedChange={field.onChange} />; return (
<Switch checked={!!field.value} onCheckedChange={field.onChange} />
);
case DynamicFormItemType.STRING_ARRAY: case DynamicFormItemType.STRING_ARRAY:
return ( return (
@@ -1430,7 +1445,7 @@ export default function DynamicFormItemComponent({
{/* 内容输入 */} {/* 内容输入 */}
<Textarea <Textarea
className="min-h-20 w-full min-w-0 flex-1 resize-y overflow-x-hidden break-all sm:w-[300px]" className="min-h-20 w-full min-w-0 flex-1 resize-y overflow-x-hidden break-all sm:w-[300px]"
value={item.content} value={item.content ?? ''}
onChange={(e) => { onChange={(e) => {
const newValue = [...(field.value ?? promptItems)]; const newValue = [...(field.value ?? promptItems)];
newValue[index] = { newValue[index] = {
@@ -310,6 +310,7 @@ function SingleSelectField({
{options.map((opt) => ( {options.map((opt) => (
<div key={opt.id}> <div key={opt.id}>
<button <button
type="button"
onClick={() => onChange(opt.id)} onClick={() => onChange(opt.id)}
className={`w-full text-left text-sm px-3 py-2 rounded-lg border transition-colors ${ className={`w-full text-left text-sm px-3 py-2 rounded-lg border transition-colors ${
value === opt.id value === opt.id
@@ -361,8 +362,16 @@ function MultiSelectField({
const selected = value.includes(opt.id); const selected = value.includes(opt.id);
return ( return (
<div key={opt.id}> <div key={opt.id}>
<button <div
role="button"
tabIndex={0}
onClick={() => toggle(opt.id)} onClick={() => toggle(opt.id)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
toggle(opt.id);
}
}}
className={`w-full text-left text-sm px-3 py-2 rounded-lg border transition-colors flex items-center gap-2 ${ className={`w-full text-left text-sm px-3 py-2 rounded-lg border transition-colors flex items-center gap-2 ${
selected selected
? 'border-primary bg-primary/5 text-primary' ? 'border-primary bg-primary/5 text-primary'
@@ -371,7 +380,7 @@ function MultiSelectField({
> >
<Checkbox checked={selected} className="pointer-events-none" /> <Checkbox checked={selected} className="pointer-events-none" />
{getI18nText(opt.label)} {getI18nText(opt.label)}
</button> </div>
{opt.has_input && selected && ( {opt.has_input && selected && (
<input <input
type="text" type="text"
@@ -464,14 +464,11 @@ const MCPForm = forwardRef<MCPFormHandle, MCPFormProps>(function MCPForm(
onRuntimeInfoChange?.(runtimeInfo); onRuntimeInfoChange?.(runtimeInfo);
}, [onRuntimeInfoChange, runtimeInfo]); }, [onRuntimeInfoChange, runtimeInfo]);
useImperativeHandle( // Expose test action and testing state to parent
ref, useImperativeHandle(ref, () => ({
() => ({ testMcp: () => testMcp(),
testMcp: () => testMcp(), isTesting: mcpTesting,
isTesting: mcpTesting, }));
}),
[mcpTesting],
);
useEffect(() => { useEffect(() => {
isInitializing.current = true; isInitializing.current = true;
@@ -169,6 +169,8 @@ export default function PipelineFormComponent({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
defaultValues: { defaultValues: {
basic: { basic: {
name: '',
description: '',
emoji: '⚙️', emoji: '⚙️',
}, },
ai: {}, ai: {},
@@ -221,8 +223,8 @@ export default function PipelineFormComponent({
const loadedValues = { const loadedValues = {
basic: { basic: {
name: resp.pipeline.name, name: resp.pipeline.name ?? '',
description: resp.pipeline.description, description: resp.pipeline.description ?? '',
emoji: resp.pipeline.emoji || '⚙️', emoji: resp.pipeline.emoji || '⚙️',
}, },
ai: resp.pipeline.config.ai, ai: resp.pipeline.config.ai,
@@ -601,7 +603,7 @@ export default function PipelineFormComponent({
<span className="text-destructive">*</span> <span className="text-destructive">*</span>
</FormLabel> </FormLabel>
<FormControl> <FormControl>
<Input {...field} /> <Input {...field} value={field.value ?? ''} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -632,7 +634,7 @@ export default function PipelineFormComponent({
<FormItem> <FormItem>
<FormLabel>{t('common.description')}</FormLabel> <FormLabel>{t('common.description')}</FormLabel>
<FormControl> <FormControl>
<Input {...field} /> <Input {...field} value={field.value ?? ''} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>