feat: 拆分任务类型并支持脚本语言联动
- 任务类型改为「执行脚本 / 发起 HTTP 请求 / 运行二进制程序」,脚本类型下新增语言选择(Python / Shell / Node / Lua) - 语言联动表单:Python 才显示虚拟环境、解释器和 uv 状态;Shell/Node/Lua 隐藏这些字段并切换脚本标签、占位符和调用预览 - 切换语言时自动设置解释器与默认脚本,Shell 通过 cmd.exe /C 执行批处理,Node/Lua 使用对应解释器 - TaskAction 新增可选 language,旧任务按解释器推断;补 Shell 执行测试、语言切换单测与 Playwright 用例
This commit is contained in:
@@ -174,6 +174,36 @@ test('manages a task and reads a real run log', async ({ page, request }) => {
|
||||
await expect(page.getByText('还没有任务')).toBeVisible()
|
||||
})
|
||||
|
||||
test('switches script language and adapts the form fields', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.getByRole('button', { name: '+ 新建任务' }).click()
|
||||
const dialog = page.getByRole('dialog', { name: '新建定时任务' })
|
||||
await expect(dialog).toBeVisible()
|
||||
|
||||
await dialog.getByRole('button', { name: 'Python', exact: true }).click()
|
||||
await page.getByRole('option', { name: 'Shell (cmd)' }).click()
|
||||
await expect(dialog.getByText(/虚拟环境目录/)).toHaveCount(0)
|
||||
await expect(dialog.getByLabel('批处理文件或相对路径')).toHaveValue('run.bat')
|
||||
await expect(dialog.locator('.cli-line-cmd')).toContainText(
|
||||
'cmd.exe /C run.bat',
|
||||
)
|
||||
|
||||
await dialog.getByRole('button', { name: 'Shell (cmd)' }).click()
|
||||
await page.getByRole('option', { name: 'Node.js' }).click()
|
||||
await expect(dialog.getByLabel('JavaScript 文件或相对路径')).toHaveValue(
|
||||
'index.js',
|
||||
)
|
||||
await expect(dialog.locator('.cli-line-cmd')).toContainText(
|
||||
'node.exe index.js',
|
||||
)
|
||||
|
||||
await page.screenshot({
|
||||
path: 'test-results/dashboard-script-language.png',
|
||||
fullPage: true,
|
||||
})
|
||||
await dialog.getByRole('button', { name: '取消' }).click()
|
||||
})
|
||||
|
||||
test('keeps the dashboard usable on a narrow viewport', async ({
|
||||
page,
|
||||
request,
|
||||
|
||||
@@ -297,6 +297,20 @@
|
||||
grid-template-columns: minmax(0, 1.4fr) minmax(0, 0.9fr);
|
||||
}
|
||||
|
||||
.language-grid {
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.language-hint {
|
||||
display: flex;
|
||||
min-height: 36px;
|
||||
align-items: center;
|
||||
border: 1px solid var(--su-border);
|
||||
border-radius: var(--su-radius-md);
|
||||
padding: 8px 12px;
|
||||
background-color: var(--su-bg-subtle);
|
||||
}
|
||||
|
||||
.form-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -469,6 +469,8 @@ export interface components {
|
||||
next_run_at?: string | null;
|
||||
task_id: string;
|
||||
};
|
||||
/** @enum {string} */
|
||||
ScriptLanguage: "python" | "shell" | "node" | "lua";
|
||||
SessionResponse: {
|
||||
/** @description Random per-process token required for mutating browser requests. */
|
||||
token: string;
|
||||
@@ -479,6 +481,7 @@ export interface components {
|
||||
[key: string]: string;
|
||||
};
|
||||
interpreter: string;
|
||||
language?: components["schemas"]["ScriptLanguage"] | null;
|
||||
script: string;
|
||||
/** @enum {string} */
|
||||
type: "script";
|
||||
|
||||
@@ -36,9 +36,11 @@ import { DirectoryPickerDialog } from './DirectoryPickerDialog'
|
||||
import {
|
||||
createEmptyTaskFormValues,
|
||||
mapServerFieldToFormField,
|
||||
SCRIPT_LANGUAGE_META,
|
||||
taskFormSchema,
|
||||
taskToFormValues,
|
||||
type KeyValueFormValue,
|
||||
type ScriptLanguage,
|
||||
type TaskFormValues,
|
||||
} from '../lib/taskForm'
|
||||
import { useDebouncedValue } from '../lib/useDebouncedValue'
|
||||
@@ -52,11 +54,17 @@ interface TaskEditorModalProps {
|
||||
}
|
||||
|
||||
const ACTION_TYPE_OPTIONS = [
|
||||
{ value: 'script', label: '执行脚本 (Python / Shell / Node / Lua)' },
|
||||
{ value: 'script', label: '执行脚本' },
|
||||
{ value: 'http', label: '发起 HTTP 请求' },
|
||||
{ value: 'process', label: '管理常驻/独立二进制' },
|
||||
{ value: 'process', label: '运行二进制程序' },
|
||||
]
|
||||
|
||||
const SCRIPT_LANGUAGE_OPTIONS = (
|
||||
Object.entries(SCRIPT_LANGUAGE_META) as Array<
|
||||
[ScriptLanguage, (typeof SCRIPT_LANGUAGE_META)[ScriptLanguage]]
|
||||
>
|
||||
).map(([value, meta]) => ({ value, label: meta.label }))
|
||||
|
||||
const HTTP_METHOD_OPTIONS = [
|
||||
{ value: 'GET', label: 'GET' },
|
||||
{ value: 'POST', label: 'POST' },
|
||||
@@ -109,6 +117,9 @@ export function TaskEditorModal({
|
||||
const cronValue = watch('cron')
|
||||
const timezoneValue = watch('timezone')
|
||||
const scriptPreview = watch('script')
|
||||
const scriptLanguage = scriptPreview.language
|
||||
const scriptMeta = SCRIPT_LANGUAGE_META[scriptLanguage]
|
||||
const isPythonScript = scriptLanguage === 'python'
|
||||
// 当前只开放 Asia/Shanghai;编辑旧任务时把它已有的时区也保留成一个选项,
|
||||
// 避免下拉显示成空值。
|
||||
const timezoneOptions = Array.from(
|
||||
@@ -147,12 +158,42 @@ export function TaskEditorModal({
|
||||
Boolean(debouncedTimezone.trim()),
|
||||
retry: false,
|
||||
})
|
||||
const applyScriptLanguage = (language: ScriptLanguage) => {
|
||||
const meta = SCRIPT_LANGUAGE_META[language]
|
||||
const currentScript = getValues('script.script').trim()
|
||||
const knownDefaults = Object.values(SCRIPT_LANGUAGE_META).map(
|
||||
(item) => item.defaultScript,
|
||||
)
|
||||
setValue('script.language', language, {
|
||||
shouldDirty: true,
|
||||
shouldValidate: true,
|
||||
})
|
||||
setValue('script.interpreter', meta.defaultInterpreter, {
|
||||
shouldDirty: true,
|
||||
shouldValidate: true,
|
||||
})
|
||||
if (!currentScript || knownDefaults.includes(currentScript)) {
|
||||
setValue('script.script', meta.defaultScript, {
|
||||
shouldDirty: true,
|
||||
shouldValidate: true,
|
||||
})
|
||||
}
|
||||
if (language !== 'python') {
|
||||
setValue('script.venv', '', {
|
||||
shouldDirty: true,
|
||||
shouldValidate: true,
|
||||
})
|
||||
autoVenvRef.current = ''
|
||||
}
|
||||
}
|
||||
|
||||
const venvProbe = useQuery({
|
||||
queryKey: [
|
||||
'python-venv',
|
||||
debouncedWorkingDir,
|
||||
debouncedInterpreter,
|
||||
debouncedVenv,
|
||||
scriptLanguage,
|
||||
],
|
||||
queryFn: ({ signal }) =>
|
||||
probePythonVenv(
|
||||
@@ -164,7 +205,10 @@ export function TaskEditorModal({
|
||||
signal,
|
||||
),
|
||||
enabled:
|
||||
open && actionType === 'script' && Boolean(debouncedWorkingDir.trim()),
|
||||
open &&
|
||||
actionType === 'script' &&
|
||||
isPythonScript &&
|
||||
Boolean(debouncedWorkingDir.trim()),
|
||||
retry: false,
|
||||
staleTime: 15_000,
|
||||
})
|
||||
@@ -185,10 +229,16 @@ export function TaskEditorModal({
|
||||
},
|
||||
onError: (error) => toast.error(error.message),
|
||||
})
|
||||
const linkedVenv = venvProbe.data?.status === 'linked' ? venvProbe.data : null
|
||||
const previewVirtualEnv =
|
||||
scriptPreview.venv.trim() || linkedVenv?.venv_path || ''
|
||||
const previewInterpreter = linkedVenv?.display_path ?? null
|
||||
const linkedVenv =
|
||||
isPythonScript && venvProbe.data?.status === 'linked'
|
||||
? venvProbe.data
|
||||
: null
|
||||
const previewVirtualEnv = isPythonScript
|
||||
? scriptPreview.venv.trim() || linkedVenv?.venv_path || ''
|
||||
: ''
|
||||
const previewInterpreter = isPythonScript
|
||||
? (linkedVenv?.display_path ?? null)
|
||||
: null
|
||||
|
||||
const scriptArgs = useFieldArray({ control, name: 'script.args' })
|
||||
const scriptEnv = useFieldArray({ control, name: 'script.env' })
|
||||
@@ -220,7 +270,7 @@ export function TaskEditorModal({
|
||||
|
||||
// 探测到虚拟环境就自动填进「虚拟环境目录」,但用户自己改过就保留用户的值。
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
if (!open || !isPythonScript) {
|
||||
return
|
||||
}
|
||||
const probe = venvProbe.data
|
||||
@@ -245,7 +295,7 @@ export function TaskEditorModal({
|
||||
shouldDirty: true,
|
||||
shouldValidate: true,
|
||||
})
|
||||
}, [debouncedVenv, debouncedWorkingDir, getValues, open, setValue, venvProbe.data])
|
||||
}, [debouncedVenv, debouncedWorkingDir, getValues, isPythonScript, open, setValue, venvProbe.data])
|
||||
|
||||
// 打开后把焦点给第一个可编辑字段:新建时是任务标识,编辑时 ID 已锁定,跳名称。
|
||||
// 原生 dialog 默认聚焦关闭按钮,不符合编辑表单的节奏。
|
||||
@@ -475,6 +525,32 @@ export function TaskEditorModal({
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
{actionType === 'script' ? (
|
||||
<div className="form-grid language-grid">
|
||||
<FormField label="脚本语言">
|
||||
<Controller
|
||||
control={control}
|
||||
name="script.language"
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
className="form-select"
|
||||
options={SCRIPT_LANGUAGE_OPTIONS}
|
||||
value={field.value}
|
||||
onChange={(value) =>
|
||||
applyScriptLanguage(value as ScriptLanguage)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
<div className="language-hint">
|
||||
<Text size="sm" tone="sub">
|
||||
{scriptMeta.hint}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{actionType === 'script' ? (
|
||||
<div className="dynamic-fields">
|
||||
<FormField
|
||||
@@ -502,48 +578,52 @@ export function TaskEditorModal({
|
||||
</div>
|
||||
</FormField>
|
||||
|
||||
<div className="form-grid venv-grid">
|
||||
<FormField
|
||||
label="虚拟环境目录 (可选)"
|
||||
htmlFor="task-venv"
|
||||
hint="检测到虚拟环境会自动填入,可直接修改;留空则按工作目录自动探测"
|
||||
error={errors.script?.venv?.message}
|
||||
>
|
||||
<Input
|
||||
id="task-venv"
|
||||
{...register('script.venv')}
|
||||
className="mono-input"
|
||||
placeholder="例如:.venv"
|
||||
/>
|
||||
</FormField>
|
||||
<FormField
|
||||
label="解释器(可选)"
|
||||
htmlFor="task-interpreter"
|
||||
hint="Python 项目留空即可,自动使用虚拟环境里的 Python;Node / Shell 等再填"
|
||||
error={errors.script?.interpreter?.message}
|
||||
>
|
||||
<Input
|
||||
id="task-interpreter"
|
||||
{...register('script.interpreter')}
|
||||
className="mono-input"
|
||||
placeholder="留空自动使用虚拟环境 Python"
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
{isPythonScript ? (
|
||||
<>
|
||||
<div className="form-grid venv-grid">
|
||||
<FormField
|
||||
label="虚拟环境目录 (可选)"
|
||||
htmlFor="task-venv"
|
||||
hint="检测到虚拟环境会自动填入,可直接修改;留空则按工作目录自动探测"
|
||||
error={errors.script?.venv?.message}
|
||||
>
|
||||
<Input
|
||||
id="task-venv"
|
||||
{...register('script.venv')}
|
||||
className="mono-input"
|
||||
placeholder="例如:.venv"
|
||||
/>
|
||||
</FormField>
|
||||
<FormField
|
||||
label="解释器(可选)"
|
||||
htmlFor="task-interpreter"
|
||||
hint="留空自动使用虚拟环境 Python;也可以填自定义 Python 路径"
|
||||
error={errors.script?.interpreter?.message}
|
||||
>
|
||||
<Input
|
||||
id="task-interpreter"
|
||||
{...register('script.interpreter')}
|
||||
className="mono-input"
|
||||
placeholder="留空自动使用虚拟环境 Python"
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
<VenvDetectorBar
|
||||
workingDir={debouncedWorkingDir}
|
||||
probe={venvProbe.data}
|
||||
pending={venvProbe.isPending}
|
||||
failed={venvProbe.isError}
|
||||
creating={createVenvMutation.isPending}
|
||||
onCreate={() =>
|
||||
createVenvMutation.mutate(scriptPreview.working_dir.trim())
|
||||
}
|
||||
/>
|
||||
<VenvDetectorBar
|
||||
workingDir={debouncedWorkingDir}
|
||||
probe={venvProbe.data}
|
||||
pending={venvProbe.isPending}
|
||||
failed={venvProbe.isError}
|
||||
creating={createVenvMutation.isPending}
|
||||
onCreate={() =>
|
||||
createVenvMutation.mutate(scriptPreview.working_dir.trim())
|
||||
}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<FormField
|
||||
label="脚本文件或相对路径"
|
||||
label={scriptMeta.scriptLabel}
|
||||
htmlFor="task-script"
|
||||
error={errors.script?.script?.message}
|
||||
>
|
||||
@@ -551,7 +631,7 @@ export function TaskEditorModal({
|
||||
id="task-script"
|
||||
{...register('script.script')}
|
||||
className="mono-input"
|
||||
placeholder="main.py 或 scripts\sync.py"
|
||||
placeholder={scriptMeta.scriptPlaceholder}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
@@ -637,9 +717,11 @@ export function TaskEditorModal({
|
||||
</div>
|
||||
))}
|
||||
<div className="cli-line-cmd">
|
||||
{formatInterpreterPreview(scriptPreview, previewInterpreter)}{' '}
|
||||
{scriptPreview.script.trim() || 'main.py'}
|
||||
{formatArgumentPreview(scriptPreview.args)}
|
||||
{formatScriptCommandPreview(
|
||||
scriptPreview,
|
||||
previewInterpreter,
|
||||
scriptLanguage,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -877,12 +959,53 @@ function venvFieldValue(workingDir: string, venvPath: string): string {
|
||||
|
||||
function formatInterpreterPreview(
|
||||
script: TaskFormValues['script'],
|
||||
linkedPythonPath?: string | null,
|
||||
linkedPythonPath: string | null | undefined,
|
||||
language: ScriptLanguage,
|
||||
): string {
|
||||
if (script.venv.trim()) {
|
||||
if (language === 'python' && script.venv.trim()) {
|
||||
return `${script.venv.trim().replace(/[\\/]+$/, '')}\\Scripts\\python.exe`
|
||||
}
|
||||
return linkedPythonPath ?? (script.interpreter.trim() || 'python.exe')
|
||||
if (linkedPythonPath) {
|
||||
return linkedPythonPath
|
||||
}
|
||||
if (script.interpreter.trim()) {
|
||||
return script.interpreter.trim()
|
||||
}
|
||||
return language === 'python'
|
||||
? 'python.exe'
|
||||
: SCRIPT_LANGUAGE_META[language].defaultInterpreter
|
||||
}
|
||||
|
||||
function isCmdInterpreter(interpreter: string): boolean {
|
||||
return (
|
||||
interpreter
|
||||
.trim()
|
||||
.split(/[\\/]/)
|
||||
.pop()
|
||||
?.toLowerCase()
|
||||
.replace(/\.exe$/, '') === 'cmd'
|
||||
)
|
||||
}
|
||||
|
||||
function formatScriptCommandPreview(
|
||||
script: TaskFormValues['script'],
|
||||
linkedPythonPath: string | null | undefined,
|
||||
language: ScriptLanguage,
|
||||
): string {
|
||||
const interpreter = formatInterpreterPreview(
|
||||
script,
|
||||
linkedPythonPath,
|
||||
language,
|
||||
)
|
||||
const file =
|
||||
script.script.trim() || SCRIPT_LANGUAGE_META[language].defaultScript
|
||||
const cmdPrefix =
|
||||
language === 'shell' &&
|
||||
isCmdInterpreter(script.interpreter) &&
|
||||
!file.startsWith('/')
|
||||
? '/C '
|
||||
: ''
|
||||
return `${interpreter} ${cmdPrefix}${file}${formatArgumentPreview(script.args)}`
|
||||
}
|
||||
|
||||
interface VenvDetectorBarProps {
|
||||
|
||||
@@ -28,6 +28,26 @@ describe('task form payload', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('submits the selected script language and interpreter', () => {
|
||||
const values = createEmptyTaskFormValues()
|
||||
values.id = 'shell_task'
|
||||
values.name = 'Shell task'
|
||||
values.script.language = 'shell'
|
||||
values.script.working_dir = 'D:\\Projects\\shell-task'
|
||||
values.script.interpreter = 'cmd.exe'
|
||||
values.script.script = 'run.bat'
|
||||
|
||||
const payload = valuesToCreateRequest(values)
|
||||
|
||||
expect(payload.action).toMatchObject({
|
||||
type: 'script',
|
||||
language: 'shell',
|
||||
working_dir: 'D:\\Projects\\shell-task',
|
||||
interpreter: 'cmd.exe',
|
||||
script: 'run.bat',
|
||||
})
|
||||
})
|
||||
|
||||
it('creates an HTTP action instead of forcing script type', () => {
|
||||
const values = createEmptyTaskFormValues()
|
||||
values.id = 'health_check'
|
||||
|
||||
@@ -12,6 +12,75 @@ export interface KeyValueFormValue {
|
||||
value: string
|
||||
}
|
||||
|
||||
export type ScriptLanguage = 'python' | 'shell' | 'node' | 'lua'
|
||||
|
||||
export const SCRIPT_LANGUAGE_META: Record<
|
||||
ScriptLanguage,
|
||||
{
|
||||
label: string
|
||||
defaultInterpreter: string
|
||||
defaultScript: string
|
||||
scriptLabel: string
|
||||
scriptPlaceholder: string
|
||||
hint: string
|
||||
}
|
||||
> = {
|
||||
python: {
|
||||
label: 'Python',
|
||||
defaultInterpreter: '',
|
||||
defaultScript: 'main.py',
|
||||
scriptLabel: '脚本文件或相对路径',
|
||||
scriptPlaceholder: 'main.py 或 scripts\\sync.py',
|
||||
hint: '优先复用项目 .venv;没有环境时可用 uv 从 pyproject.toml / requirements.txt 创建,运行时会注入虚拟环境变量。',
|
||||
},
|
||||
shell: {
|
||||
label: 'Shell (cmd)',
|
||||
defaultInterpreter: 'cmd.exe',
|
||||
defaultScript: 'run.bat',
|
||||
scriptLabel: '批处理文件或相对路径',
|
||||
scriptPlaceholder: 'run.bat 或 scripts\\start.cmd',
|
||||
hint: '通过 cmd.exe /C 执行批处理文件,不需要虚拟环境。',
|
||||
},
|
||||
node: {
|
||||
label: 'Node.js',
|
||||
defaultInterpreter: 'node.exe',
|
||||
defaultScript: 'index.js',
|
||||
scriptLabel: 'JavaScript 文件或相对路径',
|
||||
scriptPlaceholder: 'index.js 或 scripts\\worker.js',
|
||||
hint: '通过 node.exe 执行脚本,不需要虚拟环境;需要本机 PATH 中已安装 Node.js。',
|
||||
},
|
||||
lua: {
|
||||
label: 'Lua',
|
||||
defaultInterpreter: 'lua.exe',
|
||||
defaultScript: 'main.lua',
|
||||
scriptLabel: 'Lua 文件或相对路径',
|
||||
scriptPlaceholder: 'main.lua 或 scripts\\task.lua',
|
||||
hint: '通过 lua.exe 执行脚本,不需要虚拟环境;需要本机 PATH 中已安装 Lua。',
|
||||
},
|
||||
}
|
||||
|
||||
export function detectScriptLanguage(interpreter: string): ScriptLanguage {
|
||||
const name = interpreter
|
||||
.trim()
|
||||
.split(/[\\/]/)
|
||||
.pop()
|
||||
?.toLowerCase()
|
||||
.replace(/\.exe$/, '')
|
||||
if (name === 'node' || name === 'nodejs') {
|
||||
return 'node'
|
||||
}
|
||||
if (name?.startsWith('lua')) {
|
||||
return 'lua'
|
||||
}
|
||||
if (
|
||||
name &&
|
||||
['cmd', 'powershell', 'pwsh', 'bash', 'sh', 'zsh'].includes(name)
|
||||
) {
|
||||
return 'shell'
|
||||
}
|
||||
return 'python'
|
||||
}
|
||||
|
||||
export interface TaskFormValues {
|
||||
id: string
|
||||
name: string
|
||||
@@ -23,6 +92,7 @@ export interface TaskFormValues {
|
||||
script: {
|
||||
working_dir: string
|
||||
venv: string
|
||||
language: ScriptLanguage
|
||||
interpreter: string
|
||||
script: string
|
||||
args: KeyValueFormValue[]
|
||||
@@ -88,6 +158,7 @@ export const taskFormSchema = z
|
||||
script: z.object({
|
||||
working_dir: z.string(),
|
||||
venv: z.string(),
|
||||
language: z.enum(['python', 'shell', 'node', 'lua']),
|
||||
interpreter: z.string(),
|
||||
script: z.string(),
|
||||
args: z.array(keyValueSchema),
|
||||
@@ -164,6 +235,7 @@ export function createEmptyTaskFormValues(): TaskFormValues {
|
||||
script: {
|
||||
working_dir: '',
|
||||
venv: '',
|
||||
language: 'python',
|
||||
interpreter: '',
|
||||
script: 'main.py',
|
||||
args: [],
|
||||
@@ -196,9 +268,11 @@ export function taskToFormValues(task: Task): TaskFormValues {
|
||||
values.actionType = task.action.type
|
||||
|
||||
if (task.action.type === 'script') {
|
||||
const language = task.action.language ?? detectScriptLanguage(task.action.interpreter)
|
||||
values.script = {
|
||||
working_dir: task.action.working_dir,
|
||||
venv: task.action.venv ?? '',
|
||||
venv: language === 'python' ? (task.action.venv ?? '') : '',
|
||||
language,
|
||||
interpreter: task.action.interpreter,
|
||||
script: task.action.script,
|
||||
args: argsToRows(task.action.args ?? []),
|
||||
@@ -238,6 +312,7 @@ export function valuesToAction(values: TaskFormValues): TaskAction {
|
||||
type: 'script',
|
||||
working_dir: values.script.working_dir.trim(),
|
||||
venv: values.script.venv.trim() || null,
|
||||
language: values.script.language,
|
||||
interpreter: values.script.interpreter.trim(),
|
||||
script: values.script.script.trim(),
|
||||
args: argumentsToArray(values.script.args),
|
||||
|
||||
Reference in New Issue
Block a user