Files
codex-app-server/cloud/console-react/src/sections/DispatchSection.tsx
T

150 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useRef, useState } from "react";
import { useConsole } from "../context";
import { Card, Bar } from "../components/ui";
import { gatherAttachments } from "../api/client";
import { chvAttachmentBlocked, chvAvailability, chvAvailabilityText, isChvExecutor } from "../api/chv";
import { EFFORTS } from "../api/types";
export default function DispatchSection() {
const ctx = useConsole();
const [prompt, setPrompt] = useState("");
const [coordParts, setCoordParts] = useState("");
const [goalMode, setGoalMode] = useState(false);
const [goalInvincible, setGoalInvincible] = useState(false);
const [planMode, setPlanMode] = useState(false);
const [executor, setExecutor] = useState("docker");
const [model, setModel] = useState("");
const [modelCustom, setModelCustom] = useState("");
const [effort, setEffort] = useState("");
const [approval, setApproval] = useState("");
const [childModel, setChildModel] = useState("");
const [childModelCustom, setChildModelCustom] = useState("");
const [childEffort, setChildEffort] = useState("");
const filesRef = useRef<HTMLInputElement>(null);
const dirRef = useRef<HTMLInputElement>(null);
const imgRef = useRef<HTMLInputElement>(null);
const [hasAttachments, setHasAttachments] = useState(false);
const [msg, setMsg] = useState("");
const chvAvail = chvAvailability(ctx.workers, ctx.workersSummary, ctx.workersError);
const isChv = isChvExecutor(executor);
function refreshAttachments() {
setHasAttachments(
!!(filesRef.current?.files?.length || dirRef.current?.files?.length || imgRef.current?.files?.length),
);
}
async function submit() {
setMsg("");
if (!ctx.projectId) { setMsg("先选项目"); return; }
if (!prompt.trim()) { setMsg("prompt 必填"); return; }
const body: any = { project_id: ctx.projectId, prompt: prompt.trim() };
if (ctx.envSelect) body.environment_id = ctx.envSelect;
if (executor) body.executor = executor;
const m = modelCustom.trim() || model;
if (m) body.model = m;
if (effort) body.effort = effort;
const cm = childModelCustom.trim() || childModel;
if (cm) body.child_model = cm;
if (childEffort) body.child_effort = childEffort;
if (approval) {
// 选了完全访问也不再弹确认框:下拉里选 full 本身就是
// 显式选择,提交时直接送 approval;计划模式与 full
// 互斥仍在下面拦截。
body.approval = approval;
}
if (planMode) {
if (approval === "full") { setMsg("计划模式不能与完全访问同用"); return; }
if (goalMode || goalInvincible) { setMsg("计划模式不能与 goal 模式同用"); return; }
body.plan = 1;
}
// 无敌版 = goal 模式 + 任务不限时:勾了它就同时发 goal_mode 与
// no_time_limit(服务端已支持该字段);与普通 goal 勾选不冲突,
// 无敌版优先,单独勾普通 goal 时行为不变。
if (goalInvincible) { body.goal_mode = 1; body.no_time_limit = 1; }
else if (goalMode) body.goal_mode = 1;
const coordRaw = coordParts.trim();
if (coordRaw) {
try { body.coordinator_parts = JSON.parse(coordRaw); } catch { setMsg("协调员子任务 JSON 解析失败"); return; }
if (!Array.isArray(body.coordinator_parts)) { setMsg("协调员子任务必须是数组"); return; }
if (goalMode || goalInvincible || planMode) { setMsg("协调员任务不能与 goal/计划模式同用:协调员任务本身不跑模型回合,请取消 goal/计划勾选,或清空协调员 JSON"); return; }
}
const atts = gatherAttachments(filesRef.current?.files || null, dirRef.current?.files || null, imgRef.current?.files || null);
if (chvAttachmentBlocked(executor, atts.length > 0)) { setMsg("cloud-hypervisor 任务不支持附件:请先清空附件再提交(后端对带附件的 CHV 任务会 400)"); return; }
try {
const result = await ctx.dispatch(body, atts);
setMsg(result);
setPrompt(""); setCoordParts("");
if (filesRef.current) filesRef.current.value = "";
if (dirRef.current) dirRef.current.value = "";
if (imgRef.current) imgRef.current.value = "";
setHasAttachments(false);
} catch (e: any) { setMsg("提交失败:" + (e?.message || String(e))); }
}
return (
<Card id="nav-newtask" className="composer" title="新建任务">
<Bar>
<select aria-label="环境" value={ctx.envSelect} onChange={(e) => ctx.setEnvSelect(e.target.value)}>
<option value="">环境:不指定(走项目仓库)</option>
{ctx.envs.map((e) => <option key={e.id} value={e.id} disabled={e.status !== "ready"}>{e.name}({e.status})</option>)}
</select>
<select aria-label="执行器" value={executor} onChange={(e) => setExecutor(e.target.value)}>
<option value="docker">docker(默认,多用户下隔离最强)</option><option value="local">local</option><option value="cloud">cloud</option><option value="cloud-hypervisor">cloud-hypervisor(实验,默认关闭)</option>
</select>
<label><input type="checkbox" checked={goalMode} onChange={(e) => setGoalMode(e.target.checked)} /> goal 模式</label>
<label><input type="checkbox" checked={goalInvincible} onChange={(e) => setGoalInvincible(e.target.checked)} /> goal 模式无敌版</label>
<label><input type="checkbox" checked={planMode} onChange={(e) => setPlanMode(e.target.checked)} /> 计划模式(只读)</label>
</Bar>
{isChv ? <div className="muted">实验执行器,默认关闭:需后端已开启 CHV worker 且宿主有 KVM,否则任务无人认领、一直排队;带附件时建任务即被后端拒绝(错误文案原样显示在下方)。前端只传 executor 值,不绕过后端白名单;默认仍是 docker。</div> : null}
{isChv ? (
<div className="muted" aria-live="polite">
{chvAvailabilityText(chvAvail)}
{" "}<button type="button" onClick={ctx.refreshWorkers}>刷新 CHV 可用性</button>
</div>
) : null}
<textarea className="prompt stacked" placeholder="任务 prompt(必填)" value={prompt} onChange={(e) => setPrompt(e.target.value)} />
<textarea className="coord stacked" placeholder="协调员子任务 JSON(可选),留空为普通任务" value={coordParts} onChange={(e) => setCoordParts(e.target.value)} />
<div className="muted">协调员格式:[{"{"}"name":"后端","prompt":"做后端"{"}"},{"{"}"name":"前端","prompt":"做前端"{"}"}];提交后 worker 并行建子任务、等齐、合流汇总。不能与 goal/计划模式同用。协调员与 goal/计划互斥:勾了 goal 或计划模式时不要填协调员 JSON,提交前会校验并拦截。子任务费用记父任务创建人账;父任务创建时按分工数预留配额,余额不足时整个父任务被挡下。</div>
<Bar>
<select aria-label="模型" value={model} onChange={(e) => setModel(e.target.value)}>
<option value="">模型:默认(跟随配置)</option>
{ctx.models.map((m) => <option key={m} value={m}>{m}</option>)}
</select>
{ctx.modelCustomVisible ? <input type="text" placeholder="手动输入模型(中转列表取不到时用)" value={modelCustom} onChange={(e) => setModelCustom(e.target.value)} /> : null}
<select aria-label="推理强度" value={effort} onChange={(e) => setEffort(e.target.value)}>
<option value="">推理强度:默认(跟随配置)</option>
{EFFORTS.map((x) => <option key={x} value={x}>{x}</option>)}
</select>
<select aria-label="权限" value={approval} onChange={(e) => setApproval(e.target.value)}>
<option value="">权限:默认(跟随配置)</option><option value="ask">请求批准</option><option value="auto">帮我批准</option><option value="full">完全访问</option>
</select>
</Bar>
<Bar>
<select aria-label="子任务模型" value={childModel} onChange={(e) => setChildModel(e.target.value)}>
<option value="">子任务模型:跟随本任务模型</option>
{ctx.models.map((m) => <option key={m} value={m}>{m}</option>)}
</select>
<input type="text" placeholder="子任务模型手动输入(中转列表里没有时填,如 gpt-6-luna;留空=跟随本任务模型)" value={childModelCustom} onChange={(e) => setChildModelCustom(e.target.value)} />
<select aria-label="子任务强度" value={childEffort} onChange={(e) => setChildEffort(e.target.value)}>
<option value="">子任务强度:跟随本任务强度</option>
{EFFORTS.map((x) => <option key={x} value={x}>{x}</option>)}
</select>
</Bar>
<div className="muted">子任务模型/强度是派生默认:本任务用 marker task-create 或协调员分工派生的子任务,未显式写模型/强度时用它;块/分工里显式写了则覆盖它。也可以不在这里填、直接在 prompt 里写“子任务用 gpt-6-luna max”,建任务时服务端会解析成同一对字段(任务详情里可核对)。</div>
{goalMode ? <div className="muted">goal 模式:输入框内容即目标,不需要专用 prompt 与验收命令;不限轮次,模型确认达成后在回复末尾输出“目标已达成”即结束,未达成会一直运行,可用取消停止。</div> : null}
{goalInvincible ? <div className="muted">goal 模式无敌版:goal 模式 + 任务不限时(去掉 worker/docker 总超时;单轮 600 秒护栏还在,不限的是任务总时长与总轮次)。服务端默认的自动续跑与断点镜像同时生效。</div> : null}
<Bar>
<span className="muted">附件(放进任务工作区,图片与普通文件不区分):</span>
<input ref={filesRef} type="file" multiple title="文件" aria-label="附件文件" disabled={isChv} onChange={refreshAttachments} />
<input ref={dirRef} type="file" multiple title="文件夹" aria-label="附件文件夹" disabled={isChv} onChange={refreshAttachments} {...({ webkitdirectory: "" } as any)} />
<input ref={imgRef} type="file" accept="image/*" multiple title="图片" aria-label="附件图片" disabled={isChv} onChange={refreshAttachments} />
</Bar>
{isChv ? <div className="muted">cloud-hypervisor 任务不支持附件:附件输入已禁用(后端对带附件的 CHV 任务会 400,错误文案原样显示在下方;切换回其他执行器可恢复附件输入)。{hasAttachments ? <>当前仍有已选附件,请先清空再提交。 <button type="button" onClick={() => { if (filesRef.current) filesRef.current.value = ""; if (dirRef.current) dirRef.current.value = ""; if (imgRef.current) imgRef.current.value = ""; setHasAttachments(false); }}>清空附件</button></> : ""}</div> : null}
<Bar>
<button className="seal" onClick={submit}>提交任务</button>
<span className="muted" role="status" aria-live="polite">{msg || ctx.dispatchMsg}</span>
</Bar>
</Card>
);
}