Update Copy to cURL buttons to reference the new Runs API spec (#3765)
Co-authored-by: Shuchang Zheng <wintonzheng0325@gmail.com>
This commit is contained in:
@@ -21,9 +21,10 @@ import { KeyValueInput } from "@/components/KeyValueInput";
|
|||||||
import { useApiCredential } from "@/hooks/useApiCredential";
|
import { useApiCredential } from "@/hooks/useApiCredential";
|
||||||
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
||||||
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
|
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
|
||||||
import { apiBaseUrl } from "@/util/env";
|
import { runsApiBaseUrl } from "@/util/env";
|
||||||
import { CopyApiCommandDropdown } from "@/components/CopyApiCommandDropdown";
|
import { CopyApiCommandDropdown } from "@/components/CopyApiCommandDropdown";
|
||||||
import { type ApiCommandOptions } from "@/util/apiCommands";
|
import { type ApiCommandOptions } from "@/util/apiCommands";
|
||||||
|
import { buildTaskRunPayload } from "@/util/taskRunPayload";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { PlayIcon, ReloadIcon } from "@radix-ui/react-icons";
|
import { PlayIcon, ReloadIcon } from "@radix-ui/react-icons";
|
||||||
import { ToastAction } from "@radix-ui/react-toast";
|
import { ToastAction } from "@radix-ui/react-toast";
|
||||||
@@ -754,17 +755,30 @@ function CreateNewTaskForm({ initialValues }: Props) {
|
|||||||
|
|
||||||
<div className="flex justify-end gap-3">
|
<div className="flex justify-end gap-3">
|
||||||
<CopyApiCommandDropdown
|
<CopyApiCommandDropdown
|
||||||
getOptions={() =>
|
getOptions={() => {
|
||||||
({
|
const formValues = form.getValues();
|
||||||
|
const includeOverrideHeader =
|
||||||
|
formValues.maxStepsOverride !== null &&
|
||||||
|
formValues.maxStepsOverride !== MAX_STEPS_DEFAULT;
|
||||||
|
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"x-api-key": apiCredential ?? "<your-api-key>",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (includeOverrideHeader) {
|
||||||
|
headers["x-max-steps-override"] = String(
|
||||||
|
formValues.maxStepsOverride,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: `${apiBaseUrl}/tasks`,
|
url: `${runsApiBaseUrl}/run/tasks`,
|
||||||
body: createTaskRequestObject(form.getValues()),
|
body: buildTaskRunPayload(createTaskRequestObject(formValues)),
|
||||||
headers: {
|
headers,
|
||||||
"Content-Type": "application/json",
|
} satisfies ApiCommandOptions;
|
||||||
"x-api-key": apiCredential ?? "<your-api-key>",
|
}}
|
||||||
},
|
|
||||||
}) satisfies ApiCommandOptions
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<Button type="submit" disabled={mutation.isPending}>
|
<Button type="submit" disabled={mutation.isPending}>
|
||||||
{mutation.isPending && (
|
{mutation.isPending && (
|
||||||
|
|||||||
@@ -16,9 +16,10 @@ import { useApiCredential } from "@/hooks/useApiCredential";
|
|||||||
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
||||||
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
|
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
|
||||||
import { SubmitEvent } from "@/types";
|
import { SubmitEvent } from "@/types";
|
||||||
import { apiBaseUrl } from "@/util/env";
|
import { runsApiBaseUrl } from "@/util/env";
|
||||||
import { CopyApiCommandDropdown } from "@/components/CopyApiCommandDropdown";
|
import { CopyApiCommandDropdown } from "@/components/CopyApiCommandDropdown";
|
||||||
import { type ApiCommandOptions } from "@/util/apiCommands";
|
import { type ApiCommandOptions } from "@/util/apiCommands";
|
||||||
|
import { buildTaskRunPayload } from "@/util/taskRunPayload";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { PlayIcon, ReloadIcon } from "@radix-ui/react-icons";
|
import { PlayIcon, ReloadIcon } from "@radix-ui/react-icons";
|
||||||
import { ToastAction } from "@radix-ui/react-toast";
|
import { ToastAction } from "@radix-ui/react-toast";
|
||||||
@@ -31,7 +32,11 @@ import { stringify as convertToYAML } from "yaml";
|
|||||||
import { MAX_STEPS_DEFAULT } from "../constants";
|
import { MAX_STEPS_DEFAULT } from "../constants";
|
||||||
import { TaskFormSection } from "./TaskFormSection";
|
import { TaskFormSection } from "./TaskFormSection";
|
||||||
import { savedTaskFormSchema, SavedTaskFormValues } from "./taskFormTypes";
|
import { savedTaskFormSchema, SavedTaskFormValues } from "./taskFormTypes";
|
||||||
import { OrganizationApiResponse, ProxyLocation } from "@/api/types";
|
import {
|
||||||
|
CreateTaskRequest,
|
||||||
|
OrganizationApiResponse,
|
||||||
|
ProxyLocation,
|
||||||
|
} from "@/api/types";
|
||||||
import { ProxySelector } from "@/components/ProxySelector";
|
import { ProxySelector } from "@/components/ProxySelector";
|
||||||
import { TestWebhookDialog } from "@/components/TestWebhookDialog";
|
import { TestWebhookDialog } from "@/components/TestWebhookDialog";
|
||||||
|
|
||||||
@@ -39,11 +44,13 @@ type Props = {
|
|||||||
initialValues: SavedTaskFormValues;
|
initialValues: SavedTaskFormValues;
|
||||||
};
|
};
|
||||||
|
|
||||||
function transform(value: unknown) {
|
function transform<T>(value: T): T | null {
|
||||||
return value === "" ? null : value;
|
return value === "" ? null : value;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createTaskRequestObject(formValues: SavedTaskFormValues) {
|
function createTaskRequestObject(
|
||||||
|
formValues: SavedTaskFormValues,
|
||||||
|
): CreateTaskRequest {
|
||||||
return {
|
return {
|
||||||
title: formValues.title,
|
title: formValues.title,
|
||||||
url: formValues.url,
|
url: formValues.url,
|
||||||
@@ -759,17 +766,30 @@ function SavedTaskForm({ initialValues }: Props) {
|
|||||||
|
|
||||||
<div className="flex justify-end gap-3">
|
<div className="flex justify-end gap-3">
|
||||||
<CopyApiCommandDropdown
|
<CopyApiCommandDropdown
|
||||||
getOptions={() =>
|
getOptions={() => {
|
||||||
({
|
const formValues = form.getValues();
|
||||||
|
const includeOverrideHeader =
|
||||||
|
formValues.maxStepsOverride !== null &&
|
||||||
|
formValues.maxStepsOverride !== MAX_STEPS_DEFAULT;
|
||||||
|
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"x-api-key": apiCredential ?? "<your-api-key>",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (includeOverrideHeader) {
|
||||||
|
headers["x-max-steps-override"] = String(
|
||||||
|
formValues.maxStepsOverride,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: `${apiBaseUrl}/tasks`,
|
url: `${runsApiBaseUrl}/run/tasks`,
|
||||||
body: createTaskRequestObject(form.getValues()),
|
body: buildTaskRunPayload(createTaskRequestObject(formValues)),
|
||||||
headers: {
|
headers,
|
||||||
"Content-Type": "application/json",
|
} satisfies ApiCommandOptions;
|
||||||
"x-api-key": apiCredential ?? "<your-api-key>",
|
}}
|
||||||
},
|
|
||||||
}) satisfies ApiCommandOptions
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|||||||
@@ -25,14 +25,16 @@ import { useApiCredential } from "@/hooks/useApiCredential";
|
|||||||
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
||||||
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
|
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
|
||||||
import { WorkflowApiResponse } from "@/routes/workflows/types/workflowTypes";
|
import { WorkflowApiResponse } from "@/routes/workflows/types/workflowTypes";
|
||||||
import { apiBaseUrl } from "@/util/env";
|
import { runsApiBaseUrl } from "@/util/env";
|
||||||
import { ApiWebhookActionsMenu } from "@/components/ApiWebhookActionsMenu";
|
import { ApiWebhookActionsMenu } from "@/components/ApiWebhookActionsMenu";
|
||||||
import { WebhookReplayDialog } from "@/components/WebhookReplayDialog";
|
import { WebhookReplayDialog } from "@/components/WebhookReplayDialog";
|
||||||
import { type ApiCommandOptions } from "@/util/apiCommands";
|
import { type ApiCommandOptions } from "@/util/apiCommands";
|
||||||
|
import { buildTaskRunPayload } from "@/util/taskRunPayload";
|
||||||
import { PlayIcon, ReloadIcon } from "@radix-ui/react-icons";
|
import { PlayIcon, ReloadIcon } from "@radix-ui/react-icons";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { Link, Outlet, useParams } from "react-router-dom";
|
import { Link, Outlet, useParams } from "react-router-dom";
|
||||||
import { statusIsFinalized } from "../types";
|
import { statusIsFinalized } from "../types";
|
||||||
|
import { MAX_STEPS_DEFAULT } from "../constants";
|
||||||
import { useTaskQuery } from "./hooks/useTaskQuery";
|
import { useTaskQuery } from "./hooks/useTaskQuery";
|
||||||
|
|
||||||
function createTaskRequestObject(values: TaskApiResponse) {
|
function createTaskRequestObject(values: TaskApiResponse) {
|
||||||
@@ -206,14 +208,27 @@ function TaskDetails() {
|
|||||||
},
|
},
|
||||||
} satisfies ApiCommandOptions;
|
} satisfies ApiCommandOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const includeOverrideHeader =
|
||||||
|
task.max_steps_per_run !== null &&
|
||||||
|
task.max_steps_per_run !== MAX_STEPS_DEFAULT;
|
||||||
|
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"x-api-key": apiCredential ?? "<your-api-key>",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (includeOverrideHeader) {
|
||||||
|
headers["x-max-steps-override"] = String(
|
||||||
|
task.max_steps_per_run,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: `${apiBaseUrl}/tasks`,
|
url: `${runsApiBaseUrl}/run/tasks`,
|
||||||
body: createTaskRequestObject(task),
|
body: buildTaskRunPayload(createTaskRequestObject(task)),
|
||||||
headers: {
|
headers,
|
||||||
"Content-Type": "application/json",
|
|
||||||
"x-api-key": apiCredential ?? "<your-api-key>",
|
|
||||||
},
|
|
||||||
} satisfies ApiCommandOptions;
|
} satisfies ApiCommandOptions;
|
||||||
}}
|
}}
|
||||||
webhookDisabled={taskIsLoading || !taskHasTerminalState}
|
webhookDisabled={taskIsLoading || !taskHasTerminalState}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ import { useBlockScriptsQuery } from "@/routes/workflows/hooks/useBlockScriptsQu
|
|||||||
import { constructCacheKeyValueFromParameters } from "@/routes/workflows/editor/utils";
|
import { constructCacheKeyValueFromParameters } from "@/routes/workflows/editor/utils";
|
||||||
import { useWorkflowQuery } from "@/routes/workflows/hooks/useWorkflowQuery";
|
import { useWorkflowQuery } from "@/routes/workflows/hooks/useWorkflowQuery";
|
||||||
import { type ApiCommandOptions } from "@/util/apiCommands";
|
import { type ApiCommandOptions } from "@/util/apiCommands";
|
||||||
import { apiBaseUrl } from "@/util/env";
|
import { runsApiBaseUrl } from "@/util/env";
|
||||||
|
|
||||||
import { MAX_SCREENSHOT_SCROLLS_DEFAULT } from "./editor/nodes/Taskv2Node/types";
|
import { MAX_SCREENSHOT_SCROLLS_DEFAULT } from "./editor/nodes/Taskv2Node/types";
|
||||||
import { getLabelForWorkflowParameterType } from "./editor/workflowEditorUtils";
|
import { getLabelForWorkflowParameterType } from "./editor/workflowEditorUtils";
|
||||||
@@ -167,6 +167,25 @@ function getRunWorkflowRequestBody(
|
|||||||
return body;
|
return body;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Transform RunWorkflowRequestBody to match WorkflowRunRequest schema for Runs API v2
|
||||||
|
function transformToWorkflowRunRequest(
|
||||||
|
body: RunWorkflowRequestBody,
|
||||||
|
workflowId: string,
|
||||||
|
) {
|
||||||
|
const { data, webhook_callback_url, ...rest } = body;
|
||||||
|
const transformed: Record<string, unknown> = {
|
||||||
|
workflow_id: workflowId,
|
||||||
|
parameters: data,
|
||||||
|
...rest,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (webhook_callback_url) {
|
||||||
|
transformed.webhook_url = webhook_callback_url;
|
||||||
|
}
|
||||||
|
|
||||||
|
return transformed;
|
||||||
|
}
|
||||||
|
|
||||||
type RunWorkflowFormType = Record<string, unknown> & {
|
type RunWorkflowFormType = Record<string, unknown> & {
|
||||||
webhookCallbackUrl: string;
|
webhookCallbackUrl: string;
|
||||||
proxyLocation: ProxyLocation;
|
proxyLocation: ProxyLocation;
|
||||||
@@ -807,14 +826,22 @@ function RunWorkflowForm({
|
|||||||
values,
|
values,
|
||||||
workflowParameters,
|
workflowParameters,
|
||||||
);
|
);
|
||||||
|
const transformedBody = transformToWorkflowRunRequest(
|
||||||
|
body,
|
||||||
|
workflowPermanentId,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Build headers - x-max-steps-override is optional and can be added manually if needed
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"x-api-key": apiCredential ?? "<your-api-key>",
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: `${apiBaseUrl}/workflows/${workflowPermanentId}/run`,
|
url: `${runsApiBaseUrl}/run/workflows`,
|
||||||
body,
|
body: transformedBody,
|
||||||
headers: {
|
headers,
|
||||||
"Content-Type": "application/json",
|
|
||||||
"x-api-key": apiCredential ?? "<your-api-key>",
|
|
||||||
},
|
|
||||||
} satisfies ApiCommandOptions;
|
} satisfies ApiCommandOptions;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { Skeleton } from "@/components/ui/skeleton";
|
|||||||
import { toast } from "@/components/ui/use-toast";
|
import { toast } from "@/components/ui/use-toast";
|
||||||
import { useApiCredential } from "@/hooks/useApiCredential";
|
import { useApiCredential } from "@/hooks/useApiCredential";
|
||||||
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
||||||
import { apiBaseUrl } from "@/util/env";
|
import { runsApiBaseUrl } from "@/util/env";
|
||||||
import {
|
import {
|
||||||
CodeIcon,
|
CodeIcon,
|
||||||
FileIcon,
|
FileIcon,
|
||||||
@@ -312,20 +312,34 @@ function WorkflowRun() {
|
|||||||
|
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<ApiWebhookActionsMenu
|
<ApiWebhookActionsMenu
|
||||||
getOptions={() =>
|
getOptions={() => {
|
||||||
({
|
// Build headers - x-max-steps-override is optional and can be added manually if needed
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"x-api-key": apiCredential ?? "<your-api-key>",
|
||||||
|
};
|
||||||
|
|
||||||
|
const body: Record<string, unknown> = {
|
||||||
|
workflow_id: workflowPermanentId,
|
||||||
|
parameters: workflowRun?.parameters,
|
||||||
|
proxy_location: proxyLocation,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (maxScreenshotScrolls !== null) {
|
||||||
|
body.max_screenshot_scrolls = maxScreenshotScrolls;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (workflowRun?.webhook_callback_url) {
|
||||||
|
body.webhook_url = workflowRun.webhook_callback_url;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: `${apiBaseUrl}/workflows/${workflowPermanentId}/run`,
|
url: `${runsApiBaseUrl}/run/workflows`,
|
||||||
body: {
|
body,
|
||||||
data: workflowRun?.parameters,
|
headers,
|
||||||
proxy_location: "RESIDENTIAL",
|
} satisfies ApiCommandOptions;
|
||||||
},
|
}}
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"x-api-key": apiCredential ?? "<your-api-key>",
|
|
||||||
},
|
|
||||||
}) satisfies ApiCommandOptions
|
|
||||||
}
|
|
||||||
webhookDisabled={workflowRunIsLoading || !workflowRunIsFinalized}
|
webhookDisabled={workflowRunIsLoading || !workflowRunIsFinalized}
|
||||||
onTestWebhook={() => setReplayOpen(true)}
|
onTestWebhook={() => setReplayOpen(true)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -43,6 +43,19 @@ try {
|
|||||||
newWssBaseUrl = wssBaseUrl.replace("/api", "");
|
newWssBaseUrl = wssBaseUrl.replace("/api", "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Base URL for the Runs API (strip a leading `/api` segment: /api/v1 -> /v1)
|
||||||
|
const runsApiBaseUrl = (() => {
|
||||||
|
try {
|
||||||
|
const url = new URL(apiBaseUrl);
|
||||||
|
if (url.pathname.startsWith("/api")) {
|
||||||
|
url.pathname = url.pathname.replace(/^\/api/, "");
|
||||||
|
}
|
||||||
|
return `${url.origin}${url.pathname}`;
|
||||||
|
} catch (e) {
|
||||||
|
return apiBaseUrl.replace("/api", "");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
let runtimeApiKey: string | null | undefined;
|
let runtimeApiKey: string | null | undefined;
|
||||||
|
|
||||||
function readPersistedApiKey(): string | null {
|
function readPersistedApiKey(): string | null {
|
||||||
@@ -83,6 +96,7 @@ function clearRuntimeApiKey(): void {
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
apiBaseUrl,
|
apiBaseUrl,
|
||||||
|
runsApiBaseUrl,
|
||||||
environment,
|
environment,
|
||||||
artifactApiBaseUrl,
|
artifactApiBaseUrl,
|
||||||
apiPathPrefix,
|
apiPathPrefix,
|
||||||
|
|||||||
74
skyvern-frontend/src/util/taskRunPayload.test.ts
Normal file
74
skyvern-frontend/src/util/taskRunPayload.test.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import type { CreateTaskRequest } from "../api/types";
|
||||||
|
import { buildTaskRunPayload } from "./taskRunPayload";
|
||||||
|
|
||||||
|
describe("buildTaskRunPayload", () => {
|
||||||
|
it("maps v1 task fields into the Runs API payload shape", () => {
|
||||||
|
const request: CreateTaskRequest = {
|
||||||
|
title: " Test Task ",
|
||||||
|
url: " https://example.com/task ",
|
||||||
|
navigation_goal: "Navigate somewhere",
|
||||||
|
data_extraction_goal: "Collect some data",
|
||||||
|
webhook_callback_url: " https://callback.example.com ",
|
||||||
|
proxy_location: "RESIDENTIAL",
|
||||||
|
extracted_information_schema: { foo: "bar" },
|
||||||
|
error_code_mapping: { ERR42: "Meaningful message" },
|
||||||
|
extra_http_headers: { "X-Trace-Id": "abc123" },
|
||||||
|
totp_identifier: " identifier ",
|
||||||
|
browser_address: " chrome:1234 ",
|
||||||
|
include_action_history_in_verification: true,
|
||||||
|
max_screenshot_scrolls: 7,
|
||||||
|
};
|
||||||
|
|
||||||
|
const payload = buildTaskRunPayload(request);
|
||||||
|
|
||||||
|
expect(payload).toEqual({
|
||||||
|
prompt: "Navigate somewhere\n\nCollect some data",
|
||||||
|
url: "https://example.com/task",
|
||||||
|
proxy_location: "RESIDENTIAL",
|
||||||
|
data_extraction_schema: { foo: "bar" },
|
||||||
|
error_code_mapping: { ERR42: "Meaningful message" },
|
||||||
|
extra_http_headers: { "X-Trace-Id": "abc123" },
|
||||||
|
webhook_url: "https://callback.example.com",
|
||||||
|
totp_identifier: "identifier",
|
||||||
|
browser_address: "chrome:1234",
|
||||||
|
include_action_history_in_verification: true,
|
||||||
|
max_screenshot_scrolls: 7,
|
||||||
|
title: "Test Task",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("applies prompt fallbacks and drops invalid optional objects", () => {
|
||||||
|
const request: CreateTaskRequest = {
|
||||||
|
title: " ",
|
||||||
|
url: " https://fallback.example.com ",
|
||||||
|
navigation_goal: "",
|
||||||
|
data_extraction_goal: null,
|
||||||
|
webhook_callback_url: " ",
|
||||||
|
proxy_location: null,
|
||||||
|
extracted_information_schema: null,
|
||||||
|
extra_http_headers: "not an object" as unknown as Record<string, string>,
|
||||||
|
error_code_mapping: [] as unknown as Record<string, string>,
|
||||||
|
totp_identifier: null,
|
||||||
|
browser_address: " ",
|
||||||
|
include_action_history_in_verification: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const payload = buildTaskRunPayload(request);
|
||||||
|
|
||||||
|
expect(payload).toEqual({
|
||||||
|
prompt: "https://fallback.example.com",
|
||||||
|
url: "https://fallback.example.com",
|
||||||
|
proxy_location: null,
|
||||||
|
data_extraction_schema: null,
|
||||||
|
webhook_url: undefined,
|
||||||
|
totp_identifier: undefined,
|
||||||
|
browser_address: undefined,
|
||||||
|
include_action_history_in_verification: null,
|
||||||
|
max_screenshot_scrolls: undefined,
|
||||||
|
title: undefined,
|
||||||
|
extra_http_headers: undefined,
|
||||||
|
error_code_mapping: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
79
skyvern-frontend/src/util/taskRunPayload.ts
Normal file
79
skyvern-frontend/src/util/taskRunPayload.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import type { CreateTaskRequest, ProxyLocation } from "@/api/types";
|
||||||
|
|
||||||
|
type TaskRunPayload = {
|
||||||
|
prompt: string;
|
||||||
|
url?: string | null;
|
||||||
|
proxy_location?: ProxyLocation | null;
|
||||||
|
data_extraction_schema?: CreateTaskRequest["extracted_information_schema"];
|
||||||
|
error_code_mapping?: Record<string, string> | null;
|
||||||
|
extra_http_headers?: Record<string, string> | null;
|
||||||
|
webhook_url?: string | null;
|
||||||
|
totp_identifier?: string | null;
|
||||||
|
browser_address?: string | null;
|
||||||
|
include_action_history_in_verification?: boolean | null;
|
||||||
|
max_screenshot_scrolls?: number | null;
|
||||||
|
title?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper to trim and check for empty strings
|
||||||
|
const trim = (s: string | null | undefined): string | undefined => {
|
||||||
|
const t = s?.trim();
|
||||||
|
return t && t.length > 0 ? t : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Build prompt from navigation_goal + data_extraction_goal
|
||||||
|
function buildPrompt(request: CreateTaskRequest): string {
|
||||||
|
const nav = trim(request.navigation_goal);
|
||||||
|
const extract = trim(request.data_extraction_goal);
|
||||||
|
|
||||||
|
const parts = [nav, extract].filter(Boolean);
|
||||||
|
if (parts.length > 0) return parts.join("\n\n");
|
||||||
|
|
||||||
|
// Fallback chain: try title, then goals again, then url, then default
|
||||||
|
return (
|
||||||
|
trim(request.title) ??
|
||||||
|
nav ??
|
||||||
|
extract ??
|
||||||
|
trim(request.url) ??
|
||||||
|
"Task run triggered from Skyvern UI"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidRecord(val: unknown): val is Record<string, string> {
|
||||||
|
return val != null && typeof val === "object" && !Array.isArray(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms a CreateTaskRequest (old schema) to TaskRunPayload (Runs API v2 schema).
|
||||||
|
*
|
||||||
|
* Key transformations:
|
||||||
|
* - navigation_goal + data_extraction_goal → prompt (combined)
|
||||||
|
* - extracted_information_schema → data_extraction_schema
|
||||||
|
* - webhook_callback_url → webhook_url
|
||||||
|
*
|
||||||
|
* Note: max_steps is optional and can be added manually to the cURL if needed.
|
||||||
|
*/
|
||||||
|
function buildTaskRunPayload(request: CreateTaskRequest): TaskRunPayload {
|
||||||
|
return {
|
||||||
|
prompt: buildPrompt(request),
|
||||||
|
url: trim(request.url) ?? null,
|
||||||
|
proxy_location: request.proxy_location ?? null,
|
||||||
|
data_extraction_schema: request.extracted_information_schema,
|
||||||
|
webhook_url: trim(request.webhook_callback_url),
|
||||||
|
totp_identifier: trim(request.totp_identifier),
|
||||||
|
browser_address: trim(request.browser_address),
|
||||||
|
include_action_history_in_verification:
|
||||||
|
request.include_action_history_in_verification,
|
||||||
|
max_screenshot_scrolls: request.max_screenshot_scrolls,
|
||||||
|
title: trim(request.title),
|
||||||
|
extra_http_headers: isValidRecord(request.extra_http_headers)
|
||||||
|
? request.extra_http_headers
|
||||||
|
: undefined,
|
||||||
|
error_code_mapping: isValidRecord(request.error_code_mapping)
|
||||||
|
? request.error_code_mapping
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type { TaskRunPayload };
|
||||||
|
export { buildTaskRunPayload };
|
||||||
Reference in New Issue
Block a user