Simplify task backend configuration forms
Quality gate / backend (push) Canceled after 0s
Quality gate / frontend (push) Canceled after 0s

This commit is contained in:
Xin Wang
2026-10-25 23:26:35 +08:00
parent 6043eb9b63
commit d7c44064d4
5 changed files with 138 additions and 104 deletions
@@ -66,44 +66,48 @@ export function TaskBackendSection({ value, onChange, pipeline, variables, model
</Select>
{error && <div className="flex items-center justify-between gap-3 text-xs"><span role="alert" className="text-destructive">{error}</span><Button size="sm" variant="outline" onClick={() => void load()}>重试</Button></div>}
{!loading && !error && !resources.length && <p className="text-xs leading-5 text-muted-foreground">先在组件库创建一个任务后端,然后回到此处选择。</p>}
{value && selected && <>
{!selected.enabled && <p role="alert" className="text-xs text-destructive">当前后端已停用,请启用资源或重新选择后端。</p>}
<div className="space-y-1 rounded-xl border border-hairline bg-canvas-soft p-3 text-xs leading-6 text-muted-foreground">
{selected.type === "reasoner" ? <>
<p>推理模型:{modelOptions.find(model => model.value === selected.config.modelResourceId)?.label ?? selected.config.modelResourceId}</p>
<p>推理强度:{selected.config.reasoningEffort === "provider_default" ? "模型默认" : selected.config.reasoningEffort} · 服务端工具 {selected.config.toolBindings.length} 项 · 技能 {selected.config.skills.length} 项</p>
</> : <>
<p className="break-all">OpenCode:{selected.config.url} · {selected.config.directory}</p>
<p>Agent:{selected.config.agent || "远端默认"} · 权限、沙箱与技能由远端管理</p>
{selected.config.skillsNote && <p className="whitespace-pre-wrap">技能说明:{selected.config.skillsNote}</p>}
</>}
<p>任务时限 {selected.config.timeoutSeconds} 秒 · 并发 1 · 断线后任务继续</p>
</div>
</>}
{value && selected && !selected.enabled && <p role="alert" className="text-xs text-destructive">当前后端已停用,请启用资源或重新选择后端。</p>}
{value && <div className="space-y-3">
<label className="block space-y-2 text-sm">
<span className="font-medium">该助手的任务指令(可选)</span>
<span className="font-medium">任务说明(可选)</span>
<Textarea rows={3} value={value.instructions} placeholder="例如:先分析影响范围,完成后说明修改与验证结果。支持 {{变量名}}。"
onChange={event => onChange({ ...value, instructions: event.target.value })} />
</label>
<label className="block space-y-2 text-sm">
<span className="font-medium">任务要求来源</span>
<Select value={source.source} onValueChange={kind => changeSource(kind as ValueSource["source"])}>
<SelectTrigger className="w-full"><SelectValue /></SelectTrigger>
<SelectContent>{Object.entries(sourceLabels).map(([kind, label]) => <SelectItem key={kind} value={kind}>{label}</SelectItem>)}</SelectContent>
</Select>
</label>
{source.source === "model" && <p className="text-xs leading-5 text-muted-foreground">Speaker 根据对话生成本次目标,后端连接与权限由资源配置决定。</p>}
{source.source === "variable" && <label className="block space-y-2 text-sm">
<span className="font-medium">变量名</span>
<Input list={variableListId} value={source.variableName} placeholder="例如 project_task" onChange={event => onChange({ ...value, instructionSource: { source: "variable", variableName: event.target.value } })} />
<datalist id={variableListId}>{Object.keys(variables).map(name => <option key={name} value={name} />)}</datalist>
</label>}
{(source.source === "literal" || source.source === "template") && <label className="block space-y-2 text-sm">
<span className="font-medium">{source.source === "literal" ? "固定任务要求" : "任务模板"}</span>
<Textarea rows={3} value={typeof source.value === "string" ? source.value : JSON.stringify(source.value ?? "")} placeholder={source.source === "template" ? "处理 {{project}} 的任务" : "每次委派使用此任务要求"}
onChange={event => onChange({ ...value, instructionSource: { source: source.source, value: event.target.value } })} />
</label>}
{source.source === "model" && <p className="text-xs leading-5 text-muted-foreground">Speaker 根据对话生成每次任务的目标。</p>}
<details className="rounded-xl border border-hairline p-3">
<summary className="cursor-pointer text-xs font-medium text-muted-foreground">
高级设置{source.source !== "model" && <span className="ml-2 font-normal">· 当前使用{sourceLabels[source.source]}</span>}
</summary>
<div className="mt-4 space-y-3">
<label className="block space-y-2 text-sm">
<span className="font-medium">任务要求来源</span>
<Select value={source.source} onValueChange={kind => changeSource(kind as ValueSource["source"])}>
<SelectTrigger className="w-full"><SelectValue /></SelectTrigger>
<SelectContent>{Object.entries(sourceLabels).map(([kind, label]) => <SelectItem key={kind} value={kind}>{label}</SelectItem>)}</SelectContent>
</Select>
</label>
{source.source === "variable" && <label className="block space-y-2 text-sm">
<span className="font-medium">变量名</span>
<Input list={variableListId} value={source.variableName} placeholder="例如 project_task" onChange={event => onChange({ ...value, instructionSource: { source: "variable", variableName: event.target.value } })} />
<datalist id={variableListId}>{Object.keys(variables).map(name => <option key={name} value={name} />)}</datalist>
</label>}
{(source.source === "literal" || source.source === "template") && <label className="block space-y-2 text-sm">
<span className="font-medium">{source.source === "literal" ? "固定任务要求" : "任务模板"}</span>
<Textarea rows={3} value={typeof source.value === "string" ? source.value : JSON.stringify(source.value ?? "")} placeholder={source.source === "template" ? "处理 {{project}} 的任务" : "每次委派使用此任务要求"}
onChange={event => onChange({ ...value, instructionSource: { source: source.source, value: event.target.value } })} />
</label>}
{selected && <div className="space-y-1 rounded-xl bg-canvas-soft p-3 text-xs leading-6 text-muted-foreground">
{selected.type === "reasoner" ? <>
<p>推理模型:{modelOptions.find(model => model.value === selected.config.modelResourceId)?.label ?? selected.config.modelResourceId}</p>
<p>推理强度:{selected.config.reasoningEffort === "provider_default" ? "模型默认" : selected.config.reasoningEffort} · 工具 {selected.config.toolBindings.length} 项 · 技能 {selected.config.skills.length} 项</p>
</> : <>
<p className="break-all">OpenCode:{selected.config.url} · {selected.config.directory}</p>
<p>Agent:{selected.config.agent || "远端默认"} · 权限、沙箱与技能由远端管理</p>
</>}
<p>任务时限 {selected.config.timeoutSeconds} 秒 · 并发 1 · 断线后任务继续</p>
</div>}
</div>
</details>
</div>}
</>}
</SectionCard>;
@@ -3,7 +3,7 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { Copy, Loader2, Pencil, Plus, Trash2 } from "lucide-react";
import { ListPageLayout, ListPageSection } from "@/components/layout/list-page-layout";
import { BackendField, OpenCodeFields, ReasonerFields } from "@/components/task-backends/backend-fields";
import { BackendField, OpenCodeAdvancedFields, OpenCodeFields, ReasonerAdvancedFields, ReasonerFields } from "@/components/task-backends/backend-fields";
import { backendPayload, draftFromBackend, emptyBackendDraft } from "@/components/task-backends/backend-form";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
@@ -65,6 +65,11 @@ export function ComponentsTaskBackendsPage() {
(!query || `${resource.name} ${resource.description}`.toLowerCase().includes(query)));
}, [resources, search, filter]);
const hasAdvancedSettings = draft.timeoutSeconds !== "600" || Boolean(draft.instructions.trim()) ||
(draft.type === "reasoner"
? draft.reasoningEffort !== "provider_default" || draft.skills.length > 0
: draft.username !== "opencode" || Boolean(draft.agent || draft.modelProviderId || draft.modelId));
function openEditor(resource?: TaskBackendResource) {
setEditing(resource ?? null);
setDraft(resource ? draftFromBackend(resource) : emptyBackendDraft());
@@ -105,20 +110,40 @@ export function ComponentsTaskBackendsPage() {
}
}
async function setEnabled(resource: TaskBackendResource, enabled: boolean) {
if (!enabled && !window.confirm("停用后,已绑定助手需解绑或重新启用此后端才能开启新会话。已创建的任务继续执行。确认停用?")) return;
setBusyId(resource.id);
try {
const nextDraft = draftFromBackend(resource);
nextDraft.enabled = enabled;
await taskBackendsApi.update(resource.id, backendPayload(nextDraft));
await load();
} catch (error) {
setError(error instanceof Error ? error.message : "状态更新失败");
} finally {
setBusyId(null);
}
}
return <>
<ListPageLayout title="任务后端" description="为提示词助手配置独立的推理与执行环境。Speaker 负责对话,任务后端负责处理委派目标。"
action={<Button className="gap-2" disabled={loading || Boolean(error)} onClick={() => openEditor()}><Plus size={16} />添加任务后端</Button>}>
<ListPageSection>
<ListToolbar filters={<FilterPills options={filters} value={filter} onChange={setFilter} />}
search={<SearchInput value={search} onChange={setSearch} placeholder="搜索名称或说明" />} />
search={<SearchInput value={search} onChange={setSearch} placeholder="搜索任务后端" />} />
<DataList rows={filtered} rowKey={resource => resource.id} loading={loading} error={error} onRetry={() => void load()}
onRowClick={resource => openEditor(resource)} empty={{ title: "暂无任务后端", description: "创建内置 Reasoner 或外部 OpenCode,再到提示词助手中绑定。" }}
onRowClick={resource => { if (busyId !== resource.id) openEditor(resource); }} empty={{ title: "暂无任务后端", description: "创建内置 Reasoner 或外部 OpenCode,再到提示词助手中绑定。" }}
columns={[
{ key: "name", header: "名称", cell: resource => <div><div className="font-medium">{resource.name}</div><div className="mt-1 text-xs text-muted-foreground">{resource.description || "未填写说明"}</div></div> },
{ key: "name", header: "名称", cell: resource => <div className="font-medium">{resource.name}</div> },
{ key: "type", header: "后端", width: "md:w-[164px]", cell: resource => <Badge variant="secondary">{typeLabels[resource.type]}</Badge> },
{ key: "status", header: "状态", width: "md:w-[84px]", cell: resource => <span className="text-muted-foreground">{resource.enabled ? "已启用" : "已停用"}</span> },
{ key: "status", header: "状态", width: "md:w-[124px]", cell: resource => <div className="flex items-center gap-2" onClick={event => event.stopPropagation()}>
<Switch size="sm" checked={resource.enabled} disabled={busyId === resource.id}
aria-label={`${resource.enabled ? "停用" : "启用"} ${resource.name}`}
onCheckedChange={enabled => void setEnabled(resource, enabled)} />
<span className="text-muted-foreground">{resource.enabled ? "已启用" : "已停用"}</span>
</div> },
{ key: "action", header: "操作", width: "md:w-[144px]", align: "right", cell: resource => <div className="flex justify-end gap-1" onClick={event => event.stopPropagation()}>
<Button variant="ghost" size="icon" aria-label={`编辑 ${resource.name}`} onClick={() => openEditor(resource)}><Pencil size={15} /></Button>
<Button variant="ghost" size="icon" aria-label={`编辑 ${resource.name}`} disabled={busyId === resource.id} onClick={() => openEditor(resource)}><Pencil size={15} /></Button>
<Button variant="ghost" size="icon" aria-label={`复制 ${resource.name}`} disabled={busyId === resource.id} onClick={() => void act(resource, "duplicate")}><Copy size={15} /></Button>
<Button variant="ghost" size="icon" aria-label={`删除 ${resource.name}`} disabled={busyId === resource.id} onClick={() => void act(resource, "remove")}><Trash2 size={15} /></Button>
</div> },
@@ -127,14 +152,14 @@ export function ComponentsTaskBackendsPage() {
</ListPageLayout>
<Dialog open={dialogOpen} onOpenChange={open => { if (!saving) setDialogOpen(open); }}>
<DialogContent className="flex max-h-[90dvh] flex-col overflow-hidden sm:max-w-3xl">
<DialogContent className="flex max-h-[90dvh] flex-col overflow-hidden sm:max-w-2xl">
<DialogHeader>
<DialogTitle>{editing ? "编辑任务后端" : "添加任务后端"}</DialogTitle>
<DialogDescription>连接、模型、执行策略与技能在此复用。助手侧配置任务目标与委派要求。</DialogDescription>
<DialogDescription>配置模型或远端连接,任务要求在助手中填写。</DialogDescription>
</DialogHeader>
<div className="min-h-0 space-y-5 overflow-y-auto pr-2">
<div className="grid gap-4 sm:grid-cols-2">
<BackendField label="名称"><Input value={draft.name} onChange={event => setDraft(previous => ({ ...previous, name: event.target.value }))} /></BackendField>
<BackendField label="名称(可选)"><Input value={draft.name} placeholder={`留空使用${draft.type === "reasoner" ? "内置 Reasoner" : "OpenCode"}`} onChange={event => setDraft(previous => ({ ...previous, name: event.target.value }))} /></BackendField>
<BackendField label="任务后端类型">
<Select value={draft.type} disabled={Boolean(editing)} onValueChange={value => setDraft(previous => ({ ...previous, type: value as TaskBackendType }))}>
<SelectTrigger className="w-full"><SelectValue /></SelectTrigger>
@@ -142,23 +167,23 @@ export function ComponentsTaskBackendsPage() {
</Select>
</BackendField>
</div>
<BackendField label="说明"><Textarea rows={2} value={draft.description} onChange={event => setDraft(previous => ({ ...previous, description: event.target.value }))} /></BackendField>
<div className="flex items-center justify-between rounded-xl border border-hairline p-3">
<label htmlFor="backend-enabled" className="text-sm">启用此任务后端</label>
<Switch id="backend-enabled" checked={draft.enabled} onCheckedChange={enabled => setDraft(previous => ({ ...previous, enabled }))} />
</div>
{!draft.enabled && <p className="text-xs leading-5 text-muted-foreground">停用后,已绑定助手需解绑或重新启用此后端才能开启新会话。已创建的任务继续使用提交时的配置。</p>}
{draft.type === "reasoner"
? <ReasonerFields draft={draft} setDraft={setDraft} models={models} tools={tools} />
: <OpenCodeFields draft={draft} setDraft={setDraft} hasPassword={Boolean(editing?.secrets.password)} />}
<BackendField label="后端任务指令(可选)" hint="所有使用此后端的任务共享的要求。每个助手还可以添加独立的任务指令。">
<Textarea rows={4} value={draft.instructions} onChange={event => setDraft(previous => ({ ...previous, instructions: event.target.value }))} />
</BackendField>
<div className="rounded-2xl border border-hairline p-4">
<BackendField label="任务时限(秒)" hint="覆盖排队与执行;当前并发固定为 1。语音插话和断线不会取消任务,明确停止时会核查执行状态。">
<Input type="number" min={30} max={7200} value={draft.timeoutSeconds} onChange={event => setDraft(previous => ({ ...previous, timeoutSeconds: event.target.value }))} />
</BackendField>
</div>
<details key={editing?.id ?? draft.type} className="rounded-2xl border border-hairline p-4">
<summary className="cursor-pointer text-sm font-medium">高级设置{hasAdvancedSettings ? " · 已配置" : ""}</summary>
<div className="mt-4 space-y-5">
{draft.type === "reasoner"
? <ReasonerAdvancedFields draft={draft} setDraft={setDraft} models={models} />
: <OpenCodeAdvancedFields draft={draft} setDraft={setDraft} />}
<BackendField label="任务时限(秒)" hint="默认 600 秒,包含排队与执行时间。">
<Input type="number" min={30} max={7200} value={draft.timeoutSeconds} onChange={event => setDraft(previous => ({ ...previous, timeoutSeconds: event.target.value }))} />
</BackendField>
{editing?.config.instructions && <BackendField label="原有后端任务指令" hint="此资源已有的通用要求会保留。新的任务要求可在助手中填写。">
<Textarea rows={3} value={draft.instructions} onChange={event => setDraft(previous => ({ ...previous, instructions: event.target.value }))} />
</BackendField>}
</div>
</details>
</div>
{saveError && <p role="alert" className="text-sm text-destructive">{saveError}</p>}
<DialogFooter>
@@ -28,7 +28,6 @@ export function ReasonerFields({ draft, setDraft, models, tools }: FieldsProps &
const availableModels = models.filter(model => model.enabled && model.capability === "LLM" &&
["openai-llm", "dashscope-llm"].includes(model.interfaceType));
const selectedModel = models.find(model => model.id === draft.modelResourceId);
const supportsEffort = selectedModel?.values.supportsReasoningEffort === true;
const serverTools = tools.filter(tool => tool.type === "http" || tool.type === "mcp");
return <div className="space-y-5">
<BackendField label="推理模型" hint="模型与 Speaker 分别运行,各自使用独立的上下文。支持 OpenAI-compatible LLM 接口。">
@@ -46,26 +45,30 @@ export function ReasonerFields({ draft, setDraft, models, tools }: FieldsProps &
</Select>
{!availableModels.length && <Link href="/components/models" className="text-xs underline">先创建 LLM 模型资源</Link>}
</BackendField>
<BackendField label="推理强度" hint={supportsEffort
? "使用模型接口支持的 reasoning_effort 参数;需确认模型真实支持所选强度。"
: "当前模型未声明支持推理强度,使用模型默认配置。可在模型资源中声明接口能力。"}>
<div className="space-y-3 rounded-2xl border border-hairline p-4">
<div className="text-sm font-medium">可用工具(可选)</div>
<p className="text-xs leading-5 text-muted-foreground">默认仅推理;需要执行操作时,按需授权 HTTP / MCP 服务端工具。</p>
<ToolBindingEditor tools={serverTools} selectableTools={serverTools} value={draft.toolBindings} variables={{}}
onChange={toolBindings => setDraft(previous => ({ ...previous, toolBindings }))} />
</div>
</div>;
}
export function ReasonerAdvancedFields({ draft, setDraft, models }: FieldsProps & { models: ModelResource[] }) {
const supportsEffort = models.find(model => model.id === draft.modelResourceId)?.values.supportsReasoningEffort === true;
return <div className="space-y-5">
{supportsEffort || draft.reasoningEffort !== "provider_default" ? <BackendField label="推理强度" hint="默认沿用模型设置;自定义强度需模型接口实际支持。">
<Select value={draft.reasoningEffort} onValueChange={value => setDraft(previous => ({ ...previous, reasoningEffort: value as ReasoningEffort }))}>
<SelectTrigger className="w-full"><SelectValue /></SelectTrigger>
<SelectContent>{Object.entries(effortLabels).map(([value, label]) =>
<SelectItem key={value} value={value} disabled={value !== "provider_default" && !supportsEffort}>{label}</SelectItem>)}</SelectContent>
</Select>
{!supportsEffort && draft.reasoningEffort !== "provider_default" &&
<span role="alert" className="text-xs text-destructive">保存前请改为模型默认,或选择已声明支持推理强度的模型。</span>}
</BackendField>
<div className="space-y-3 rounded-2xl border border-hairline p-4">
<div className="text-sm font-medium">可用工具</div>
<p className="text-xs leading-5 text-muted-foreground">仅允许配置的 HTTP / MCP 服务端工具。浏览器交互工具由 Speaker 使用;技能不能增加工具权限。</p>
<ToolBindingEditor tools={serverTools} selectableTools={serverTools} value={draft.toolBindings} variables={{}}
onChange={toolBindings => setDraft(previous => ({ ...previous, toolBindings }))} />
</div>
<span role="alert" className="text-xs text-destructive">当前模型未声明支持推理强度,请改为模型默认或更换模型。</span>}
</BackendField> : <p className="text-xs leading-5 text-muted-foreground">推理强度使用模型默认配置;模型声明支持后可在此调整。</p>}
<div className="space-y-3 rounded-2xl border border-hairline p-4">
<div className="flex items-center justify-between gap-3">
<span className="text-sm font-medium">技能说明</span>
<span className="text-sm font-medium">技能说明(可选)</span>
<Button type="button" variant="outline" size="sm" onClick={() => setDraft(previous => ({ ...previous, skills: [...previous.skills, { name: "", content: "" }] }))}>
<Plus size={14} />添加技能
</Button>
@@ -85,30 +88,35 @@ export function ReasonerFields({ draft, setDraft, models, tools }: FieldsProps &
</div>;
}
type ConnectionFieldName = "url" | "directory" | "username" | "password" | "agent" | "modelProviderId" | "modelId";
function ConnectionField({ draft, setDraft, name, label, hint, placeholder }: FieldsProps & {
name: ConnectionFieldName; label: string; hint?: string; placeholder?: string;
}) {
return <BackendField label={label} hint={hint}>
<Input value={draft[name]} type={name === "password" ? "password" : "text"} autoComplete={name === "password" ? "new-password" : undefined}
placeholder={placeholder} onChange={event => setDraft(previous => ({ ...previous, [name]: event.target.value }))} />
</BackendField>;
}
export function OpenCodeFields({ draft, setDraft, hasPassword }: FieldsProps & { hasPassword: boolean }) {
function textField(key: "url" | "directory" | "username" | "password" | "agent" | "modelProviderId" | "modelId", label: string, hint?: string, placeholder?: string) {
return <BackendField label={label} hint={hint}>
<Input value={draft[key]} type={key === "password" ? "password" : "text"} autoComplete={key === "password" ? "new-password" : undefined}
placeholder={placeholder} onChange={event => setDraft(previous => ({ ...previous, [key]: event.target.value }))} />
</BackendField>;
}
return <div className="space-y-4">
{textField("url", "服务地址", "由平台后端访问;同机宿主运行可填写 http://127.0.0.1:4096。", "http://127.0.0.1:4096")}
{textField("directory", "远端工作目录", "使用 OpenCode 所在机器上的绝对路径。工作目录不等同于沙箱。", "/workspace/my-project")}
<ConnectionField draft={draft} setDraft={setDraft} name="url" label="服务地址" hint="填写平台后端可访问的 OpenCode 服务地址。" placeholder="http://127.0.0.1:4096" />
<ConnectionField draft={draft} setDraft={setDraft} name="directory" label="远端工作目录" hint="填写 OpenCode 所在机器上的项目绝对路径。" placeholder="/workspace/my-project" />
<ConnectionField draft={draft} setDraft={setDraft} name="password" label="连接密码(可选)"
hint={hasPassword ? "已设置密码;留空保留原凭据。" : "远端服务启用鉴权时填写。"} />
<p className="text-xs leading-5 text-muted-foreground">模型、工具、技能与执行权限默认使用远端配置。</p>
</div>;
}
export function OpenCodeAdvancedFields({ draft, setDraft }: FieldsProps) {
return <div className="space-y-4">
<ConnectionField draft={draft} setDraft={setDraft} name="username" label="服务用户名" hint="默认使用 opencode。" />
<ConnectionField draft={draft} setDraft={setDraft} name="agent" label="远端 Agent(可选)" hint="留空使用远端默认 Agent。" placeholder="build" />
<div className="grid gap-4 sm:grid-cols-2">
{textField("username", "服务用户名")}
{textField("password", "服务密码", hasPassword ? "已设置密码;留空保留原凭据。" : "对应 OPENCODE_SERVER_PASSWORD。")}
<ConnectionField draft={draft} setDraft={setDraft} name="modelProviderId" label="远端模型 Provider ID(可选)" hint="与 Model ID 同时填写,留空使用远端默认模型。" placeholder="anthropic" />
<ConnectionField draft={draft} setDraft={setDraft} name="modelId" label="远端 Model ID(可选)" hint="模型凭据在远端配置。" />
</div>
{textField("agent", "远端 Agent(可选)", "留空使用远端默认 Agent;执行权限由该 Agent 与运行环境管理。", "build")}
<div className="grid gap-4 sm:grid-cols-2">
{textField("modelProviderId", "远端模型 Provider ID(可选)", "与 Model ID 同时填写,留空使用远端默认模型。", "anthropic")}
{textField("modelId", "远端 Model ID(可选)", "模型凭据在远端配置。")}
</div>
<BackendField label="远端技能说明(可选)" hint="记录远端部署的技能及配置来源;此处不会上传或安装技能。">
<Textarea rows={3} value={draft.skillsNote} onChange={event => setDraft(previous => ({ ...previous, skillsNote: event.target.value }))} />
</BackendField>
<div className="rounded-xl border border-hairline bg-canvas-soft p-3 text-xs leading-6 text-muted-foreground">
文件、Shell、沙箱与技能权限由远端 OpenCode Agent 和运行账号管理。服务密码只用于连接服务,不会授予额外的执行权限。
</div>
<p className="text-xs leading-5 text-muted-foreground">文件、Shell、沙箱与技能权限由远端 Agent 和运行环境管理。</p>
</div>;
}
@@ -54,12 +54,12 @@ export function draftFromBackend(resource: TaskBackendResource): TaskBackendDraf
}
export function backendPayload(draft: TaskBackendDraft): TaskBackendUpsert {
if (!draft.name.trim()) throw new Error("请输入任务后端名称");
const timeoutSeconds = Number(draft.timeoutSeconds);
if (!Number.isInteger(timeoutSeconds) || timeoutSeconds < 30 || timeoutSeconds > 7200) {
throw new Error("任务时限须为 30–7200 秒的整数");
}
const base = { name: draft.name.trim(), description: draft.description.trim(), enabled: draft.enabled };
const name = draft.name.trim() || (draft.type === "reasoner" ? "内置 Reasoner" : "OpenCode");
const base = { name, description: draft.description.trim(), enabled: draft.enabled };
const common = { instructions: draft.instructions, timeoutSeconds, maxConcurrency: 1 as const };
if (draft.type === "reasoner") {
if (!draft.modelResourceId) throw new Error("请选择推理模型");