console: React 生产级重写(左列表 + 中对话 + 右详情)
旧单页 console.html 换成 Vite + React + TypeScript 单文件构建产物 (309525 字节,sha 90345ec8…,重建字节一致),源码在 cloud/console-react/,改页面只改 src 再 npm run build:console, 产物不手改。布局:左侧列表(模块导航 + 任务列表)、中间标准对话 (实时链路时间线 + 继续会话聊天框,共用 Chat 组件)、右侧详情 (元数据、操作、评审、审批、协调子任务、子代理、线程)。 API 与后端语义未改,key 只存浏览器 localStorage。 6 个 console 标记单测随实现改名更新(意图不变:壳确实调了 这些接口、turn 信息仍在详情里、旧 goal 输入仍不存在); P1 标记单测未动且通过。
This commit is contained in:
1 parent
3f4e849a93
commit
ebc5624893
57 files changed
+4789
-1746
No files matched your search
@@ -0,0 +1,121 @@
|
||||
import { useRef, useState } from "react";
|
||||
import { useConsole } from "../context";
|
||||
import { Card, Bar } from "../components/ui";
|
||||
import { gatherAttachments } from "../api/client";
|
||||
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 [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 [msg, setMsg] = useState("");
|
||||
|
||||
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) {
|
||||
if (approval === "full" && !window.confirm("完全访问 = 自动批准 + 不受工作区限制,agent 可读写工作区外路径。确认提交吗?")) return;
|
||||
body.approval = approval;
|
||||
}
|
||||
if (planMode) {
|
||||
if (approval === "full") { setMsg("计划模式不能与完全访问同用"); return; }
|
||||
if (goalMode) { setMsg("计划模式不能与 goal 模式同用"); return; }
|
||||
body.plan = 1;
|
||||
}
|
||||
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 || planMode) { setMsg("协调员任务不能与 goal/计划模式同用:协调员任务本身不跑模型回合,请取消 goal/计划勾选,或清空协调员 JSON"); return; }
|
||||
}
|
||||
const atts = gatherAttachments(filesRef.current?.files || null, dirRef.current?.files || null, imgRef.current?.files || null);
|
||||
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 = "";
|
||||
} 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>
|
||||
</select>
|
||||
<label><input type="checkbox" checked={goalMode} onChange={(e) => setGoalMode(e.target.checked)} /> goal 模式</label>
|
||||
<label><input type="checkbox" checked={planMode} onChange={(e) => setPlanMode(e.target.checked)} /> 计划模式(只读)</label>
|
||||
</Bar>
|
||||
<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}
|
||||
<Bar>
|
||||
<span className="muted">附件(放进任务工作区,图片与普通文件不区分):</span>
|
||||
<input ref={filesRef} type="file" multiple title="文件" />
|
||||
<input ref={dirRef} type="file" multiple title="文件夹" {...({ webkitdirectory: "" } as any)} />
|
||||
<input ref={imgRef} type="file" accept="image/*" multiple title="图片" />
|
||||
</Bar>
|
||||
<Bar>
|
||||
<button className="seal" onClick={submit}>提交任务</button>
|
||||
<span className="muted">{msg || ctx.dispatchMsg}</span>
|
||||
</Bar>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user