fix(agent): support String and Object Invoke parameters (#21953)

This commit is contained in:
Sbaaoui Idriss authored and GitHub committed 2026-11-04 15:06:12 +08:00
1 parent 06d8605d4c
commit 6aeb92deff
6 files changed
+171 -22

No files matched your search

+12
View File
@@ -230,6 +230,18 @@ func (i *InvokeComponent) Invoke(ctx context.Context, db *gorm.DB, inputs map[st
}
}
}
if variable["type"] == "object" {
if text, ok := value.(string); ok {
var object map[string]any
if err := json.Unmarshal([]byte(text), &object); err != nil || object == nil {
return nil, fmt.Errorf("Invoke: parameter %q must be a valid JSON object", key)
}
value = object
}
if object, ok := value.(map[string]any); !ok || object == nil {
return nil, fmt.Errorf("Invoke: parameter %q must be a valid JSON object", key)
}
}
fields[key] = value
}
if len(fields) > 0 {
+76
View File
@@ -18,6 +18,7 @@ package component
import (
"context"
"encoding/json"
"fmt"
"io"
"net"
@@ -699,3 +700,78 @@ func TestInvoke_ProxyRejectsHostnameTarget(t *testing.T) {
t.Errorf("hostname+proxy target: _ERROR = %q, want %q", got, "URL not valid")
}
}
func TestInvoke_ParameterTypes(t *testing.T) {
setupAllowAnyHost(t, true)
for _, tc := range []struct {
name, kind string
value any
invalid bool
}{
{"legacy", "", `{"nested":{"items":[1,true]}}`, false},
{"string", "string", `{"nested":{"items":[1,true]}}`, false},
{"object", "object", `{"nested":{"items":[1,true]}}`, false},
{"native", "object", map[string]any{"nested": map[string]any{"items": []any{float64(1), true}}}, false},
{"invalid", "object", `{`, true},
{"array", "object", `[]`, true},
{"null", "object", `null`, true},
} {
t.Run(tc.name, func(t *testing.T) {
for _, ref := range []string{"", "source@result"} {
var captured any
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var body map[string]any
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
t.Error(err)
}
captured = body["input"]
w.Write([]byte(`ok`))
}))
c, err := NewInvokeComponent(map[string]any{"url": srv.URL, "method": "POST", "variables": []any{map[string]any{"key": "input", "type": tc.kind, "value": tc.value, "ref": ref}}})
if err != nil {
t.Fatal(err)
}
_, err = c.Invoke(t.Context(), nil, map[string]any{"source@result": tc.value})
srv.Close()
if tc.invalid {
if err == nil || !strings.Contains(err.Error(), `parameter "input" must be a valid JSON object`) {
t.Fatalf("error = %v", err)
}
continue
}
if err != nil {
t.Fatal(err)
}
expected := tc.value
if tc.kind == "object" {
expected = map[string]any{"nested": map[string]any{"items": []any{float64(1), true}}}
}
if !reflect.DeepEqual(captured, expected) {
t.Fatalf("wire value = %#v, want %#v", captured, expected)
}
}
})
}
}
func TestInvoke_ExplicitBodyIgnoresParameterTypes(t *testing.T) {
setupAllowAnyHost(t, true)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
if string(body) != `{"explicit":true}` {
t.Errorf("body = %s", body)
}
w.Write([]byte("ok"))
}))
defer srv.Close()
c, err := NewInvokeComponent(map[string]any{
"url": srv.URL, "method": "PUT", "body": `{"explicit":true}`,
"variables": []any{map[string]any{"key": "ignored", "type": "object", "value": "invalid JSON"}},
})
if err != nil {
t.Fatal(err)
}
if _, err := c.Invoke(t.Context(), nil, nil); err != nil {
t.Fatal(err)
}
}
+5
View File
@@ -515,6 +515,11 @@ export const initialCrawlerValues = {
query: '',
};
export const InvokeParameterTypeOptions = [
{ label: 'String', value: 'string' },
{ label: 'Object', value: 'object' },
];
export const initialInvokeValues = {
url: '',
method: 'GET',
+28 -5
View File
@@ -1,10 +1,33 @@
import { z } from 'zod';
export const VariableFormSchema = z.object({
key: z.string(),
ref: z.string(),
value: z.string(),
});
export const VariableFormSchema = z
.object({
key: z.string(),
ref: z.string(),
value: z.string(),
type: z.enum(['string', 'object']).optional(),
})
.superRefine(({ type, ref, value }, context) => {
if (type !== 'object' || ref) return;
try {
const object = JSON.parse(value);
if (
object !== null &&
typeof object === 'object' &&
!Array.isArray(object)
)
return;
} catch {
// Report malformed JSON and non-object JSON with the same field error.
}
context.addIssue({
code: z.ZodIssueCode.custom,
path: ['value'],
message: 'Enter a valid JSON object.',
});
});
// {user_id} or {component@variable}
const placeholderRegex = /\{([a-zA-Z_][a-zA-Z0-9_.@-]*)\}/g;
@@ -1,4 +1,5 @@
import { Button } from '@/components/ui/button';
import { SelectWithSearch } from '@/components/originui/select-with-search';
import {
Dialog,
DialogContent,
@@ -15,15 +16,17 @@ import {
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { IModalProps } from '@/interfaces/common';
import { zodResolver } from '@hookform/resolvers/zod';
import { isEmpty } from 'lodash';
import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { useForm, useWatch } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';
import { InvokeParameterTypeOptions } from '../../constant';
import { QueryVariable } from '../components/query-variable';
import { VariableFormSchemaType } from './schema';
import { VariableFormSchema, VariableFormSchemaType } from './schema';
type ModalFormProps = {
initialValue: VariableFormSchemaType;
@@ -39,18 +42,17 @@ function VariableForm({
submit,
}: ModalFormProps) {
const { t } = useTranslation();
const FormSchema = z.object({
key: z
.string()
.trim()
.min(1)
.refine(
(value) =>
!value || !otherThanCurrentQuery.some((x) => x.key === value),
{ message: 'The key cannot be repeated!' },
),
ref: z.string(),
value: z.string(),
const FormSchema = VariableFormSchema.superRefine(({ key }, context) => {
if (
!key.trim() ||
otherThanCurrentQuery.some((x) => x.key === key.trim())
) {
context.addIssue({
code: z.ZodIssueCode.custom,
path: ['key'],
message: !key.trim() ? 'Required' : 'The key cannot be repeated!',
});
}
});
const form = useForm<z.infer<typeof FormSchema>>({
@@ -60,19 +62,22 @@ function VariableForm({
key: '',
value: '',
ref: '',
type: 'string',
},
});
useEffect(() => {
if (!isEmpty(initialValue)) {
form.reset(initialValue);
form.reset({ type: 'string', ...initialValue });
}
}, [form, initialValue]);
function onSubmit(data: z.infer<typeof FormSchema>) {
submit(data);
submit({ ...data, key: data.key.trim() });
}
const parameterType = useWatch({ control: form.control, name: 'type' });
return (
<Form {...form}>
<form
@@ -95,6 +100,23 @@ function VariableForm({
)}
/>
<QueryVariable name="ref" label={t('flow.ref')}></QueryVariable>
<FormField
name="type"
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>{t('flow.type')}</FormLabel>
<FormControl>
<SelectWithSearch
value={field.value ?? 'string'}
onChange={field.onChange}
options={InvokeParameterTypeOptions}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
name="value"
control={form.control}
@@ -102,7 +124,11 @@ function VariableForm({
<FormItem>
<FormLabel>{t('flow.value')}</FormLabel>
<FormControl>
<Input {...field} />
{parameterType === 'object' ? (
<Textarea {...field} className="font-mono" rows={5} />
) : (
<Input {...field} />
)}
</FormControl>
<FormMessage />
</FormItem>
@@ -76,6 +76,13 @@ export function VariableTable({
);
},
},
{
accessorKey: 'type',
header: t('flow.type'),
cell: ({ row }) => (
<div>{row.original.type === 'object' ? 'Object' : 'String'}</div>
),
},
{
accessorKey: 'ref',
header: t('flow.ref'),