mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-11 01:36:37 +08:00
修复任务参数类型转换
This commit is contained in:
@@ -11,6 +11,7 @@ import {Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, Di
|
|||||||
import {Calendar as CalendarIcon, Clock, Info, Layers, Play} from "lucide-react"
|
import {Calendar as CalendarIcon, Clock, Info, Layers, Play} from "lucide-react"
|
||||||
|
|
||||||
import {AdminService, DispatchTaskRequest, TaskMeta} from "@/lib/services"
|
import {AdminService, DispatchTaskRequest, TaskMeta} from "@/lib/services"
|
||||||
|
import {buildTaskPayload} from "@/lib/task-param-utils"
|
||||||
import {ErrorInline} from "@/components/layout/error"
|
import {ErrorInline} from "@/components/layout/error"
|
||||||
import {LoadingStateWithBorder} from "@/components/layout/loading"
|
import {LoadingStateWithBorder} from "@/components/layout/loading"
|
||||||
import {EmptyStateWithBorder} from "@/components/layout/empty"
|
import {EmptyStateWithBorder} from "@/components/layout/empty"
|
||||||
@@ -195,19 +196,16 @@ export function TaskManager() {
|
|||||||
if (userId) params.user_id = userId
|
if (userId) params.user_id = userId
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle dynamic parameters
|
// Handle dynamic parameters — type coercion (number vs string) is
|
||||||
|
// centralised in buildTaskPayload; do not inline it here.
|
||||||
if (targetTask?.params && targetTask.params.length > 0) {
|
if (targetTask?.params && targetTask.params.length > 0) {
|
||||||
const payloadData: Record<string, string> = {}
|
const { payload, error } = buildTaskPayload(targetTask.params, paramValues)
|
||||||
for (const param of targetTask.params) {
|
if (error) {
|
||||||
const val = (paramValues[param.name] || "").trim()
|
toast.error(error)
|
||||||
if (param.required && !val) {
|
setDispatching(false)
|
||||||
toast.error(`${param.label}不能为空`)
|
return
|
||||||
setDispatching(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
payloadData[param.name] = val
|
|
||||||
}
|
}
|
||||||
params.payload = JSON.stringify(payloadData)
|
params.payload = payload ?? undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const taskID = await AdminService.dispatchTask(params)
|
const taskID = await AdminService.dispatchTask(params)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from "@/c
|
|||||||
import {Clock, Edit2, Info, Plus, RefreshCw, Trash2} from "lucide-react"
|
import {Clock, Edit2, Info, Plus, RefreshCw, Trash2} from "lucide-react"
|
||||||
|
|
||||||
import {AdminService, CreateScheduleRequest, Schedule, TaskMeta, UpdateScheduleRequest} from "@/lib/services"
|
import {AdminService, CreateScheduleRequest, Schedule, TaskMeta, UpdateScheduleRequest} from "@/lib/services"
|
||||||
|
import {buildTaskPayload} from "@/lib/task-param-utils"
|
||||||
import {ErrorInline} from "@/components/layout/error"
|
import {ErrorInline} from "@/components/layout/error"
|
||||||
import {LoadingStateWithBorder} from "@/components/layout/loading"
|
import {LoadingStateWithBorder} from "@/components/layout/loading"
|
||||||
import {EmptyStateWithBorder} from "@/components/layout/empty"
|
import {EmptyStateWithBorder} from "@/components/layout/empty"
|
||||||
@@ -146,22 +147,18 @@ export function TaskSchedulesManager() {
|
|||||||
setSubmitLoading(true)
|
setSubmitLoading(true)
|
||||||
|
|
||||||
const targetTask = taskTypes.find(t => t.type === selectedTaskType)
|
const targetTask = taskTypes.find(t => t.type === selectedTaskType)
|
||||||
const payloadData: Record<string, string> = {}
|
let payloadStr = '{}'
|
||||||
|
|
||||||
if (targetTask?.params) {
|
if (targetTask?.params && targetTask.params.length > 0) {
|
||||||
for (const param of targetTask.params) {
|
const { payload, error } = buildTaskPayload(targetTask.params, paramValues)
|
||||||
const val = (paramValues[param.name] || "").trim()
|
if (error) {
|
||||||
if (param.required && !val) {
|
toast.error(error)
|
||||||
toast.error(`${param.label}不能为空`)
|
setSubmitLoading(false)
|
||||||
setSubmitLoading(false)
|
return
|
||||||
return
|
|
||||||
}
|
|
||||||
payloadData[param.name] = val
|
|
||||||
}
|
}
|
||||||
|
payloadStr = payload ?? '{}'
|
||||||
}
|
}
|
||||||
|
|
||||||
const payloadStr = JSON.stringify(payloadData)
|
|
||||||
|
|
||||||
if (editingSchedule) {
|
if (editingSchedule) {
|
||||||
const req: UpdateScheduleRequest = {
|
const req: UpdateScheduleRequest = {
|
||||||
name,
|
name,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import type {
|
|||||||
SystemStatus,
|
SystemStatus,
|
||||||
TaskExecution,
|
TaskExecution,
|
||||||
TaskMeta,
|
TaskMeta,
|
||||||
|
TaskParamType,
|
||||||
TaskTypeResponse,
|
TaskTypeResponse,
|
||||||
Template,
|
Template,
|
||||||
ToggleAuthSourceRequest,
|
ToggleAuthSourceRequest,
|
||||||
@@ -187,7 +188,9 @@ export class AdminService extends BaseService {
|
|||||||
params: (item.Params || item.params || []).map(p => ({
|
params: (item.Params || item.params || []).map(p => ({
|
||||||
name: p.Name || p.name || '',
|
name: p.Name || p.name || '',
|
||||||
label: p.Label || p.label || '',
|
label: p.Label || p.label || '',
|
||||||
type: p.Type || p.type || '',
|
// Cast to TaskParamType: backend only emits 'string' | 'text' | 'number'.
|
||||||
|
// Unknown future values fall through safely to the string branch.
|
||||||
|
type: (p.Type || p.type || 'string') as TaskParamType,
|
||||||
required: p.Required ?? p.required ?? false,
|
required: p.Required ?? p.required ?? false,
|
||||||
placeholder: p.Placeholder || p.placeholder || '',
|
placeholder: p.Placeholder || p.placeholder || '',
|
||||||
description: p.Description || p.description || '',
|
description: p.Description || p.description || '',
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ export type {
|
|||||||
AuthSourceRequest,
|
AuthSourceRequest,
|
||||||
ToggleAuthSourceRequest,
|
ToggleAuthSourceRequest,
|
||||||
TaskMeta,
|
TaskMeta,
|
||||||
|
TaskParam,
|
||||||
|
TaskParamType,
|
||||||
TaskExecution,
|
TaskExecution,
|
||||||
TaskExecutionStatus,
|
TaskExecutionStatus,
|
||||||
ListTaskExecutionsRequest,
|
ListTaskExecutionsRequest,
|
||||||
|
|||||||
@@ -92,13 +92,21 @@ export interface TaskTypeResponse {
|
|||||||
params?: TaskParamResponse[];
|
params?: TaskParamResponse[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务参数的数据类型
|
||||||
|
* - string → 单行文本输入,JSON 中序列化为 string
|
||||||
|
* - text → 多行文本输入,JSON 中序列化为 string
|
||||||
|
* - number → 数字输入,JSON 中序列化为 number(而非 string)
|
||||||
|
*/
|
||||||
|
export type TaskParamType = 'string' | 'text' | 'number';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 任务参数定义
|
* 任务参数定义
|
||||||
*/
|
*/
|
||||||
export interface TaskParam {
|
export interface TaskParam {
|
||||||
name: string;
|
name: string;
|
||||||
label: string;
|
label: string;
|
||||||
type: string;
|
type: TaskParamType;
|
||||||
required: boolean;
|
required: boolean;
|
||||||
placeholder: string;
|
placeholder: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
|||||||
@@ -99,6 +99,8 @@ export type {
|
|||||||
CreateUserRequest,
|
CreateUserRequest,
|
||||||
UpdateSystemConfigRequest,
|
UpdateSystemConfigRequest,
|
||||||
TaskMeta,
|
TaskMeta,
|
||||||
|
TaskParam,
|
||||||
|
TaskParamType,
|
||||||
TaskExecution,
|
TaskExecution,
|
||||||
TaskExecutionStatus,
|
TaskExecutionStatus,
|
||||||
ListTaskExecutionsRequest,
|
ListTaskExecutionsRequest,
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
/**
|
||||||
|
* task-param-utils.ts
|
||||||
|
*
|
||||||
|
* 统一的任务参数 Payload 构建工具。
|
||||||
|
*
|
||||||
|
* 设计原则:
|
||||||
|
* - 所有 Task Param 的类型转换和空值过滤在这里集中处理,
|
||||||
|
* 组件只负责收集原始字符串输入值。
|
||||||
|
* - 新增参数类型时,只需在 coerceParamValue 中增加一个 case。
|
||||||
|
* - 不引入任何框架/UI 依赖,纯工具函数。
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type {TaskParam} from '@/lib/services/admin/types';
|
||||||
|
|
||||||
|
/** buildTaskPayload 的返回结构 */
|
||||||
|
export interface BuildPayloadResult {
|
||||||
|
/** 验证通过时返回序列化好的 JSON 字符串;失败时为 null */
|
||||||
|
payload: string | null;
|
||||||
|
/** 验证失败时返回人类可读的错误信息;成功时为 null */
|
||||||
|
error: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 TaskParam 的 type 声明,将原始字符串输入值转换为正确的 JSON 值类型。
|
||||||
|
*
|
||||||
|
* 规则:
|
||||||
|
* - 'number' → 转为 JS number;空字符串视为「未填写」,返回 undefined(由调用方决定是否跳过)
|
||||||
|
* - 'string' / 'text' → 保持字符串,原样返回
|
||||||
|
*
|
||||||
|
* @returns 转换后的值,或 `undefined` 表示该参数应当从 payload 中省略
|
||||||
|
*/
|
||||||
|
function coerceParamValue(
|
||||||
|
param: TaskParam,
|
||||||
|
rawValue: string,
|
||||||
|
): { value: string | number; ok: true } | { value: undefined; ok: true } | { error: string; ok: false } {
|
||||||
|
const trimmed = rawValue.trim();
|
||||||
|
|
||||||
|
switch (param.type) {
|
||||||
|
case 'number': {
|
||||||
|
if (trimmed === '') {
|
||||||
|
// 空值 → 省略该字段(对 omitempty 字段友好)
|
||||||
|
return { value: undefined, ok: true };
|
||||||
|
}
|
||||||
|
const num = Number(trimmed);
|
||||||
|
if (Number.isNaN(num)) {
|
||||||
|
return { error: `「${param.label}」必须是有效的数字`, ok: false };
|
||||||
|
}
|
||||||
|
return { value: num, ok: true };
|
||||||
|
}
|
||||||
|
case 'string':
|
||||||
|
case 'text':
|
||||||
|
default:
|
||||||
|
// 字符串类型:空值也保留(空字符串是合法的字符串值)
|
||||||
|
return { value: trimmed, ok: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将表单收集的原始参数值(全为 string)构建为类型正确的 JSON payload 字符串。
|
||||||
|
*
|
||||||
|
* 行为:
|
||||||
|
* - `number` 类型参数:序列化为 JSON number,而非 JSON string。
|
||||||
|
* - `number` 类型参数为空且非必填:从 payload 中省略该字段(不发送),
|
||||||
|
* 避免后端收到 `"target_type":""` 再尝试解析为 int 时报错。
|
||||||
|
* - `required` 字段为空:返回 error。
|
||||||
|
*
|
||||||
|
* @param params 任务的参数定义列表(来自 TaskMeta.params)
|
||||||
|
* @param values 表单收集的原始字符串值,key 为 param.name
|
||||||
|
* @returns BuildPayloadResult
|
||||||
|
*/
|
||||||
|
export function buildTaskPayload(
|
||||||
|
params: TaskParam[],
|
||||||
|
values: Record<string, string>,
|
||||||
|
): BuildPayloadResult {
|
||||||
|
const payloadData: Record<string, string | number> = {};
|
||||||
|
|
||||||
|
for (const param of params) {
|
||||||
|
const raw = values[param.name] ?? '';
|
||||||
|
|
||||||
|
// 必填项空值检查
|
||||||
|
if (param.required && raw.trim() === '') {
|
||||||
|
return { payload: null, error: `「${param.label}」不能为空` };
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = coerceParamValue(param, raw);
|
||||||
|
|
||||||
|
if (!result.ok) {
|
||||||
|
return { payload: null, error: result.error };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.value !== undefined) {
|
||||||
|
// 有值(包括空字符串)才写入
|
||||||
|
payloadData[param.name] = result.value;
|
||||||
|
}
|
||||||
|
// undefined → 省略该字段(number 类型的空值)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { payload: JSON.stringify(payloadData), error: null };
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user