Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7983567491 | ||
|
|
f6df76ee8f | ||
|
|
834eced686 | ||
|
|
4d3ea048a5 | ||
|
|
d5642c6252 |
No files matched your search
+6
-1
@@ -734,7 +734,12 @@ interrupted 且终态任务 cancel 返回 400、心跳后 sweep 不扫刚心跳
|
||||
非租户模式完全一致(`/app/work`、codex-home、repos/<项目>、
|
||||
test-repo),只是宿主挂载源按项目分开——容器内看不到其它
|
||||
项目的任何文件;同时每个项目进独立的 Docker 网络
|
||||
`codex-<项目>`(容器跨项目网络隔离),容器统一加
|
||||
`codex-<项目>`(容器跨项目网络隔离),建网时显式
|
||||
`--subnet` 把子网固定在 172.16.0.0/16 内(/24 按网络名
|
||||
确定性划段),保证落在宿主 iptables 的代理放行范围
|
||||
(172.16.0.0/12)内;2026-11-07 之前建好的范围外旧网
|
||||
(如 codex-smoke-20261107)不会自动纠正,须人工删除后
|
||||
重建。容器统一加
|
||||
`--cap-drop=ALL --security-opt=no-new-privileges`。codex-home
|
||||
只播种模板的 config.toml(relay 端点与 provider 名,不含
|
||||
密钥值),绝不复制 thread_history。注意:内层 codex 沙箱仍
|
||||
|
||||
@@ -29,14 +29,19 @@ export const paths = {
|
||||
environmentsCreate: "/environments",
|
||||
environmentArchive: (id: string) => "/environments/" + enc(id) + "/archive",
|
||||
tasks: (pid: string) => "/tasks?project_id=" + enc(pid),
|
||||
tasksPage: (pid: string, limit: number, offset: number, status?: string, executor?: string, q?: string) => {
|
||||
tasksPage: (pid: string, limit: number, offset: number, status?: string, executor?: string, q?: string, kind?: string) => {
|
||||
let u = "/tasks?project_id=" + enc(pid) + "&limit=" + limit + "&offset=" + offset + "&order=desc";
|
||||
if (status) u += "&status=" + enc(status);
|
||||
if (executor) u += "&executor=" + enc(executor);
|
||||
if (q && q.trim()) u += "&q=" + enc(q.trim());
|
||||
if (kind) u += "&kind=" + enc(kind);
|
||||
return u;
|
||||
},
|
||||
tasksByParent: (pid: string, parent: string, kind?: string) => {
|
||||
let u = "/tasks?project_id=" + enc(pid) + "&parent_task_id=" + enc(parent);
|
||||
if (kind) u += "&kind=" + enc(kind);
|
||||
return u;
|
||||
},
|
||||
tasksByParent: (pid: string, parent: string) => "/tasks?project_id=" + enc(pid) + "&parent_task_id=" + enc(parent),
|
||||
tasksCreate: "/tasks",
|
||||
task: (id: string) => "/tasks/" + enc(id),
|
||||
taskAction: (id: string, kind: string) => "/tasks/" + enc(id) + "/" + kind,
|
||||
|
||||
@@ -19,6 +19,9 @@ export interface Task {
|
||||
turn_id?: string; root_turn_id?: string; plan?: number | boolean; forked_from?: string;
|
||||
coordinator_parts?: Array<{ name: string; prompt?: string }> | null; parent_task_id?: string;
|
||||
coordinator_part?: string; goal_mode?: number | boolean; verify_command?: string;
|
||||
// 旁聊(Muse 角色对齐 P2):服务端 _task_to_dict 回 kind,
|
||||
// 缺列/NULL 的老行按 "main" 计。
|
||||
kind?: "main" | "side";
|
||||
no_time_limit?: number | boolean;
|
||||
environment_id?: string; attachments?: string[]; model?: string; effort?: string;
|
||||
child_model?: string; child_effort?: string; approval?: string;
|
||||
@@ -71,6 +74,8 @@ export interface DispatchBody {
|
||||
model?: string; effort?: string; child_model?: string; child_effort?: string;
|
||||
approval?: string; plan?: number; goal_mode?: number; no_time_limit?: number; upload_id?: string;
|
||||
coordinator_parts?: Array<{ name: string; prompt?: string }>;
|
||||
// 旁聊(Muse 角色对齐 P2):kind="side" 必须配 parent_task_id。
|
||||
kind?: "side"; parent_task_id?: string;
|
||||
}
|
||||
export interface ContinueBody {
|
||||
message: string; model?: string; effort?: string; child_model?: string;
|
||||
|
||||
@@ -21,6 +21,7 @@ export interface ConsoleCtx {
|
||||
models: string[]; modelCustomVisible: boolean;
|
||||
tasks: Task[]; taskCountText: string; taskError: string; refreshTasks: () => void;
|
||||
statusFilter: string; setStatusFilter: (v: string) => void; executorFilter: string; setExecutorFilter: (v: string) => void; searchText: string; setSearchText: (v: string) => void;
|
||||
kindFilter: string; setKindFilter: (v: string) => void;
|
||||
filteredTasks: Task[];
|
||||
taskPage: number; taskTotalPages: number; taskTotal: number; taskStatusCounts: Record<string, number>; goToTaskPage: (p: number) => void;
|
||||
currentTask: Task | null; detailBody: string; actionMsg: string; actionReasons: string;
|
||||
@@ -28,6 +29,8 @@ export interface ConsoleCtx {
|
||||
forkVisible: boolean; setForkVisible: (v: boolean) => void; forkTask: (msg: string) => Promise<void>; forkMsg: string;
|
||||
// Continue input is ContinuePanel (ChatInput); its send path is sendChatMessage below.
|
||||
coordChildren: Task[]; coordText: string; agents: Agent[]; agentsNote: string;
|
||||
// 旁聊(Muse 角色对齐 P2):当前任务的旁聊列表 + 派发器旁聊模式的父任务。
|
||||
sideThreads: Task[]; sideParentId: string | null; setSideParentId: (v: string | null) => void;
|
||||
reviews: Review[]; reviewMsg: string; reviewError: string; refreshReviews: () => void; postReview: (body: ReviewBody) => Promise<void>; submitFindings: (id: string) => void;
|
||||
traceEntries: TraceEntry[]; traceNote: string; traceDone: string; traceKind: string; setTraceKind: (v: string) => void; downloadTrace: () => void; traceMsg: string;
|
||||
sendChatMessage: (text: string) => Promise<void>; chatMsg: string;
|
||||
|
||||
@@ -23,6 +23,10 @@ export function useConsoleStore(): ConsoleCtx {
|
||||
const onTasksReload = useCallback(() => { void taskList.refreshTasks(undefined, projectIdRef.current); }, [taskList]);
|
||||
const detail = useTaskDetail(getRole, onTasksReload);
|
||||
const [refreshStamp, setRefreshStamp] = useState("--:--:--");
|
||||
// 旁聊(Muse 角色对齐 P2):派发器的旁聊父任务。列表“开旁聊”
|
||||
// 点选主聊后填这里,派发器读到即进旁聊模式;提交成功或点
|
||||
// “取消旁聊模式”后清空。放胶水层,不进 App.tsx。
|
||||
const [sideParentId, setSideParentId] = useState<string | null>(null);
|
||||
|
||||
const selectProject = useCallback((id: string) => {
|
||||
catalog.setProjectId(id);
|
||||
@@ -126,6 +130,7 @@ export function useConsoleStore(): ConsoleCtx {
|
||||
refreshTasks: () => void taskList.refreshTasks(undefined, projectIdRef.current),
|
||||
statusFilter: taskList.statusFilter, setStatusFilter: taskList.setStatusFilter,
|
||||
executorFilter: taskList.executorFilter, setExecutorFilter: taskList.setExecutorFilter,
|
||||
kindFilter: taskList.kindFilter, setKindFilter: taskList.setKindFilter,
|
||||
searchText: taskList.searchText, setSearchText: taskList.setSearchText,
|
||||
filteredTasks: taskList.filteredTasks,
|
||||
taskPage: taskList.page, taskTotalPages: taskList.totalPages, taskTotal: taskList.taskTotal,
|
||||
@@ -135,6 +140,7 @@ export function useConsoleStore(): ConsoleCtx {
|
||||
doAction: detail.doAction, taskCost: detail.taskCost, costBox: detail.costBox, costSummary: detail.costSummary,
|
||||
forkVisible: detail.forkVisible, setForkVisible: detail.setForkVisible, forkTask: detail.forkTask, forkMsg: detail.forkMsg,
|
||||
coordChildren: detail.coordChildren, coordText: "", agents: detail.agents, agentsNote: detail.agentsNote,
|
||||
sideThreads: detail.sideThreads, sideParentId, setSideParentId,
|
||||
reviews: detail.reviews, reviewMsg: detail.reviewMsg, reviewError: detail.reviewError,
|
||||
refreshReviews: detail.refreshReviews, postReview: detail.postReview, submitFindings: detail.submitFindings,
|
||||
traceEntries: detail.traceEntries, traceNote: detail.traceNote, traceDone: detail.traceDone,
|
||||
|
||||
@@ -19,6 +19,9 @@ export function useTaskDetail(getRole: (pid?: string) => string | null, onTasksR
|
||||
const [forkVisible, setForkVisible] = useState(false);
|
||||
const [forkMsg, setForkMsg] = useState("");
|
||||
const [coordChildren, setCoordChildren] = useState<Task[]>([]);
|
||||
// 旁聊(Muse 角色对齐 P2):当前任务挂着的旁聊线程(kind=side
|
||||
// 且 parent_task_id=本任务),与协调员子任务同款拉取方式。
|
||||
const [sideThreads, setSideThreads] = useState<Task[]>([]);
|
||||
const [agents, setAgents] = useState<Agent[]>([]);
|
||||
const [agentsNote, setAgentsNote] = useState("");
|
||||
const [reviews, setReviews] = useState<Review[]>([]);
|
||||
@@ -71,6 +74,13 @@ export function useTaskDetail(getRole: (pid?: string) => string | null, onTasksR
|
||||
} catch { setCoordChildren([]); }
|
||||
}, []);
|
||||
|
||||
const loadSideThreads = useCallback(async (task: Task) => {
|
||||
try {
|
||||
const d = await api<{ tasks: Task[] }>(paths.tasksByParent(task.project_id || "", task.id, "side"));
|
||||
setSideThreads(d.tasks || []);
|
||||
} catch { setSideThreads([]); }
|
||||
}, []);
|
||||
|
||||
const loadCost = useCallback(async (taskId: string) => {
|
||||
try {
|
||||
const c = await api<TaskCost>(paths.taskCost(taskId));
|
||||
@@ -248,6 +258,7 @@ export function useTaskDetail(getRole: (pid?: string) => string | null, onTasksR
|
||||
void startTrace(taskId, t.status);
|
||||
void loadAgents(taskId);
|
||||
void loadCoordChildren(t);
|
||||
void loadSideThreads(t);
|
||||
void loadChatHistory(taskId);
|
||||
void loadFollowups(taskId);
|
||||
void loadPeerMessages(taskId);
|
||||
@@ -259,7 +270,7 @@ export function useTaskDetail(getRole: (pid?: string) => string | null, onTasksR
|
||||
setCurrentTask(null);
|
||||
setDetailBody("加载失败:" + e.message);
|
||||
}
|
||||
}, [stopTrace, startTrace, loadAgents, loadCoordChildren, loadChatHistory, loadFollowups, loadPeerMessages, loadThread, loadThreads, loadReviews, loadApprovals, loadCost, getRole]);
|
||||
}, [stopTrace, startTrace, loadAgents, loadCoordChildren, loadSideThreads, loadChatHistory, loadFollowups, loadPeerMessages, loadThread, loadThreads, loadReviews, loadApprovals, loadCost, getRole]);
|
||||
|
||||
const refreshDetailLists = useCallback(async () => {
|
||||
const t = currentTaskRef.current;
|
||||
@@ -277,12 +288,13 @@ export function useTaskDetail(getRole: (pid?: string) => string | null, onTasksR
|
||||
void loadReviews(t.id);
|
||||
void loadApprovals(t.id);
|
||||
void loadCoordChildren(t);
|
||||
void loadSideThreads(t);
|
||||
void loadAgents(t.id);
|
||||
void loadPeerMessages(t.id);
|
||||
void loadThread(t.id);
|
||||
void loadThreads(t.id);
|
||||
void loadCost(t.id);
|
||||
}, [loadFollowups, loadChatHistory, loadReviews, loadApprovals, loadCoordChildren, loadAgents, loadPeerMessages, loadThread, loadThreads, loadCost]);
|
||||
}, [loadFollowups, loadChatHistory, loadReviews, loadApprovals, loadCoordChildren, loadSideThreads, loadAgents, loadPeerMessages, loadThread, loadThreads, loadCost]);
|
||||
|
||||
const doAction = useCallback(async (kind: string) => {
|
||||
const t = currentTaskRef.current;
|
||||
@@ -423,7 +435,7 @@ export function useTaskDetail(getRole: (pid?: string) => string | null, onTasksR
|
||||
|
||||
return {
|
||||
currentTask, detailBody, actionMsg, actionReasons, costBox, costSummary, forkVisible, setForkVisible, forkMsg,
|
||||
coordChildren, agents, agentsNote, reviews, reviewMsg, reviewError, traceEntries, traceNote, traceDone,
|
||||
coordChildren, sideThreads, agents, agentsNote, reviews, reviewMsg, reviewError, traceEntries, traceNote, traceDone,
|
||||
traceKind, setTraceKind, traceMsg, followups, approvals, approvalMsg, peerMessages, peerMsg,
|
||||
threadText, threadGateNote, threadMsg, threadList, chatMsg, chatHistory,
|
||||
showDetail, refreshDetailLists, doAction, taskCost, forkTask, postReview, submitFindings,
|
||||
|
||||
@@ -21,13 +21,14 @@ export function useTaskList() {
|
||||
const [taskError, setTaskError] = useState("");
|
||||
const [statusFilter, setStatusFilter] = useState("");
|
||||
const [executorFilter, setExecutorFilter] = useState("");
|
||||
const [kindFilter, setKindFilter] = useState("");
|
||||
const [searchText, setSearchText] = useState("");
|
||||
const [page, setPageState] = useState(0);
|
||||
const [taskTotal, setTaskTotal] = useState(0);
|
||||
const [taskStatusCounts, setTaskStatusCounts] = useState<Record<string, number>>({});
|
||||
const pageRef = useRef(0);
|
||||
const filterRef = useRef({ status: "", executor: "", q: "" });
|
||||
filterRef.current = { status: statusFilter, executor: executorFilter, q: searchText };
|
||||
const filterRef = useRef({ status: "", executor: "", q: "", kind: "" });
|
||||
filterRef.current = { status: statusFilter, executor: executorFilter, q: searchText, kind: kindFilter };
|
||||
const lastProjectRef = useRef("");
|
||||
|
||||
const refreshTasks = useCallback(async (pid?: string, currentPid?: string, pageOverride?: number) => {
|
||||
@@ -38,7 +39,7 @@ export function useTaskList() {
|
||||
const f = filterRef.current;
|
||||
try {
|
||||
const d = await api<{ tasks: Task[]; total?: number; status_counts?: Record<string, number> }>(
|
||||
paths.tasksPage(id, TASK_PAGE_SIZE, p * TASK_PAGE_SIZE, f.status, f.executor, f.q));
|
||||
paths.tasksPage(id, TASK_PAGE_SIZE, p * TASK_PAGE_SIZE, f.status, f.executor, f.q, f.kind));
|
||||
setTasks(d.tasks || []);
|
||||
setTaskTotal(d.total ?? (d.tasks || []).length);
|
||||
setTaskStatusCounts(d.status_counts || {});
|
||||
@@ -66,13 +67,14 @@ export function useTaskList() {
|
||||
const t = setTimeout(() => { void refreshTasks(lastProjectRef.current, undefined, 0); }, 300);
|
||||
return () => clearTimeout(t);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [statusFilter, executorFilter, searchText]);
|
||||
}, [statusFilter, executorFilter, searchText, kindFilter]);
|
||||
|
||||
// Filter/project changes reset to page 1: the old page may not
|
||||
// exist under the new filter, and staying on it would show an
|
||||
// empty page with a misleading "共 N 个" count.
|
||||
const setStatusFilterPage = useCallback((v: string) => { setStatusFilter(v); setPage(0); }, [setPage]);
|
||||
const setExecutorFilterPage = useCallback((v: string) => { setExecutorFilter(v); setPage(0); }, [setPage]);
|
||||
const setKindFilterPage = useCallback((v: string) => { setKindFilter(v); setPage(0); }, [setPage]);
|
||||
const setSearchTextPage = useCallback((v: string) => { setSearchText(v); setPage(0); }, [setPage]);
|
||||
const refreshTasksFirstPage = useCallback(async (pid?: string) => {
|
||||
setPage(0);
|
||||
@@ -99,6 +101,7 @@ export function useTaskList() {
|
||||
return {
|
||||
tasks, taskError, statusFilter, setStatusFilter: setStatusFilterPage,
|
||||
executorFilter, setExecutorFilter: setExecutorFilterPage,
|
||||
kindFilter, setKindFilter: setKindFilterPage,
|
||||
searchText, setSearchText: setSearchTextPage,
|
||||
filteredTasks, taskCountText, refreshTasks, refreshTasksFirstPage,
|
||||
page, totalPages, taskTotal, taskStatusCounts, goToPage, dispatch,
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
import { useRef, useState } from "react";
|
||||
import { useConsole } from "../context";
|
||||
import { Card, Bar } from "../components/ui";
|
||||
import { gatherAttachments } from "../api/client";
|
||||
import { gatherAttachments, shortId } from "../api/client";
|
||||
import { EFFORTS } from "../api/types";
|
||||
|
||||
export default function DispatchSection() {
|
||||
const ctx = useConsole();
|
||||
// 旁聊模式(Muse 角色对齐 P2):列表“开旁聊”点选主聊后,
|
||||
// sideParentId 即父任务 id;此时隐藏环境/协调员/附件,
|
||||
// 执行器默认不指定(服务端旁聊默认 local),提交带
|
||||
// kind="side" + parent_task_id。
|
||||
const isSide = !!ctx.sideParentId;
|
||||
const [prompt, setPrompt] = useState("");
|
||||
const [coordParts, setCoordParts] = useState("");
|
||||
const [goalMode, setGoalMode] = useState(false);
|
||||
const [planMode, setPlanMode] = useState(false);
|
||||
const [executor, setExecutor] = useState("docker");
|
||||
const [sideExecutor, setSideExecutor] = useState("");
|
||||
const [model, setModel] = useState("");
|
||||
const [modelCustom, setModelCustom] = useState("");
|
||||
const [effort, setEffort] = useState("");
|
||||
@@ -28,8 +34,15 @@ export default function DispatchSection() {
|
||||
if (!ctx.projectId) { setMsg("先选项目"); return; }
|
||||
if (!prompt.trim()) { setMsg("prompt 必填"); return; }
|
||||
const body: any = { project_id: ctx.projectId, prompt: prompt.trim() };
|
||||
if (isSide) {
|
||||
body.kind = "side";
|
||||
body.parent_task_id = ctx.sideParentId;
|
||||
// 旁聊默认不指定执行器(服务端填 local);显式选了才送。
|
||||
if (sideExecutor) body.executor = sideExecutor;
|
||||
} else {
|
||||
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;
|
||||
@@ -52,15 +65,20 @@ export default function DispatchSection() {
|
||||
// 单独发不带 no_time_limit 的 goal_mode。
|
||||
if (goalMode) { body.goal_mode = 1; body.no_time_limit = 1; }
|
||||
const coordRaw = coordParts.trim();
|
||||
if (coordRaw) {
|
||||
if (coordRaw && !isSide) {
|
||||
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);
|
||||
const atts = isSide ? [] : gatherAttachments(filesRef.current?.files || null, dirRef.current?.files || null, imgRef.current?.files || null);
|
||||
try {
|
||||
const result = await ctx.dispatch(body, atts);
|
||||
if (isSide) {
|
||||
setMsg("旁聊已创建(无 worktree)");
|
||||
ctx.setSideParentId(null);
|
||||
} else {
|
||||
setMsg(result);
|
||||
}
|
||||
setPrompt(""); setCoordParts("");
|
||||
if (filesRef.current) filesRef.current.value = "";
|
||||
if (dirRef.current) dirRef.current.value = "";
|
||||
@@ -70,20 +88,35 @@ export default function DispatchSection() {
|
||||
|
||||
return (
|
||||
<Card id="nav-newtask" className="composer" title="新建任务">
|
||||
{isSide ? (
|
||||
<div className="muted">旁聊模式:挂在父任务 {shortId(ctx.sideParentId)} 下,不建 worktree/分支/容器;终态后结论回写父级。
|
||||
{" "}<button type="button" onClick={() => ctx.setSideParentId(null)}>取消旁聊模式</button>
|
||||
</div>
|
||||
) : null}
|
||||
<Bar>
|
||||
{isSide ? null : (
|
||||
<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>
|
||||
)}
|
||||
{isSide ? (
|
||||
<select aria-label="执行器" value={sideExecutor} onChange={(e) => setSideExecutor(e.target.value)}>
|
||||
<option value="">执行器:不指定(默认 local)</option><option value="local">local</option><option value="docker">docker</option><option value="cloud">cloud</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)} />
|
||||
{isSide ? null : (<>
|
||||
<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>
|
||||
@@ -111,12 +144,14 @@ export default function DispatchSection() {
|
||||
</Bar>
|
||||
<div className="muted">子任务模型/强度是派生默认:本任务用 marker task-create 或协调员分工派生的子任务,未显式写模型/强度时用它;块/分工里显式写了则覆盖它。也可以不在这里填、直接在 prompt 里写“子任务用 gpt-6-luna max”,建任务时服务端会解析成同一对字段(任务详情里可核对)。</div>
|
||||
{goalMode ? <div className="muted">goal 模式:输入框内容即目标,不需要专用 prompt 与验收命令;不限轮次,模型确认达成后在回复末尾输出“目标已达成”即结束,未达成会一直运行,可用取消停止;任务不限时(去掉 worker/docker 总超时;单轮 600 秒护栏还在)。服务端默认的自动续跑与断点镜像同时生效。</div> : null}
|
||||
{isSide ? 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>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { firstChar, durationOf, tokenOf, fmtCostUsd } from "../api/taskMeta";
|
||||
import DetailActions from "./detail/DetailActions";
|
||||
import BrowserPanel from "./detail/BrowserPanel";
|
||||
import ReviewPanel from "./detail/ReviewPanel";
|
||||
import { CoordPanel, AgentsPanel, ApprovalPanel, PeerPanel, ThreadPanel } from "./detail/DetailLists";
|
||||
import { CoordPanel, SideThreadPanel, AgentsPanel, ApprovalPanel, PeerPanel, ThreadPanel } from "./detail/DetailLists";
|
||||
|
||||
// Task inspector (redesign 2026-10-30): the old detail stack —
|
||||
// 概览 + 基本信息 + prompt + 操作 + 协调 + 子代理 + 评审 +
|
||||
@@ -134,9 +134,11 @@ export default function TaskDetailPanel() {
|
||||
<div><dt>权限</dt><dd>{t.approval || "默认"}</dd></div>
|
||||
<div><dt>项目</dt><dd>{shortId(t.project_id)}</dd></div>
|
||||
{modeText ? <div><dt>模式</dt><dd>{modeText}</dd></div> : null}
|
||||
{t.kind === "side" ? <div><dt>类型</dt><dd>旁聊(无 worktree/分支/容器)</dd></div> : null}
|
||||
{t.forked_from ? <div><dt>分支自</dt><dd>{shortId(t.forked_from)}</dd></div> : null}
|
||||
{t.coordinator_parts ? <div><dt>协调员</dt><dd>{t.coordinator_parts.length} 个子任务({t.coordinator_parts.map((p) => p.name).join("、")})</dd></div> : null}
|
||||
{t.parent_task_id ? <div><dt>协调子任务</dt><dd>父任务 {shortId(t.parent_task_id)}{t.coordinator_part ? "(" + t.coordinator_part + ")" : ""}</dd></div> : null}
|
||||
{t.parent_task_id && t.kind === "side" ? <div><dt>父会话</dt><dd>父任务 {shortId(t.parent_task_id)}{" "}<button type="button" onClick={() => ctx.showDetail(t.parent_task_id as string)}>跳转到父任务</button></dd></div> : null}
|
||||
{t.parent_task_id && t.kind !== "side" ? <div><dt>协调子任务</dt><dd>父任务 {shortId(t.parent_task_id)}{t.coordinator_part ? "(" + t.coordinator_part + ")" : ""}</dd></div> : null}
|
||||
{(t.attachments && t.attachments.length) ? <div><dt>附件</dt><dd>{t.attachments.join("、")}</dd></div> : null}
|
||||
{t.pr_url ? <div><dt>PR</dt><dd><a href={t.pr_url} target="_blank" rel="noopener noreferrer">PR #{t.pr_number}</a></dd></div> : null}
|
||||
</dl>
|
||||
@@ -151,6 +153,7 @@ export default function TaskDetailPanel() {
|
||||
</Card>
|
||||
</>) : tab === "collab" ? (<>
|
||||
<Card title="协调子任务"><CoordPanel /></Card>
|
||||
<Card title="旁聊"><SideThreadPanel /></Card>
|
||||
<Card title="子代理链路"><AgentsPanel /></Card>
|
||||
<Card title="给其他任务发消息"><PeerPanel /></Card>
|
||||
</>) : tab === "review" ? (<>
|
||||
|
||||
@@ -27,7 +27,17 @@ export default function TaskListSection({ onNew }: { onNew: () => void }) {
|
||||
</div>
|
||||
<div className="list-section-head">
|
||||
<span>主要聊天</span>
|
||||
<span>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="开旁聊"
|
||||
title={ctx.currentTask && ctx.currentTask.kind !== "side" && !ctx.currentTask.coordinator_parts ? "在选中主聊下开一个旁聊(不建 worktree)" : "先选中一个主聊任务,才能在它下面开旁聊"}
|
||||
disabled={!ctx.currentTask || ctx.currentTask.kind === "side" || !!ctx.currentTask.coordinator_parts}
|
||||
onClick={() => { if (ctx.currentTask) { ctx.setSideParentId(ctx.currentTask.id); onNew(); } }}
|
||||
>开旁聊</button>
|
||||
{" "}
|
||||
<button type="button" aria-label="派发新任务" title="派发新任务" onClick={onNew}>+</button>
|
||||
</span>
|
||||
</div>
|
||||
<div className="bar">
|
||||
<select aria-label="状态筛选" value={ctx.statusFilter} onChange={(e) => ctx.setStatusFilter(e.target.value)}>
|
||||
@@ -37,6 +47,9 @@ export default function TaskListSection({ onNew }: { onNew: () => void }) {
|
||||
<select aria-label="执行器筛选" value={ctx.executorFilter} onChange={(e) => ctx.setExecutorFilter(e.target.value)}>
|
||||
<option value="">执行器:全部</option><option value="local">local</option><option value="docker">docker</option><option value="cloud">cloud</option>
|
||||
</select>
|
||||
<select aria-label="类型筛选" value={ctx.kindFilter} onChange={(e) => ctx.setKindFilter(e.target.value)}>
|
||||
<option value="">类型:全部</option><option value="main">主聊</option><option value="side">旁聊</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="bar">
|
||||
<button onClick={ctx.refreshTasks}>刷新</button>
|
||||
@@ -62,7 +75,7 @@ export default function TaskListSection({ onNew }: { onNew: () => void }) {
|
||||
<span className="issue-first" title={"任务首字:" + fc} aria-hidden="true">{fc}</span>
|
||||
<span style={{ flex: 1, minWidth: 0 }}>
|
||||
<div className="issue-title">
|
||||
{t.plan ? "[计划] " : ""}{t.forked_from ? "[分支] " : ""}{t.coordinator_parts ? "[协调] " : ""}{t.parent_task_id ? "[协调子任务] " : ""}{String(t.prompt || shortId(t.id)).slice(0, 60)}
|
||||
{t.plan ? "[计划] " : ""}{t.forked_from ? "[分支] " : ""}{t.coordinator_parts ? "[协调] " : ""}{t.parent_task_id ? (t.kind === "side" ? "[旁聊] " : "[协调子任务] ") : ""}{String(t.prompt || shortId(t.id)).slice(0, 60)}
|
||||
</div>
|
||||
<div className="issue-meta">
|
||||
{shortId(t.id)} · <span className={"st-" + t.status}>{t.status}</span> · {t.executor || ""}{t.updated_at ? " · " + new Date(t.updated_at * 1000).toLocaleString() : ""}
|
||||
|
||||
@@ -12,18 +12,23 @@ export default function DetailActions() {
|
||||
if (!t) return null;
|
||||
const terminal = TRACE_TERMINAL.includes(t.status);
|
||||
const retryOk = ["failed", "interrupted", "merge-failed"].includes(t.status);
|
||||
// 旁聊(Muse 角色对齐 P2):无 worktree/分支,后端 /fork /merge
|
||||
// /pr 一律 409,这里只留重试/取消/查成本,不渲染那三处入口。
|
||||
const isSide = t.kind === "side";
|
||||
return (
|
||||
<>
|
||||
<Bar>
|
||||
<button disabled={!retryOk} title={retryOk ? "" : "仅 failed/interrupted/merge-failed 可重试"} onClick={() => ctx.doAction("retry")}>重试</button>
|
||||
<button className="danger" disabled={terminal} title={terminal ? "终态任务不可取消" : ""} onClick={() => ctx.doAction("cancel")}>取消</button>
|
||||
{isSide ? null : (<>
|
||||
<button disabled={t.status !== "completed"} title={t.status !== "completed" ? "仅 completed 可合并" : ""} onClick={() => ctx.doAction("merge")}>合并(completed → merged)</button>
|
||||
<button disabled={t.status !== "merged"} title={t.status !== "merged" ? "仅 merged 可提 PR" : ""} onClick={() => ctx.doAction("pr")}>提 PR(须 merged)</button>
|
||||
{terminal ? <button onClick={() => ctx.setForkVisible(true)}>分支</button> : null}
|
||||
</>)}
|
||||
{terminal && !isSide ? <button onClick={() => ctx.setForkVisible(true)}>分支</button> : null}
|
||||
<button onClick={ctx.taskCost}>查成本</button>
|
||||
<span className="muted">{ctx.actionMsg} {ctx.actionReasons}</span>
|
||||
</Bar>
|
||||
{ctx.forkVisible ? (
|
||||
{ctx.forkVisible && !isSide ? (
|
||||
<div>
|
||||
<input type="text" placeholder="分支首轮消息(必填,带历史的新会话)" style={{ width: "100%", boxSizing: "border-box", margin: "6px 0" }} value={forkMsg} onChange={(e) => setForkMsg(e.target.value)} />
|
||||
<Bar><button className="seal" onClick={async () => { await ctx.forkTask(forkMsg); setForkMsg(""); }}>创建分支任务</button><span className="muted">{ctx.forkMsg}</span></Bar>
|
||||
|
||||
@@ -19,6 +19,23 @@ export function CoordPanel() {
|
||||
);
|
||||
}
|
||||
|
||||
// 旁聊(Muse 角色对齐 P2):当前任务挂着的旁聊线程,与协调员
|
||||
// 子任务列表同款组件复用;点行切到该旁聊的详情。
|
||||
export function SideThreadPanel() {
|
||||
const ctx = useConsole();
|
||||
if (!ctx.currentTask) return <div className="muted">点任务行查看详情后,这里显示旁聊</div>;
|
||||
if (ctx.sideThreads.length === 0) return <div className="muted">(暂无旁聊;列表“开旁聊”可在此任务下新建,不建 worktree)</div>;
|
||||
return (
|
||||
<DataListRaw>
|
||||
{ctx.sideThreads.map((k) => (
|
||||
<li key={k.id} onClick={() => ctx.showDetail(k.id)} style={{ cursor: "pointer" }}>
|
||||
<span className="row-main">旁聊 | 状态 <span className={"st-" + k.status}>{k.status}</span> | {shortId(k.id)} | {String(k.prompt || "").slice(0, 60)}</span>
|
||||
</li>
|
||||
))}
|
||||
</DataListRaw>
|
||||
);
|
||||
}
|
||||
|
||||
export function AgentsPanel() {
|
||||
const ctx = useConsole();
|
||||
if (!ctx.currentTask) return <div className="muted">点任务行查看详情后,这里显示子代理链路</div>;
|
||||
|
||||
+31
-31
@@ -4,7 +4,7 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Codex 命令台</title>
|
||||
<script type="module" crossorigin>(function(){const r=document.createElement("link").relList;if(r&&r.supports&&r.supports("modulepreload"))return;for(const y of document.querySelectorAll('link[rel="modulepreload"]'))f(y);new MutationObserver(y=>{for(const C of y)if(C.type==="childList")for(const A of C.addedNodes)A.tagName==="LINK"&&A.rel==="modulepreload"&&f(A)}).observe(document,{childList:!0,subtree:!0});function h(y){const C={};return y.integrity&&(C.integrity=y.integrity),y.referrerPolicy&&(C.referrerPolicy=y.referrerPolicy),y.crossOrigin==="use-credentials"?C.credentials="include":y.crossOrigin==="anonymous"?C.credentials="omit":C.credentials="same-origin",C}function f(y){if(y.ep)return;y.ep=!0;const C=h(y);fetch(y.href,C)}})();function Jh(i){return i&&i.__esModule&&Object.prototype.hasOwnProperty.call(i,"default")?i.default:i}var io={exports:{}},si={};/**
|
||||
<script type="module" crossorigin>(function(){const r=document.createElement("link").relList;if(r&&r.supports&&r.supports("modulepreload"))return;for(const y of document.querySelectorAll('link[rel="modulepreload"]'))f(y);new MutationObserver(y=>{for(const A of y)if(A.type==="childList")for(const M of A.addedNodes)M.tagName==="LINK"&&M.rel==="modulepreload"&&f(M)}).observe(document,{childList:!0,subtree:!0});function h(y){const A={};return y.integrity&&(A.integrity=y.integrity),y.referrerPolicy&&(A.referrerPolicy=y.referrerPolicy),y.crossOrigin==="use-credentials"?A.credentials="include":y.crossOrigin==="anonymous"?A.credentials="omit":A.credentials="same-origin",A}function f(y){if(y.ep)return;y.ep=!0;const A=h(y);fetch(y.href,A)}})();function Jh(i){return i&&i.__esModule&&Object.prototype.hasOwnProperty.call(i,"default")?i.default:i}var co={exports:{}},ci={};/**
|
||||
* @license React
|
||||
* react-jsx-runtime.production.js
|
||||
*
|
||||
@@ -12,7 +12,7 @@
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/var Dh;function Hy(){if(Dh)return si;Dh=1;var i=Symbol.for("react.transitional.element"),r=Symbol.for("react.fragment");function h(f,y,C){var A=null;if(C!==void 0&&(A=""+C),y.key!==void 0&&(A=""+y.key),"key"in y){C={};for(var S in y)S!=="key"&&(C[S]=y[S])}else C=y;return y=C.ref,{$$typeof:i,type:f,key:A,ref:y!==void 0?y:null,props:C}}return si.Fragment=r,si.jsx=h,si.jsxs=h,si}var Uh;function By(){return Uh||(Uh=1,io.exports=Hy()),io.exports}var c=By(),co={exports:{}},gt={};/**
|
||||
*/var Dh;function Hy(){if(Dh)return ci;Dh=1;var i=Symbol.for("react.transitional.element"),r=Symbol.for("react.fragment");function h(f,y,A){var M=null;if(A!==void 0&&(M=""+A),y.key!==void 0&&(M=""+y.key),"key"in y){A={};for(var p in y)p!=="key"&&(A[p]=y[p])}else A=y;return y=A.ref,{$$typeof:i,type:f,key:M,ref:y!==void 0?y:null,props:A}}return ci.Fragment=r,ci.jsx=h,ci.jsxs=h,ci}var kh;function By(){return kh||(kh=1,co.exports=Hy()),co.exports}var c=By(),so={exports:{}},gt={};/**
|
||||
* @license React
|
||||
* react.production.js
|
||||
*
|
||||
@@ -20,7 +20,7 @@
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/var wh;function qy(){if(wh)return gt;wh=1;var i=Symbol.for("react.transitional.element"),r=Symbol.for("react.portal"),h=Symbol.for("react.fragment"),f=Symbol.for("react.strict_mode"),y=Symbol.for("react.profiler"),C=Symbol.for("react.consumer"),A=Symbol.for("react.context"),S=Symbol.for("react.forward_ref"),_=Symbol.for("react.suspense"),M=Symbol.for("react.memo"),Y=Symbol.for("react.lazy"),x=Symbol.for("react.activity"),B=Symbol.for("react.view_transition"),V=Symbol.iterator;function et(g){return g===null||typeof g!="object"?null:(g=V&&g[V]||g["@@iterator"],typeof g=="function"?g:null)}var ft={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},F=Object.assign,yt={};function _t(g,H,st){this.props=g,this.context=H,this.refs=yt,this.updater=st||ft}_t.prototype.isReactComponent={},_t.prototype.setState=function(g,H){if(typeof g!="object"&&typeof g!="function"&&g!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,g,H,"setState")},_t.prototype.forceUpdate=function(g){this.updater.enqueueForceUpdate(this,g,"forceUpdate")};function kt(){}kt.prototype=_t.prototype;function Q(g,H,st){this.props=g,this.context=H,this.refs=yt,this.updater=st||ft}var dt=Q.prototype=new kt;dt.constructor=Q,F(dt,_t.prototype),dt.isPureReactComponent=!0;var R=Array.isArray;function X(){}var lt={H:null,A:null,T:null,S:null},pt=Object.prototype.hasOwnProperty;function Et(g,H,st){var ct=st.ref;return{$$typeof:i,type:g,key:H,ref:ct!==void 0?ct:null,props:st}}function mt(g,H){return Et(g.type,H,g.props)}function Rt(g){return typeof g=="object"&&g!==null&&g.$$typeof===i}function k(g){var H={"=":"=0",":":"=2"};return"$"+g.replace(/[=:]/g,function(st){return H[st]})}var ut=/\/+/g;function U(g,H){return typeof g=="object"&&g!==null&&g.key!=null?k(""+g.key):H.toString(36)}function G(g){switch(g.status){case"fulfilled":return g.value;case"rejected":throw g.reason;default:switch(typeof g.status=="string"?g.then(X,X):(g.status="pending",g.then(function(H){g.status==="pending"&&(g.status="fulfilled",g.value=H)},function(H){g.status==="pending"&&(g.status="rejected",g.reason=H)})),g.status){case"fulfilled":return g.value;case"rejected":throw g.reason}}throw g}function L(g,H,st,ct,Mt){var Nt=typeof g;(Nt==="undefined"||Nt==="boolean")&&(g=null);var wt=!1;if(g===null)wt=!0;else switch(Nt){case"bigint":case"string":case"number":wt=!0;break;case"object":switch(g.$$typeof){case i:case r:wt=!0;break;case Y:return wt=g._init,L(wt(g._payload),H,st,ct,Mt)}}if(wt)return Mt=Mt(g),wt=ct===""?"."+U(g,0):ct,R(Mt)?(st="",wt!=null&&(st=wt.replace(ut,"$&/")+"/"),L(Mt,H,st,"",function(re){return re})):Mt!=null&&(Rt(Mt)&&(Mt=mt(Mt,st+(Mt.key==null||g&&g.key===Mt.key?"":(""+Mt.key).replace(ut,"$&/")+"/")+wt)),H.push(Mt)),1;wt=0;var tt=ct===""?".":ct+":";if(R(g))for(var ht=0;ht<g.length;ht++)ct=g[ht],Nt=tt+U(ct,ht),wt+=L(ct,H,st,Nt,Mt);else if(ht=et(g),typeof ht=="function")for(g=ht.call(g),ht=0;!(ct=g.next()).done;)ct=ct.value,Nt=tt+U(ct,ht++),wt+=L(ct,H,st,Nt,Mt);else if(Nt==="object"){if(typeof g.then=="function")return L(G(g),H,st,ct,Mt);throw H=String(g),Error("Objects are not valid as a React child (found: "+(H==="[object Object]"?"object with keys {"+Object.keys(g).join(", ")+"}":H)+"). If you meant to render a collection of children, use an array instead.")}return wt}function it(g,H,st){if(g==null)return g;var ct=[],Mt=0;return L(g,ct,"","",function(Nt){return H.call(st,Nt,Mt++)}),ct}function jt(g){if(g._status===-1){var H=g._result,st=H();st.then(function(ct){(g._status===0||g._status===-1)&&(g._status=1,g._result=ct,st.status===void 0&&(st.status="fulfilled",st.value=ct))},function(ct){(g._status===0||g._status===-1)&&(g._status=2,g._result=ct,st.status===void 0&&(st.status="rejected",st.reason=ct))}),g._status===-1&&(g._status=0,g._result=st)}if(g._status===1)return g._result.default;throw g._result}var St=typeof reportError=="function"?reportError:function(g){if(typeof window=="object"&&typeof window.ErrorEvent=="function"){var H=new window.ErrorEvent("error",{bubbles:!0,cancelable:!0,message:typeof g=="object"&&g!==null&&typeof g.message=="string"?String(g.message):String(g),error:g});if(!window.dispatchEvent(H))return}else if(typeof process=="object"&&typeof process.emit=="function"){process.emit("uncaughtException",g);return}console.error(g)};function ye(g){var H=lt.T,st={};st.types=H!==null?H.types:null,lt.T=st;try{var ct=g(),Mt=lt.S;Mt!==null&&Mt(st,ct),typeof ct=="object"&&ct!==null&&typeof ct.then=="function"&&ct.then(X,St)}catch(Nt){St(Nt)}finally{H!==null&&st.types!==null&&(H.types=st.types),lt.T=H}}function we(g){var H=lt.T;if(H!==null){var st=H.types;st===null?H.types=[g]:st.indexOf(g)===-1&&st.push(g)}else ye(we.bind(null,g))}var ce={map:it,forEach:function(g,H,st){it(g,function(){H.apply(this,arguments)},st)},count:function(g){var H=0;return it(g,function(){H++}),H},Line truncated
|
||||
*/var Uh;function qy(){if(Uh)return gt;Uh=1;var i=Symbol.for("react.transitional.element"),r=Symbol.for("react.portal"),h=Symbol.for("react.fragment"),f=Symbol.for("react.strict_mode"),y=Symbol.for("react.profiler"),A=Symbol.for("react.consumer"),M=Symbol.for("react.context"),p=Symbol.for("react.forward_ref"),_=Symbol.for("react.suspense"),R=Symbol.for("react.memo"),Q=Symbol.for("react.lazy"),b=Symbol.for("react.activity"),H=Symbol.for("react.view_transition"),Z=Symbol.iterator;function W(g){return g===null||typeof g!="object"?null:(g=Z&&g[Z]||g["@@iterator"],typeof g=="function"?g:null)}var ot={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},rt=Object.assign,bt={};function it(g,w,et){this.props=g,this.context=w,this.refs=bt,this.updater=et||ot}it.prototype.isReactComponent={},it.prototype.setState=function(g,w){if(typeof g!="object"&&typeof g!="function"&&g!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,g,w,"setState")},it.prototype.forceUpdate=function(g){this.updater.enqueueForceUpdate(this,g,"forceUpdate")};function wt(){}wt.prototype=it.prototype;function L(g,w,et){this.props=g,this.context=w,this.refs=bt,this.updater=et||ot}var ut=L.prototype=new wt;ut.constructor=L,rt(ut,it.prototype),ut.isPureReactComponent=!0;var O=Array.isArray;function V(){}var lt={H:null,A:null,T:null,S:null},St=Object.prototype.hasOwnProperty;function Nt(g,w,et){var nt=et.ref;return{$$typeof:i,type:g,key:w,ref:nt!==void 0?nt:null,props:et}}function mt(g,w){return Nt(g.type,w,g.props)}function Tt(g){return typeof g=="object"&&g!==null&&g.$$typeof===i}function q(g){var w={"=":"=0",":":"=2"};return"$"+g.replace(/[=:]/g,function(et){return w[et]})}var ht=/\/+/g;function _t(g,w){return typeof g=="object"&&g!==null&&g.key!=null?q(""+g.key):w.toString(36)}function G(g){switch(g.status){case"fulfilled":return g.value;case"rejected":throw g.reason;default:switch(typeof g.status=="string"?g.then(V,V):(g.status="pending",g.then(function(w){g.status==="pending"&&(g.status="fulfilled",g.value=w)},function(w){g.status==="pending"&&(g.status="rejected",g.reason=w)})),g.status){case"fulfilled":return g.value;case"rejected":throw g.reason}}throw g}function Y(g,w,et,nt,Mt){var Ct=typeof g;(Ct==="undefined"||Ct==="boolean")&&(g=null);var kt=!1;if(g===null)kt=!0;else switch(Ct){case"bigint":case"string":case"number":kt=!0;break;case"object":switch(g.$$typeof){case i:case r:kt=!0;break;case Q:return kt=g._init,Y(kt(g._payload),w,et,nt,Mt)}}if(kt)return Mt=Mt(g),kt=nt===""?"."+_t(g,0):nt,O(Mt)?(et="",kt!=null&&(et=kt.replace(ht,"$&/")+"/"),Y(Mt,w,et,"",function(Ne){return Ne})):Mt!=null&&(Tt(Mt)&&(Mt=mt(Mt,et+(Mt.key==null||g&&g.key===Mt.key?"":(""+Mt.key).replace(ht,"$&/")+"/")+kt)),w.push(Mt)),1;kt=0;var P=nt===""?".":nt+":";if(O(g))for(var ft=0;ft<g.length;ft++)nt=g[ft],Ct=P+_t(nt,ft),kt+=Y(nt,w,et,Ct,Mt);else if(ft=W(g),typeof ft=="function")for(g=ft.call(g),ft=0;!(nt=g.next()).done;)nt=nt.value,Ct=P+_t(nt,ft++),kt+=Y(nt,w,et,Ct,Mt);else if(Ct==="object"){if(typeof g.then=="function")return Y(G(g),w,et,nt,Mt);throw w=String(g),Error("Objects are not valid as a React child (found: "+(w==="[object Object]"?"object with keys {"+Object.keys(g).join(", ")+"}":w)+"). If you meant to render a collection of children, use an array instead.")}return kt}function z(g,w,et){if(g==null)return g;var nt=[],Mt=0;return Y(g,nt,"","",function(Ct){return w.call(et,Ct,Mt++)}),nt}function vt(g){if(g._status===-1){var w=g._result,et=w();et.then(function(nt){(g._status===0||g._status===-1)&&(g._status=1,g._result=nt,et.status===void 0&&(et.status="fulfilled",et.value=nt))},function(nt){(g._status===0||g._status===-1)&&(g._status=2,g._result=nt,et.status===void 0&&(et.status="rejected",et.reason=nt))}),g._status===-1&&(g._status=0,g._result=et)}if(g._status===1)return g._result.default;throw g._result}var pt=typeof reportError=="function"?reportError:function(g){if(typeof window=="object"&&typeof window.ErrorEvent=="function"){var w=new window.ErrorEvent("error",{bubbles:!0,cancelable:!0,message:typeof g=="object"&&g!==null&&typeof g.message=="string"?String(g.message):String(g),error:g});if(!window.dispatchEvent(w))return}else if(typeof process=="object"&&typeof process.emit=="function"){process.emit("uncaughtException",g);return}console.error(g)};function ee(g){var w=lt.T,et={};et.types=w!==null?w.types:null,lt.T=et;try{var nt=g(),Mt=lt.S;Mt!==null&&Mt(et,nt),typeof nt=="object"&&nt!==null&&typeof nt.then=="function"&&nt.then(V,pt)}catch(Ct){pt(Ct)}finally{w!==null&&et.types!==null&&(w.types=et.types),lt.T=w}}function fe(g){var w=lt.T;if(w!==null){var et=w.types;et===null?w.types=[g]:et.indexOf(g)===-1&&et.push(g)}else ee(fe.bind(null,g))}var ne={map:z,forEach:function(g,w,et){z(g,function(){w.apply(this,arguments)},et)},count:function(g){var w=0;return z(g,function(){w++}),w},toALine truncated
|
||||
* @license React
|
||||
* scheduler.production.js
|
||||
*
|
||||
@@ -28,7 +28,7 @@
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/var Hh;function Gy(){return Hh||(Hh=1,(function(i){function r(G,L){var it=G.length;G.push(L);t:for(;0<it;){var jt=it-1>>>1,St=G[jt];if(0<y(St,L))G[jt]=L,G[it]=St,it=jt;else break t}}function h(G){return G.length===0?null:G[0]}function f(G){if(G.length===0)return null;var L=G[0],it=G.pop();if(it!==L){G[0]=it;t:for(var jt=0,St=G.length,ye=St>>>1;jt<ye;){var we=2*(jt+1)-1,ce=G[we],g=we+1,H=G[g];if(0>y(ce,it))g<St&&0>y(H,ce)?(G[jt]=H,G[g]=it,jt=g):(G[jt]=ce,G[we]=it,jt=we);else if(g<St&&0>y(H,it))G[jt]=H,G[g]=it,jt=g;else break t}}return L}function y(G,L){var it=G.sortIndex-L.sortIndex;return it!==0?it:G.id-L.id}if(i.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var C=performance;i.unstable_now=function(){return C.now()}}else{var A=Date,S=A.now();i.unstable_now=function(){return A.now()-S}}var _=[],M=[],Y=1,x=null,B=3,V=!1,et=!1,ft=!1,F=!1,yt=typeof setTimeout=="function"?setTimeout:null,_t=typeof clearTimeout=="function"?clearTimeout:null,kt=typeof setImmediate<"u"?setImmediate:null;function Q(G){for(var L=h(M);L!==null;){if(L.callback===null)f(M);else if(L.startTime<=G)f(M),L.sortIndex=L.expirationTime,r(_,L);else break;L=h(M)}}function dt(G){if(ft=!1,Q(G),!et)if(h(_)!==null)et=!0,R||(R=!0,Rt());else{var L=h(M);L!==null&&U(dt,L.startTime-G)}}var R=!1,X=-1,lt=5,pt=-1;function Et(){return F?!0:!(i.unstable_now()-pt<lt)}function mt(){if(F=!1,R){var G=i.unstable_now();pt=G;var L=!0;try{t:{et=!1,ft&&(ft=!1,_t(X),X=-1),V=!0;var it=B;try{e:{for(Q(G),x=h(_);x!==null&&!(x.expirationTime>G&&Et());){var jt=x.callback;if(typeof jt=="function"){x.callback=null,B=x.priorityLevel;var St=jt(x.expirationTime<=G);if(G=i.unstable_now(),typeof St=="function"){x.callback=St,Q(G),L=!0;break e}x===h(_)&&f(_),Q(G)}else f(_);x=h(_)}if(x!==null)L=!0;else{var ye=h(M);ye!==null&&U(dt,ye.startTime-G),L=!1}}break t}finally{x=null,B=it,V=!1}L=void 0}}finally{L?Rt():R=!1}}}var Rt;if(typeof kt=="function")Rt=function(){kt(mt)};else if(typeof MessageChannel<"u"){var k=new MessageChannel,ut=k.port2;k.port1.onmessage=mt,Rt=function(){ut.postMessage(null)}}else Rt=function(){yt(mt,0)};function U(G,L){X=yt(function(){G(i.unstable_now())},L)}i.unstable_IdlePriority=5,i.unstable_ImmediatePriority=1,i.unstable_LowPriority=4,i.unstable_NormalPriority=3,i.unstable_Profiling=null,i.unstable_UserBlockingPriority=2,i.unstable_cancelCallback=function(G){G.callback=null},i.unstable_forceFrameRate=function(G){0>G||125<G?console.error("forceFrameRate takes a positive int between 0 and 125, forcing frame rates higher than 125 fps is not supported"):lt=0<G?Math.floor(1e3/G):5},i.unstable_getCurrentPriorityLevel=function(){return B},i.unstable_next=function(G){switch(B){case 1:case 2:case 3:var L=3;break;default:L=B}var it=B;B=L;try{return G()}finally{B=it}},i.unstable_requestPaint=function(){F=!0},i.unstable_runWithPriority=function(G,L){switch(G){case 1:case 2:case 3:case 4:case 5:break;default:G=3}var it=B;B=G;try{return L()}finally{B=it}},i.unstable_scheduleCallback=function(G,L,it){var jt=i.unstable_now();switch(typeof it=="object"&&it!==null?(it=it.delay,it=typeof it=="number"&&0<it?jt+it:jt):it=jt,G){case 1:var St=-1;break;case 2:St=250;break;case 5:St=1073741823;break;case 4:St=1e4;break;default:St=5e3}return St=it+St,G={id:Y++,callback:L,priorityLevel:G,startTime:it,expirationTime:St,sortIndex:-1},it>jt?(G.sortIndex=it,r(M,G),h(_)===null&&G===h(M)&&(ft?(_t(X),X=-1):ft=!0,U(dt,it-jt))):(G.sortIndex=St,r(_,G),et||V||(et=!0,R||(R=!0,Rt()))),G},i.unstable_shouldYield=Et,i.unstable_wrapCallback=function(G){var L=B;return function(){var it=B;B=L;try{return G.apply(this,arguments)}finally{B=it}}}})(oo)),oo}var Bh;function Qy(){return Bh||(Bh=1,ro.exports=Gy()),ro.exports}var fo={exports:{}},Ue={};/**
|
||||
*/var Hh;function Gy(){return Hh||(Hh=1,(function(i){function r(G,Y){var z=G.length;G.push(Y);t:for(;0<z;){var vt=z-1>>>1,pt=G[vt];if(0<y(pt,Y))G[vt]=Y,G[z]=pt,z=vt;else break t}}function h(G){return G.length===0?null:G[0]}function f(G){if(G.length===0)return null;var Y=G[0],z=G.pop();if(z!==Y){G[0]=z;t:for(var vt=0,pt=G.length,ee=pt>>>1;vt<ee;){var fe=2*(vt+1)-1,ne=G[fe],g=fe+1,w=G[g];if(0>y(ne,z))g<pt&&0>y(w,ne)?(G[vt]=w,G[g]=z,vt=g):(G[vt]=ne,G[fe]=z,vt=fe);else if(g<pt&&0>y(w,z))G[vt]=w,G[g]=z,vt=g;else break t}}return Y}function y(G,Y){var z=G.sortIndex-Y.sortIndex;return z!==0?z:G.id-Y.id}if(i.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var A=performance;i.unstable_now=function(){return A.now()}}else{var M=Date,p=M.now();i.unstable_now=function(){return M.now()-p}}var _=[],R=[],Q=1,b=null,H=3,Z=!1,W=!1,ot=!1,rt=!1,bt=typeof setTimeout=="function"?setTimeout:null,it=typeof clearTimeout=="function"?clearTimeout:null,wt=typeof setImmediate<"u"?setImmediate:null;function L(G){for(var Y=h(R);Y!==null;){if(Y.callback===null)f(R);else if(Y.startTime<=G)f(R),Y.sortIndex=Y.expirationTime,r(_,Y);else break;Y=h(R)}}function ut(G){if(ot=!1,L(G),!W)if(h(_)!==null)W=!0,O||(O=!0,Tt());else{var Y=h(R);Y!==null&&_t(ut,Y.startTime-G)}}var O=!1,V=-1,lt=5,St=-1;function Nt(){return rt?!0:!(i.unstable_now()-St<lt)}function mt(){if(rt=!1,O){var G=i.unstable_now();St=G;var Y=!0;try{t:{W=!1,ot&&(ot=!1,it(V),V=-1),Z=!0;var z=H;try{e:{for(L(G),b=h(_);b!==null&&!(b.expirationTime>G&&Nt());){var vt=b.callback;if(typeof vt=="function"){b.callback=null,H=b.priorityLevel;var pt=vt(b.expirationTime<=G);if(G=i.unstable_now(),typeof pt=="function"){b.callback=pt,L(G),Y=!0;break e}b===h(_)&&f(_),L(G)}else f(_);b=h(_)}if(b!==null)Y=!0;else{var ee=h(R);ee!==null&&_t(ut,ee.startTime-G),Y=!1}}break t}finally{b=null,H=z,Z=!1}Y=void 0}}finally{Y?Tt():O=!1}}}var Tt;if(typeof wt=="function")Tt=function(){wt(mt)};else if(typeof MessageChannel<"u"){var q=new MessageChannel,ht=q.port2;q.port1.onmessage=mt,Tt=function(){ht.postMessage(null)}}else Tt=function(){bt(mt,0)};function _t(G,Y){V=bt(function(){G(i.unstable_now())},Y)}i.unstable_IdlePriority=5,i.unstable_ImmediatePriority=1,i.unstable_LowPriority=4,i.unstable_NormalPriority=3,i.unstable_Profiling=null,i.unstable_UserBlockingPriority=2,i.unstable_cancelCallback=function(G){G.callback=null},i.unstable_forceFrameRate=function(G){0>G||125<G?console.error("forceFrameRate takes a positive int between 0 and 125, forcing frame rates higher than 125 fps is not supported"):lt=0<G?Math.floor(1e3/G):5},i.unstable_getCurrentPriorityLevel=function(){return H},i.unstable_next=function(G){switch(H){case 1:case 2:case 3:var Y=3;break;default:Y=H}var z=H;H=Y;try{return G()}finally{H=z}},i.unstable_requestPaint=function(){rt=!0},i.unstable_runWithPriority=function(G,Y){switch(G){case 1:case 2:case 3:case 4:case 5:break;default:G=3}var z=H;H=G;try{return Y()}finally{H=z}},i.unstable_scheduleCallback=function(G,Y,z){var vt=i.unstable_now();switch(typeof z=="object"&&z!==null?(z=z.delay,z=typeof z=="number"&&0<z?vt+z:vt):z=vt,G){case 1:var pt=-1;break;case 2:pt=250;break;case 5:pt=1073741823;break;case 4:pt=1e4;break;default:pt=5e3}return pt=z+pt,G={id:Q++,callback:Y,priorityLevel:G,startTime:z,expirationTime:pt,sortIndex:-1},z>vt?(G.sortIndex=z,r(R,G),h(_)===null&&G===h(R)&&(ot?(it(V),V=-1):ot=!0,_t(ut,z-vt))):(G.sortIndex=pt,r(_,G),W||Z||(W=!0,O||(O=!0,Tt()))),G},i.unstable_shouldYield=Nt,i.unstable_wrapCallback=function(G){var Y=H;return function(){var z=H;H=Y;try{return G.apply(this,arguments)}finally{H=z}}}})(fo)),fo}var Bh;function Qy(){return Bh||(Bh=1,oo.exports=Gy()),oo.exports}var mo={exports:{}},ke={};/**
|
||||
* @license React
|
||||
* react-dom.production.js
|
||||
*
|
||||
@@ -36,7 +36,7 @@
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/var qh;function Xy(){if(qh)return Ue;qh=1;var i=go();function r(Y){var x="https://react.dev/errors/"+Y;if(1<arguments.length){x+="?args[]="+encodeURIComponent(arguments[1]);for(var B=2;B<arguments.length;B++)x+="&args[]="+encodeURIComponent(arguments[B])}return"Minified React error #"+Y+"; visit "+x+" for the full message or use the non-minified dev environment for full errors and additional helpful warnings."}function h(){}var f={d:{f:h,r:function(){throw Error(r(522))},D:h,C:h,L:h,m:h,X:h,S:h,M:h},p:0,findDOMNode:null},y=Symbol.for("react.portal"),C=Symbol.for("react.recoverable"),A=Symbol.for("react.optimistic_key");function S(Y,x,B){var V=3<arguments.length&&arguments[3]!==void 0?arguments[3]:null;return{$$typeof:y,key:V==null?null:V===A?A:""+V,children:Y,containerInfo:x,implementation:B}}var _=i.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;function M(Y,x){if(Y==="font")return"";if(typeof x=="string")return x==="use-credentials"?x:""}return Ue.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE=f,Ue.browser=function(Y){return{$$typeof:C,_reason:Y}},Ue.createPortal=function(Y,x){var B=2<arguments.length&&arguments[2]!==void 0?arguments[2]:null;if(!x||x.nodeType!==1&&x.nodeType!==9&&x.nodeType!==11)throw Error(r(299));return S(Y,x,null,B)},Ue.flushSync=function(Y){var x=_.T,B=f.p;try{if(_.T=null,f.p=2,Y)return Y()}finally{_.T=x,f.p=B,f.d.f()}},Ue.preconnect=function(Y,x){typeof Y=="string"&&(x?(x=x.crossOrigin,x=typeof x=="string"?x==="use-credentials"?x:"":void 0):x=null,f.d.C(Y,x))},Ue.prefetchDNS=function(Y){typeof Y=="string"&&f.d.D(Y)},Ue.preinit=function(Y,x){if(typeof Y=="string"&&x&&typeof x.as=="string"){var B=x.as,V=M(B,x.crossOrigin),et=typeof x.integrity=="string"?x.integrity:void 0,ft=typeof x.fetchPriority=="string"?x.fetchPriority:void 0;B==="style"?f.d.S(Y,typeof x.precedence=="string"?x.precedence:void 0,{crossOrigin:V,integrity:et,fetchPriority:ft}):B==="script"&&f.d.X(Y,{crossOrigin:V,integrity:et,fetchPriority:ft,nonce:typeof x.nonce=="string"?x.nonce:void 0})}},Ue.preinitModule=function(Y,x){if(typeof Y=="string")if(typeof x=="object"&&x!==null){if(x.as==null||x.as==="script"){var B=M(x.as,x.crossOrigin);f.d.M(Y,{crossOrigin:B,integrity:typeof x.integrity=="string"?x.integrity:void 0,nonce:typeof x.nonce=="string"?x.nonce:void 0,fetchPriority:typeof x.fetchPriority=="string"?x.fetchPriority:void 0})}}else x==null&&f.d.M(Y)},Ue.preload=function(Y,x){if(typeof Y=="string"&&typeof x=="object"&&x!==null&&typeof x.as=="string"){var B=x.as,V=M(B,x.crossOrigin);f.d.L(Y,B,{crossOrigin:V,integrity:typeof x.integrity=="string"?x.integrity:void 0,nonce:typeof x.nonce=="string"?x.nonce:void 0,type:typeof x.type=="string"?x.type:void 0,fetchPriority:typeof x.fetchPriority=="string"?x.fetchPriority:void 0,referrerPolicy:typeof x.referrerPolicy=="string"?x.referrerPolicy:void 0,imageSrcSet:typeof x.imageSrcSet=="string"?x.imageSrcSet:void 0,imageSizes:typeof x.imageSizes=="string"?x.imageSizes:void 0,media:typeof x.media=="string"?x.media:void 0})}},Ue.preloadModule=function(Y,x){if(typeof Y=="string")if(x){var B=M(x.as,x.crossOrigin);f.d.m(Y,{as:typeof x.as=="string"&&x.as!=="script"?x.as:void 0,crossOrigin:B,integrity:typeof x.integrity=="string"?x.integrity:void 0,nonce:typeof x.nonce=="string"?x.nonce:void 0,fetchPriority:typeof x.fetchPriority=="string"?x.fetchPriority:void 0})}else f.d.m(Y)},Ue.requestFormReset=function(Y){f.d.r(Y)},Ue.unstable_batchedUpdates=function(Y,x){return Y(x)},Ue.useFormState=function(Y,x,B){return _.H.useFormState(Y,x,B)},Ue.useFormStatus=function(){return _.H.useHostTransitionStatus()},Ue.version="19.3.1",Ue}var Yh;function Ly(){if(Yh)return fo.exports;Yh=1;function i(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(i)}catch(r){console.error(r)}}return i(),fo.exports=Xy(),fo.exports}/**
|
||||
*/var qh;function Xy(){if(qh)return ke;qh=1;var i=po();function r(Q){var b="https://react.dev/errors/"+Q;if(1<arguments.length){b+="?args[]="+encodeURIComponent(arguments[1]);for(var H=2;H<arguments.length;H++)b+="&args[]="+encodeURIComponent(arguments[H])}return"Minified React error #"+Q+"; visit "+b+" for the full message or use the non-minified dev environment for full errors and additional helpful warnings."}function h(){}var f={d:{f:h,r:function(){throw Error(r(522))},D:h,C:h,L:h,m:h,X:h,S:h,M:h},p:0,findDOMNode:null},y=Symbol.for("react.portal"),A=Symbol.for("react.recoverable"),M=Symbol.for("react.optimistic_key");function p(Q,b,H){var Z=3<arguments.length&&arguments[3]!==void 0?arguments[3]:null;return{$$typeof:y,key:Z==null?null:Z===M?M:""+Z,children:Q,containerInfo:b,implementation:H}}var _=i.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;function R(Q,b){if(Q==="font")return"";if(typeof b=="string")return b==="use-credentials"?b:""}return ke.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE=f,ke.browser=function(Q){return{$$typeof:A,_reason:Q}},ke.createPortal=function(Q,b){var H=2<arguments.length&&arguments[2]!==void 0?arguments[2]:null;if(!b||b.nodeType!==1&&b.nodeType!==9&&b.nodeType!==11)throw Error(r(299));return p(Q,b,null,H)},ke.flushSync=function(Q){var b=_.T,H=f.p;try{if(_.T=null,f.p=2,Q)return Q()}finally{_.T=b,f.p=H,f.d.f()}},ke.preconnect=function(Q,b){typeof Q=="string"&&(b?(b=b.crossOrigin,b=typeof b=="string"?b==="use-credentials"?b:"":void 0):b=null,f.d.C(Q,b))},ke.prefetchDNS=function(Q){typeof Q=="string"&&f.d.D(Q)},ke.preinit=function(Q,b){if(typeof Q=="string"&&b&&typeof b.as=="string"){var H=b.as,Z=R(H,b.crossOrigin),W=typeof b.integrity=="string"?b.integrity:void 0,ot=typeof b.fetchPriority=="string"?b.fetchPriority:void 0;H==="style"?f.d.S(Q,typeof b.precedence=="string"?b.precedence:void 0,{crossOrigin:Z,integrity:W,fetchPriority:ot}):H==="script"&&f.d.X(Q,{crossOrigin:Z,integrity:W,fetchPriority:ot,nonce:typeof b.nonce=="string"?b.nonce:void 0})}},ke.preinitModule=function(Q,b){if(typeof Q=="string")if(typeof b=="object"&&b!==null){if(b.as==null||b.as==="script"){var H=R(b.as,b.crossOrigin);f.d.M(Q,{crossOrigin:H,integrity:typeof b.integrity=="string"?b.integrity:void 0,nonce:typeof b.nonce=="string"?b.nonce:void 0,fetchPriority:typeof b.fetchPriority=="string"?b.fetchPriority:void 0})}}else b==null&&f.d.M(Q)},ke.preload=function(Q,b){if(typeof Q=="string"&&typeof b=="object"&&b!==null&&typeof b.as=="string"){var H=b.as,Z=R(H,b.crossOrigin);f.d.L(Q,H,{crossOrigin:Z,integrity:typeof b.integrity=="string"?b.integrity:void 0,nonce:typeof b.nonce=="string"?b.nonce:void 0,type:typeof b.type=="string"?b.type:void 0,fetchPriority:typeof b.fetchPriority=="string"?b.fetchPriority:void 0,referrerPolicy:typeof b.referrerPolicy=="string"?b.referrerPolicy:void 0,imageSrcSet:typeof b.imageSrcSet=="string"?b.imageSrcSet:void 0,imageSizes:typeof b.imageSizes=="string"?b.imageSizes:void 0,media:typeof b.media=="string"?b.media:void 0})}},ke.preloadModule=function(Q,b){if(typeof Q=="string")if(b){var H=R(b.as,b.crossOrigin);f.d.m(Q,{as:typeof b.as=="string"&&b.as!=="script"?b.as:void 0,crossOrigin:H,integrity:typeof b.integrity=="string"?b.integrity:void 0,nonce:typeof b.nonce=="string"?b.nonce:void 0,fetchPriority:typeof b.fetchPriority=="string"?b.fetchPriority:void 0})}else f.d.m(Q)},ke.requestFormReset=function(Q){f.d.r(Q)},ke.unstable_batchedUpdates=function(Q,b){return Q(b)},ke.useFormState=function(Q,b,H){return _.H.useFormState(Q,b,H)},ke.useFormStatus=function(){return _.H.useHostTransitionStatus()},ke.version="19.3.1",ke}var Yh;function Ly(){if(Yh)return mo.exports;Yh=1;function i(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(i)}catch(r){console.error(r)}}return i(),mo.exports=Xy(),mo.exports}/**
|
||||
* @license React
|
||||
* react-dom-client.production.js
|
||||
*
|
||||
@@ -44,37 +44,37 @@
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/var Gh;function Vy(){if(Gh)return ri;Gh=1;var i=Qy(),r=go(),h=Ly();function f(t){var e="https://react.dev/errors/"+t;if(1<arguments.length){e+="?args[]="+encodeURIComponent(arguments[1]);for(var l=2;l<arguments.length;l++)e+="&args[]="+encodeURIComponent(arguments[l])}return"Minified React error #"+t+"; visit "+e+" for the full message or use the non-minified dev environment for full errors and additional helpful warnings."}function y(t){return!(!t||t.nodeType!==1&&t.nodeType!==9&&t.nodeType!==11)}function C(t){for(var e=t,l=e;l&&!l.alternate;)e=l,(e.flags&4098)!==0&&(t=e.return),l=e.return;for(;e.return;)e=e.return;return e.tag===3?t:null}function A(t){if(t.tag===13){var e=t.memoizedState;if(e===null&&(t=t.alternate,t!==null&&(e=t.memoizedState)),e!==null)return e.dehydrated}return null}function S(t){if(t.tag===31){var e=t.memoizedState;if(e===null&&(t=t.alternate,t!==null&&(e=t.memoizedState)),e!==null)return e.dehydrated}return null}function _(t){if(C(t)!==t)throw Error(f(188))}function M(t){var e=t.alternate;if(!e){if(e=C(t),e===null)throw Error(f(188));return e!==t?null:t}for(var l=t,a=e;;){var n=l.return;if(n===null)break;var u=n.alternate;if(u===null){if(a=n.return,a!==null){l=a;continue}break}if(n.child===u.child){for(u=n.child;u;){if(u===l)return _(n),t;if(u===a)return _(n),e;u=u.sibling}throw Error(f(188))}if(l.return!==a.return)l=n,a=u;else{for(var s=!1,o=n.child;o;){if(o===l){s=!0,l=n,a=u;break}if(o===a){s=!0,a=n,l=u;break}o=o.sibling}if(!s){for(o=u.child;o;){if(o===l){s=!0,l=u,a=n;break}if(o===a){s=!0,a=u,l=n;break}o=o.sibling}if(!s)throw Error(f(189))}}if(l.alternate!==a)throw Error(f(190))}if(l.tag!==3)throw Error(f(188));return l.stateNode.current===l?t:e}function Y(t){var e=t.tag;if(e===5||e===26||e===27||e===6)return t;for(t=t.child;t!==null;){if(e=Y(t),e!==null)return e;t=t.sibling}return null}function x(t,e,l,a,n,u){for(;t!==null;){if((t.tag===5||t.tag===27||t.tag===6)&&l(t,a,n,u)||(t.tag!==22||t.memoizedState===null)&&(e||t.tag!==5&&t.tag!==27)&&x(t.child,e,l,a,n,u))return!0;t=t.sibling}return!1}function B(t){for(t=t.return;t!==null;){if(t.tag===3||t.tag===5||t.tag===27)return t;t=t.return}return null}function V(t){var e=!1;for(t=t.return;t!==null&&(t.tag===4&&(e=!0),!(t.tag===3||t.tag===5||t.tag===27));)t=t.return;return e}function et(t){var e=[null,null],l=B(t);return l===null||ft(e,t,l.child,{foundSelf:!1}),e}function ft(t,e,l,a){for(;l!==null;){if(l===e)a.foundSelf=!0;else if(l.tag===5||l.tag===27||l.tag===6){if(a.foundSelf)return t[1]=l,!0;t[0]=l}else if((l.tag!==22||l.memoizedState===null)&&ft(t,e,l.child,a))return!0;l=l.sibling}return!1}function F(t){switch(t.tag){case 5:case 27:case 6:return t.stateNode;case 3:return t.stateNode.containerInfo;default:throw Error(f(559))}}var yt=null,_t=null;function kt(t,e,l){return t===l?!0:t===e?(yt=t,!0):!1}function Q(t,e,l){return t===l?(_t=t,!1):t===e?(_t!==null&&(yt=t),!0):!1}function dt(t){if(t===null)return null;do t=t===null?null:t.return;while(t&&t.tag!==5&&t.tag!==27&&t.tag!==3);return t||null}function R(t,e,l){for(var a=0,n=t;n;n=l(n))a++;n=0;for(var u=e;u;u=l(u))n++;for(;0<a-n;)t=l(t),a--;for(;0<n-a;)e=l(e),n--;for(;a--;){if(t===e||e!==null&&t===e.alternate)return t;t=l(t),e=l(e)}return null}var X=Object.assign,lt=Symbol.for("react.element"),pt=Symbol.for("react.transitional.element"),Et=Symbol.for("react.portal"),mt=Symbol.for("react.fragment"),Rt=Symbol.for("react.strict_mode"),k=Symbol.for("react.profiler"),ut=Symbol.for("react.consumer"),U=Symbol.for("react.context"),G=Symbol.for("react.forward_ref"),L=Symbol.for("react.suspense"),it=Symbol.for("react.suspense_list"),jt=Symbol.for("react.memo"),St=Symbol.for("react.lazy"),ye=Symbol.for("react.activity"),we=Symbol.for("react.legacy_hidden"),ce=Symbol.for("react.memo_cache_sentinel"),g=Symbol.for("react.view_transition"),H=Symbol.for("react.recoverable"),st=Symbol.iterator;function ct(t){return t===null||typeof t!="object"?null:(t=st&&t[st]||t["@@iterator"],typeof t=="function"?t:null)}var Mt=Symbol.for("react.client.reference");function Nt(t){if(t==null)return null;if(typeof t=="function")return t.$$typeof===Mt?null:t.displayName||t.name||null;if(typeof t=="string")return t;switch(t){case mt:return"Fragment";case k:return"Profiler";case Rt:return"StrictMode";case L:return"Suspense";case it:return"SuspenseList";case ye:return"Activity";case g:return"ViewTransition"}if(typeof t=="object")switch(t.$$typeof){case Et:return"Portal";case U:return t.displayName||"Context";case ut:return(t._context.displayName||"Context")+".Consumer";case G:var e=t.render;return t=t.displayName,t||(t=e.displayName||e.name||"",t=t!==""?"ForwardRef("+t+")":"ForwardRef"),t;case jt:return e=t.displayName||null,e!==null?e:Nt(t.type)||"Memo";case St:e=t._payload,t=t._init;try{return Nt(t(e))}catch{}}return null}var wt=Array.isArray,tt=r.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,ht=h.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,re={pending:!1,data:null,method:null,actioLine truncated
|
||||
*/var Gh;function Vy(){if(Gh)return si;Gh=1;var i=Qy(),r=po(),h=Ly();function f(t){var e="https://react.dev/errors/"+t;if(1<arguments.length){e+="?args[]="+encodeURIComponent(arguments[1]);for(var l=2;l<arguments.length;l++)e+="&args[]="+encodeURIComponent(arguments[l])}return"Minified React error #"+t+"; visit "+e+" for the full message or use the non-minified dev environment for full errors and additional helpful warnings."}function y(t){return!(!t||t.nodeType!==1&&t.nodeType!==9&&t.nodeType!==11)}function A(t){for(var e=t,l=e;l&&!l.alternate;)e=l,(e.flags&4098)!==0&&(t=e.return),l=e.return;for(;e.return;)e=e.return;return e.tag===3?t:null}function M(t){if(t.tag===13){var e=t.memoizedState;if(e===null&&(t=t.alternate,t!==null&&(e=t.memoizedState)),e!==null)return e.dehydrated}return null}function p(t){if(t.tag===31){var e=t.memoizedState;if(e===null&&(t=t.alternate,t!==null&&(e=t.memoizedState)),e!==null)return e.dehydrated}return null}function _(t){if(A(t)!==t)throw Error(f(188))}function R(t){var e=t.alternate;if(!e){if(e=A(t),e===null)throw Error(f(188));return e!==t?null:t}for(var l=t,a=e;;){var n=l.return;if(n===null)break;var u=n.alternate;if(u===null){if(a=n.return,a!==null){l=a;continue}break}if(n.child===u.child){for(u=n.child;u;){if(u===l)return _(n),t;if(u===a)return _(n),e;u=u.sibling}throw Error(f(188))}if(l.return!==a.return)l=n,a=u;else{for(var s=!1,o=n.child;o;){if(o===l){s=!0,l=n,a=u;break}if(o===a){s=!0,a=n,l=u;break}o=o.sibling}if(!s){for(o=u.child;o;){if(o===l){s=!0,l=u,a=n;break}if(o===a){s=!0,a=u,l=n;break}o=o.sibling}if(!s)throw Error(f(189))}}if(l.alternate!==a)throw Error(f(190))}if(l.tag!==3)throw Error(f(188));return l.stateNode.current===l?t:e}function Q(t){var e=t.tag;if(e===5||e===26||e===27||e===6)return t;for(t=t.child;t!==null;){if(e=Q(t),e!==null)return e;t=t.sibling}return null}function b(t,e,l,a,n,u){for(;t!==null;){if((t.tag===5||t.tag===27||t.tag===6)&&l(t,a,n,u)||(t.tag!==22||t.memoizedState===null)&&(e||t.tag!==5&&t.tag!==27)&&b(t.child,e,l,a,n,u))return!0;t=t.sibling}return!1}function H(t){for(t=t.return;t!==null;){if(t.tag===3||t.tag===5||t.tag===27)return t;t=t.return}return null}function Z(t){var e=!1;for(t=t.return;t!==null&&(t.tag===4&&(e=!0),!(t.tag===3||t.tag===5||t.tag===27));)t=t.return;return e}function W(t){var e=[null,null],l=H(t);return l===null||ot(e,t,l.child,{foundSelf:!1}),e}function ot(t,e,l,a){for(;l!==null;){if(l===e)a.foundSelf=!0;else if(l.tag===5||l.tag===27||l.tag===6){if(a.foundSelf)return t[1]=l,!0;t[0]=l}else if((l.tag!==22||l.memoizedState===null)&&ot(t,e,l.child,a))return!0;l=l.sibling}return!1}function rt(t){switch(t.tag){case 5:case 27:case 6:return t.stateNode;case 3:return t.stateNode.containerInfo;default:throw Error(f(559))}}var bt=null,it=null;function wt(t,e,l){return t===l?!0:t===e?(bt=t,!0):!1}function L(t,e,l){return t===l?(it=t,!1):t===e?(it!==null&&(bt=t),!0):!1}function ut(t){if(t===null)return null;do t=t===null?null:t.return;while(t&&t.tag!==5&&t.tag!==27&&t.tag!==3);return t||null}function O(t,e,l){for(var a=0,n=t;n;n=l(n))a++;n=0;for(var u=e;u;u=l(u))n++;for(;0<a-n;)t=l(t),a--;for(;0<n-a;)e=l(e),n--;for(;a--;){if(t===e||e!==null&&t===e.alternate)return t;t=l(t),e=l(e)}return null}var V=Object.assign,lt=Symbol.for("react.element"),St=Symbol.for("react.transitional.element"),Nt=Symbol.for("react.portal"),mt=Symbol.for("react.fragment"),Tt=Symbol.for("react.strict_mode"),q=Symbol.for("react.profiler"),ht=Symbol.for("react.consumer"),_t=Symbol.for("react.context"),G=Symbol.for("react.forward_ref"),Y=Symbol.for("react.suspense"),z=Symbol.for("react.suspense_list"),vt=Symbol.for("react.memo"),pt=Symbol.for("react.lazy"),ee=Symbol.for("react.activity"),fe=Symbol.for("react.legacy_hidden"),ne=Symbol.for("react.memo_cache_sentinel"),g=Symbol.for("react.view_transition"),w=Symbol.for("react.recoverable"),et=Symbol.iterator;function nt(t){return t===null||typeof t!="object"?null:(t=et&&t[et]||t["@@iterator"],typeof t=="function"?t:null)}var Mt=Symbol.for("react.client.reference");function Ct(t){if(t==null)return null;if(typeof t=="function")return t.$$typeof===Mt?null:t.displayName||t.name||null;if(typeof t=="string")return t;switch(t){case mt:return"Fragment";case q:return"Profiler";case Tt:return"StrictMode";case Y:return"Suspense";case z:return"SuspenseList";case ee:return"Activity";case g:return"ViewTransition"}if(typeof t=="object")switch(t.$$typeof){case Nt:return"Portal";case _t:return t.displayName||"Context";case ht:return(t._context.displayName||"Context")+".Consumer";case G:var e=t.render;return t=t.displayName,t||(t=e.displayName||e.name||"",t=t!==""?"ForwardRef("+t+")":"ForwardRef"),t;case vt:return e=t.displayName||null,e!==null?e:Ct(t.type)||"Memo";case pt:e=t._payload,t=t._init;try{return Ct(t(e))}catch{}}return null}var kt=Array.isArray,P=r.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,ft=h.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,Ne={pending:!1,data:null,method:null,actionLine truncated
|
||||
at`)?" (<anonymous>)":-1<l.stack.indexOf("@")?"@unknown:0:0":""}return`
|
||||
`+He+t+oe}var fl=!1;function $l(t,e){if(!t||fl)return"";fl=!0;var l=Error.prepareStackTrace;Error.prepareStackTrace=void 0;try{var a={DetermineComponentFrameRoot:function(){try{if(e){var z=function(){throw Error()};if(Object.defineProperty(z.prototype,"props",{set:function(){throw Error()}}),typeof Reflect=="object"&&Reflect.construct){try{Reflect.construct(z,[])}catch(Z){var p=Z}Reflect.construct(t,[],z)}else{try{z.call()}catch(Z){p=Z}z=!1;try{var E=Object.getOwnPropertyDescriptor(t.prototype,"props");Object.defineProperty(t.prototype,"props",{configurable:!0,set:function(){throw Error()}}),z=!0,new t}finally{z&&(E!==void 0?Object.defineProperty(t.prototype,"props",E):delete t.prototype.props)}}}else{try{throw Error()}catch(Z){p=Z}(z=t())&&typeof z.catch=="function"&&z.catch(function(){})}}catch(Z){if(Z&&p&&typeof Z.stack=="string")return[Z.stack,p.stack]}return[null,null]}};a.DetermineComponentFrameRoot.displayName="DetermineComponentFrameRoot";var n=Object.getOwnPropertyDescriptor(a.DetermineComponentFrameRoot,"name");n&&n.configurable&&Object.defineProperty(a.DetermineComponentFrameRoot,"name",{value:"DetermineComponentFrameRoot"});var u=a.DetermineComponentFrameRoot(),s=u[0],o=u[1];if(s&&o){var m=s.split(`
|
||||
`),b=o.split(`
|
||||
`);for(n=a=0;a<m.length&&!m[a].includes("DetermineComponentFrameRoot");)a++;for(;n<b.length&&!b[n].includes("DetermineComponentFrameRoot");)n++;if(a===m.length||n===b.length)for(a=m.length-1,n=b.length-1;1<=a&&0<=n&&m[a]!==b[n];)n--;for(;1<=a&&0<=n;a--,n--)if(m[a]!==b[n]){if(a!==1||n!==1)do if(a--,n--,0>n||m[a]!==b[n]){var N=`
|
||||
`+m[a].replace(" at new "," at ");return t.displayName&&N.includes("<anonymous>")&&(N=N.replace("<anonymous>",t.displayName)),N}while(1<=a&&0<=n);break}}}finally{fl=!1,Error.prepareStackTrace=l}return(l=t?t.displayName||t.name:"")?fe(l):""}function ya(t,e){switch(t.tag){case 26:case 27:case 5:return fe(t.type);case 16:return fe("Lazy");case 13:return t.child!==e&&e!==null?fe("Suspense Fallback"):fe("Suspense");case 19:return fe("SuspenseList");case 0:case 15:return $l(t.type,!1);case 11:return $l(t.type.render,!1);case 1:return $l(t.type,!0);case 31:return fe("Activity");case 30:return fe("ViewTransition");default:return""}}function kl(t){try{var e="",l=null;do e+=ya(t,l),l=t,t=t.return;while(t);return e}catch(a){return`
|
||||
`+xl+t+Dl}var me=!1;function il(t,e){if(!t||me)return"";me=!0;var l=Error.prepareStackTrace;Error.prepareStackTrace=void 0;try{var a={DetermineComponentFrameRoot:function(){try{if(e){var k=function(){throw Error()};if(Object.defineProperty(k.prototype,"props",{set:function(){throw Error()}}),typeof Reflect=="object"&&Reflect.construct){try{Reflect.construct(k,[])}catch(K){var S=K}Reflect.construct(t,[],k)}else{try{k.call()}catch(K){S=K}k=!1;try{var E=Object.getOwnPropertyDescriptor(t.prototype,"props");Object.defineProperty(t.prototype,"props",{configurable:!0,set:function(){throw Error()}}),k=!0,new t}finally{k&&(E!==void 0?Object.defineProperty(t.prototype,"props",E):delete t.prototype.props)}}}else{try{throw Error()}catch(K){S=K}(k=t())&&typeof k.catch=="function"&&k.catch(function(){})}}catch(K){if(K&&S&&typeof K.stack=="string")return[K.stack,S.stack]}return[null,null]}};a.DetermineComponentFrameRoot.displayName="DetermineComponentFrameRoot";var n=Object.getOwnPropertyDescriptor(a.DetermineComponentFrameRoot,"name");n&&n.configurable&&Object.defineProperty(a.DetermineComponentFrameRoot,"name",{value:"DetermineComponentFrameRoot"});var u=a.DetermineComponentFrameRoot(),s=u[0],o=u[1];if(s&&o){var m=s.split(`
|
||||
`),j=o.split(`
|
||||
`);for(n=a=0;a<m.length&&!m[a].includes("DetermineComponentFrameRoot");)a++;for(;n<j.length&&!j[n].includes("DetermineComponentFrameRoot");)n++;if(a===m.length||n===j.length)for(a=m.length-1,n=j.length-1;1<=a&&0<=n&&m[a]!==j[n];)n--;for(;1<=a&&0<=n;a--,n--)if(m[a]!==j[n]){if(a!==1||n!==1)do if(a--,n--,0>n||m[a]!==j[n]){var C=`
|
||||
`+m[a].replace(" at new "," at ");return t.displayName&&C.includes("<anonymous>")&&(C=C.replace("<anonymous>",t.displayName)),C}while(1<=a&&0<=n);break}}}finally{me=!1,Error.prepareStackTrace=l}return(l=t?t.displayName||t.name:"")?de(l):""}function kl(t,e){switch(t.tag){case 26:case 27:case 5:return de(t.type);case 16:return de("Lazy");case 13:return t.child!==e&&e!==null?de("Suspense Fallback"):de("Suspense");case 19:return de("SuspenseList");case 0:case 15:return il(t.type,!1);case 11:return il(t.type.render,!1);case 1:return il(t.type,!0);case 31:return de("Activity");case 30:return de("ViewTransition");default:return""}}function Fl(t){try{var e="",l=null;do e+=kl(t,l),l=t,t=t.return;while(t);return e}catch(a){return`
|
||||
Error generating stack: `+a.message+`
|
||||
`+a.stack}}var te=Object.prototype.hasOwnProperty,tl=i.unstable_scheduleCallback,Be=i.unstable_cancelCallback,Nl=i.unstable_shouldYield,dl=i.unstable_requestPaint,$t=i.unstable_now,Hl=i.unstable_getCurrentPriorityLevel,vn=i.unstable_ImmediatePriority,Va=i.unstable_UserBlockingPriority,be=i.unstable_NormalPriority,Il=i.unstable_LowPriority,ga=i.unstable_IdlePriority,fu=i.log,du=i.unstable_setDisableYieldValue,Pl=null,Ce=null;function ml(t){if(typeof fu=="function"&&du(t),Ce&&typeof Ce.setStrictMode=="function")try{Ce.setStrictMode(Pl,t)}catch{}}var Ae=Math.clz32?Math.clz32:vu,mu=Math.log,hu=Math.LN2;function vu(t){return t>>>=0,t===0?32:31-(mu(t)/hu|0)|0}var q=256,w=262144,P=4194304;function nt(t){var e=t&42;if(e!==0)return e;switch(t&-t){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return t&-t;case 262144:case 524288:case 1048576:case 2097152:return t&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return t&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return t}}function qt(t,e,l){var a=t.pendingLanes;if(a===0)return 0;var n=0,u=t.suspendedLanes,s=t.pingedLanes;t=t.warmLanes;var o=a&134217727;return o!==0?(a=o&~u,a!==0?n=nt(a):(s&=o,s!==0?n=nt(s):l||(l=o&~t,l!==0&&(n=nt(l))))):(o=a&~u,o!==0?n=nt(o):s!==0?n=nt(s):l||(l=a&~t,l!==0&&(n=nt(l)))),n===0?0:e!==0&&e!==n&&(e&u)===0&&(u=n&-n,l=e&-e,u>=l||u===32&&(l&4194048)!==0)?e:n}function hl(t,e){return(t.pendingLanes&~(t.suspendedLanes&~t.pingedLanes)&e)===0}function D(t,e){(e&8)!==0&&(e|=e&32);var l=t.entangledLanes;if(l!==0)for(t=t.entanglements,l&=e;0<l;){var a=31-Ae(l),n=1<<a;e|=t[a],l&=~n}return e}function K(t,e){switch(t){case 1:case 2:case 4:case 8:case 64:return e+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function J(){var t=P;return P<<=1,(P&62914560)===0&&(P=4194304),t}function Yt(t){for(var e=[],l=0;31>l;l++)e.push(t);return e}function xt(t,e){t.pendingLanes|=e,e!==268435456&&(t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0)}function qe(t,e,l,a,n,u){var s=t.pendingLanes;t.pendingLanes=l,t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0,t.expiredLanes&=l,t.entangledLanes&=l,t.errorRecoveryDisabledLanes&=l,t.shellSuspendCounter=0;var o=t.entanglements,m=t.expirationTimes,b=t.hiddenUpdates;for(l=s&~l;0<l;){var N=31-Ae(l),z=1<<N;o[N]=0,m[N]=-1;var p=b[N];if(p!==null)for(b[N]=null,N=0;N<p.length;N++){var E=p[N];E!==null&&(E.lane&=-536870913)}l&=~z}a!==0&&Cl(t,a,0),u!==0&&n===0&&t.tag!==0&&(t.suspendedLanes|=u&~(s&~e))}function Cl(t,e,l){t.pendingLanes|=e,t.suspendedLanes&=~e;var a=31-Ae(e);t.entangledLanes|=e,t.entanglements[a]=t.entanglements[a]|1073741824|l&261930}function yn(t,e){var l=t.entangledLanes|=e;for(t=t.entanglements;l;){var a=31-Ae(l),n=1<<a;n&e|t[a]&e&&(t[a]|=e),l&=~n}}function ta(t,e){var l=e&-e;return l=(l&42)!==0?1:Bl(l),(l&(t.suspendedLanes|e))!==0?0:l}function Bl(t){switch(t){case 2:t=1;break;case 8:t=4;break;case 32:t=16;break;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:case 4194304:case 8388608:case 16777216:case 33554432:t=128;break;case 268435456:t=134217728;break;default:t=0}return t}function yu(t){return t&=-t,2<t?8<t?(t&134217727)!==0?32:268435456:8:2}function gu(){var t=ht.p;return t!==0?t:(t=window.event,t===void 0?32:Nh(t.type))}function Ke(t,e){var l=ht.p;try{return ht.p=t,e()}finally{ht.p=l}}var ne=Math.random().toString(36).slice(2),pe="__reactFiber$"+ne,Je="__reactProps$"+ne,gn="__reactContainer$"+ne,xo="__reactEvents$"+ne,Ph="__reactListeners$"+ne,tv="__reactHandles$"+ne,jo="__reactResources$"+ne,pu="__reactMarker$"+ne,fi="__reactLoad$"+ne;function di(t){delete t[pe],delete t[Je],delete t[Ph],delete t[tv]}function Za(t){var e;if(e=t[pe])return e;for(var l=t.parentNode;l;){if(e=l[gn]||l[pe]){if(l=e.alternate,e.child!==null||l!==null&&l.child!==null)for(t=ih(t);t!==null;){if(l=t[pe])return l;t=ih(t)}return e}t=l,l=t.parentNode}return null}function pn(t){if(t=t[pe]||t[gn]){var e=t.tag;if(e===5||e===6||e===13||e===31||e===26||e===27||e===3)return t}return null}function Su(t){var e=t.tag;if(e===5||e===26||e===27||e===6)return t.stateNode;throw Error(f(33))}function Sn(t){var e=t[jo];return e||(e=t[jo]={hoistableStyles:new Map,hoistableScripts:new Map}),e}function Te(t){t[pu]=!0}function bo(t){t[fi]=void 0}var To=new Set,_o={};function Ka(t,e){xn(t,e),xn(t+"Capture",e)}function xn(t,eLine truncated
|
||||
`).replace(G0,"")}function Ym(t,e){return e=qm(e),qm(t)===e}function Kt(t,e,l,a,n,u){switch(l){case"children":if(typeof a=="string")e==="body"||e==="textarea"&&a===""||bn(t,a);else if(typeof a=="number"||typeof a=="bigint")e!=="body"&&bn(t,""+a);else return;break;case"className":hi(t,"class",a);break;case"tabIndex":hi(t,"tabindex",a);break;case"dir":case"role":case"viewBox":case"width":case"height":hi(t,l,a);break;case"style":Uo(t,a,u);return;case"data":if(e!=="object"){hi(t,"data",a);break}case"src":case"href":if(a===""&&(e!=="a"||l!=="href")){t.removeAttribute(l);break}if(a==null||typeof a=="function"||typeof a=="symbol"||typeof a=="boolean"){t.removeAttribute(l);break}a=vi(a),t.setAttribute(l,a);break;case"action":case"formAction":if(typeof a=="function"){t.setAttribute(l,"javascript:throw new Error('A React form was unexpectedly submitted. If you called form.submit() manually, consider using form.requestSubmit() instead. If you\\'re trying to use event.stopPropagation() in a submit event handler, consider also calling event.preventDefault().')");break}else typeof u=="function"&&(l==="formAction"?(e!=="input"&&Kt(t,e,"name",n.name,n,null),Kt(t,e,"formEncType",n.formEncType,n,null),Kt(t,e,"formMethod",n.formMethod,n,null),Kt(t,e,"formTarget",n.formTarget,n,null)):(Kt(t,e,"encType",n.encType,n,null),Kt(t,e,"method",n.method,n,null),Kt(t,e,"target",n.target,n,null)));if(a==null||typeof a=="symbol"||typeof a=="boolean"){t.removeAttribute(l);break}a=vi(a),t.setAttribute(l,a);break;case"onClick":a!=null&&(t.onclick=ql);return;case"onScroll":a!=null&&zt("scroll",t);return;case"onScrollEnd":a!=null&&zt("scrollend",t);return;case"dangerouslySetInnerHTML":if(a!=null){if(typeof a!="object"||!("__html"in a))throw Error(f(61));if(l=a.__html,l!=null){if(n.children!=null)throw Error(f(60));(u!=null?u.__html:void 0)!==l&&(t.innerHTML=l)}}break;case"multiple":t.multiple=a&&typeof a!="function"&&typeof a!="symbol";break;case"muted":t.muted=a&&typeof a!="function"&&typeof a!="symbol";break;case"suppressContentEditableWarning":case"suppressHydrationWarning":case"defaultValue":case"defaultChecked":case"innerHTML":case"ref":break;case"autoFocus":break;case"xlinkHref":if(a==null||typeof a=="function"||typeof a=="boolean"||typeof a=="symbol"){t.removeAttribute("xlink:href");break}l=vi(a),t.setAttributeNS("http://www.w3.org/1999/xlink","xlink:href",l);break;case"contentEditable":case"spellCheck":case"draggable":case"value":case"autoReverse":case"externalResourcesRequired":case"focusable":case"preserveAlpha":a!=null&&typeof a!="function"&&typeof a!="symbol"?t.setAttribute(l,a):t.removeAttribute(l);break;case"inert":case"allowFullScreen":case"async":case"autoPlay":case"controls":case"credentialless":case"default":case"defer":case"disabled":case"disablePictureInPicture":case"disableRemotePlayback":case"formNoValidate":case"hidden":case"loop":case"noModule":case"noValidate":case"open":case"playsInline":case"readOnly":case"required":case"reversed":case"scoped":case"seamless":case"itemScope":a&&typeof a!="function"&&typeof a!="symbol"?t.setAttribute(l,""):t.removeAttribute(l);break;case"capture":case"download":a===!0?t.setAttribute(l,""):a!==!1&&a!=null&&typeof a!="function"&&typeof a!="symbol"?t.setAttribute(l,a):t.removeAttribute(l);break;case"cols":case"rows":case"size":case"span":a!=null&&typeof a!="function"&&typeof a!="symbol"&&!isNaN(a)&&1<=a?t.setAttribute(l,a):t.removeAttribute(l);break;case"rowSpan":case"start":a==null||typeof a=="function"||typeof a=="symbol"||isNaN(a)?t.removeAttribute(l):t.setAttribute(l,a);break;case"popover":zt("beforetoggle",t),zt("toggle",t),mi(t,"popover",a);break;case"xlinkActuate":ea(t,"http://www.w3.org/1999/xlink","xlink:actuate",a);break;case"xlinkArcrole":ea(t,"http://www.w3.org/1999/xlink","xlink:arcrole",a);break;case"xlinkRole":ea(t,"http://www.w3.org/1999/xlink","xlink:role",a);break;case"xlinkShow":ea(t,"http://www.w3.org/1999/xlink","xlink:show",a);break;case"xlinkTitle":ea(t,"http://www.w3.org/1999/xlink","xlink:title",a);break;case"xlinkType":ea(t,"http://www.w3.org/1999/xlink","xlink:type",a);break;case"xmlBase":ea(t,"http://www.w3.org/XML/1998/namespace","xml:base",a);break;case"xmlLang":ea(t,"http://www.w3.org/XML/1998/namespace","xml:lang",a);break;case"xmlSpace":ea(t,"http://www.w3.org/XML/1998/namespace","xml:space",a);break;case"is":mi(t,"is",a);break;case"innerText":case"textContent":return;default:if(!(2<l.length)||l[0]!=="o"&&l[0]!=="O"||l[1]!=="n"&&l[1]!=="N")l=iv.get(l)||l,mi(t,l,a);else return}Bt=!0}function Hr(t,e,l,a,n,u){switch(l){case"style":Uo(t,a,u);return;case"dangerouslySetInnerHTML":if(a!=null){if(typeof a!="object"||!("__html"in a))throw Error(f(61));if(l=a.__html,l!=null){if(n.children!=null)throw Error(f(60));(u!=null?u.__html:void 0)!==l&&(t.innerHTML=l)}}break;case"children":if(typeof a=="string")bn(t,a);else if(typeof a=="number"||typeof a=="bigint")bn(t,""+a);else return;break;case"onScroll":a!=null&&zt("scroll",t);return;case"onScrollEnd":a!=null&&zt("scroLine truncated
|
||||
`;Yt&&(qe+=Yt+`
|
||||
`),xt&&(qe+=(Array.isArray(xt)?xt.join(`
|
||||
`):String(xt))+`
|
||||
`),qe+=JSON.stringify(J,null,2).slice(0,4e3),_l(qe),re("已返回登录信息,见下方")}catch(J){re("发起失败:"+J.message)}},[]),P=d.useCallback(async()=>{const D=et.trim();if(!D){V("请填要查配额的用户 id");return}try{const K=await $(W.adminQuotaUser(D));V("用户 "+D.slice(0,8)+":本月用量 "+iu(K.used_usd_micros)+" / 配额 "+iu(K.quota_usd_micros)+(K.remaining_usd_micros!==null&&K.remaining_usd_micros!==void 0?"(剩余 "+iu(K.remaining_usd_micros)+")":"")+" | 月份 "+(K.month||""))}catch(K){V(Dc(K))}},[et]),nt=d.useCallback(async(D,K)=>{const J=D.trim();if(!J){V("请填要设置配额的用户 id");return}let Yt;if(!K.trim())Yt=null;else{const xt=Number(K.trim());if(!Number.isFinite(xt)||xt<0){V("配额金额无效:请输入 >= 0 的美元数,留空表示不限额");return}Yt=Math.round(xt*1e6)}try{const xt=await $(W.adminQuotas,"POST",{user_id:J,quota_usd_micros:Yt});V("配额已设置:用户 "+J.slice(0,8)+":本月用量 "+iu(xt.used_usd_micros)+" / 配额 "+iu(xt.quota_usd_micros)+(xt.remaining_usd_micros!==null&&xt.remaining_usd_micros!==void 0?"(剩余 "+iu(xt.remaining_usd_micros)+")":"")+" | 月份 "+(xt.month||""))}catch(xt){V(Dc(xt))}},[]),qt=d.useCallback(async()=>{if(window.confirm("确定手动触发 sweep 吗?超阈值的陈旧 running 任务会被标为 interrupted。"))try{const K=(await $(W.adminSweep,"POST",{})).swept||[];V("sweep 完成,清理 "+K.length+" 个任务"+(K.length?":"+K.join("、"):""))}catch(D){V(Dc(D))}},[]),hl=d.useCallback(async()=>{try{const D=await $(W.adminBackup,"POST",{});V("备份完成:backend="+D.backend+" | 路径 "+D.path+" | 大小 "+D.size_bytes+" 字节 | 任务 "+D.task_count+" 行")}catch(D){V(Dc(D))}},[]);return{keySaved:i,keyState:h,quotaText:y,healthText:A,adminGate:_,adminBody:Y,adminResult:B,adminUserId:et,setAdminUserId:ft,projects:F,projectId:_t,setProjectId:kt,projectMsg:Q,githubRepos:R,workers:lt,workersSummary:Et,workersError:Rt,members:ut,memberMsg:G,memberError:it,skills:St,apps:we,mcpServers:g,skillMsg:st,skillError:Mt,mcpMsg:wt,mcpOauthMsg:ht,mcpOauthBox:La,envs:Ve,envCount:Qt,envMsg:Wl,envRepoLine:va,envRepoBar:Ul,envRepoMsg:El,envSelect:oe,setEnvSelect:fe,models:fl,modelCustomVisible:ya,refreshHealth:Be,loadQuota:Nl,probeAdmin:dl,refreshProjects:$t,loadGithubRepos:Hl,loadRelayModels:vn,refreshWorkers:Va,refreshEnvs:be,refreshMembers:Il,refreshSkills:ga,saveKeyInput:fu,clearKeyInput:du,createProject:Pl,createEnv:Ce,archiveEnv:ml,saveEnvRepo:Ae,inviteMember:mu,changeMemberRole:hu,removeMember:vu,reloadMcp:q,mcpOauth:w,adminQuota:P,adminSetQuota:nt,adminSweep:qt,adminBackup:hl}}const mo=20;function ag(){const[i,r]=d.useState([]),[h,f]=d.useState(""),[y,C]=d.useState(""),[A,S]=d.useState(""),[_,M]=d.useState(""),[Y,x]=d.useState(0),[B,V]=d.useState(0),[et,ft]=d.useState({}),F=d.useRef(0),yt=d.useRef({status:"",executor:"",q:""});yt.current={status:y,executor:A,q:_};const _t=d.useRef(""),kt=d.useCallback(async(ut,U,G)=>{const L=ut!==void 0?ut:U;if(!L){r([]),V(0),ft({});return}_t.current=L;const it=G!==void 0?G:F.current,jt=yt.current;try{const St=await $(W.tasksPage(L,mo,it*mo,jt.status,jt.executor,jt.q));r(St.tasks||[]),V(St.total??(St.tasks||[]).length),ft(St.status_counts||{}),f("")}catch(St){f("加载失败:"+St.message)}},[]),Q=d.useCallback(ut=>{const U=Math.max(0,ut);F.current=U,x(U)},[]),dt=d.useCallback(ut=>{Q(ut),kt(_t.current||void 0,void 0,ut)},[kt,Q]);d.useEffect(()=>{if(!_t.current)return;const ut=setTimeout(()=>{kt(_t.current,void 0,0)},300);return()=>clearTimeout(ut)},[y,A,_]);const R=d.useCallback(ut=>{C(ut),Q(0)},[Q]),X=d.useCallback(ut=>{S(ut),Q(0)},[Q]),lt=d.useCallback(ut=>{M(ut),Q(0)},[Q]),pt=d.useCallback(async ut=>{Q(0),await kt(ut,void 0,0)},[kt,Q]),Et=Math.max(1,Math.ceil(B/mo)),mt=B===0?"共 0 个任务":"共 "+B+" 个任务,第 "+(Y+1)+" / "+Et+" 页",Rt=i,k=d.useCallback(async(ut,U)=>{const G={...ut};U.length&&(G.upload_id=await tg(U));const L=await $(W.tasksCreate,"POST",G);return"任务已建:"+xe(L.id)},[]);return{tasks:i,taskError:h,statusFilter:y,setStatusFilter:R,executorFilter:A,setExecutorFilter:X,searchText:_,setSearchText:lt,filteredTasks:Rt,taskCountText:mt,refreshTasks:kt,refreshTasksFirstPage:pt,page:Y,totalPages:Et,taskTotal:B,taskStatusCounts:et,goToPage:dt,dispatch:k}}function ng(i,r){const[h,f]=d.useState(null),[y,C]=d.useState(""),[A,S]=d.useState(""),[_,M]=d.useState(""),[Y,x]=d.useState(""),[B,V]=d.useState(void 0),[et,ft]=d.useState(!1),[F,yt]=d.useState(""),[_t,kt]=d.useState([]),[Q,dt]=d.useState([]),[R,X]=d.useState(""),[lt,pt]=d.useState([]),[Et,mt]=d.useState(""),[Rt,k]=d.useState(""),[ut,U]=d.useState([]),[G,L]=d.useState(""),[it,jt]=d.useState(""),[St,ye]=d.useState(""),[we,ce]=d.useState(""),[g,H]=d.useState([]),[st,ct]=d.useState([]),[Mt,Nt]=d.useState(""),[wt,tt]=d.useState([]),[ht,re]=d.useState(""),[La,_l]=d.useState(""),[Ve,ae]=d.useState(""),[Qt,ge]=d.useState(""),[Wl,je]=d.useState([]),[va,Ze]=d.useState(""),[Ul,wl]=Line truncated
|
||||
`+a.stack}}var ue=Object.prototype.hasOwnProperty,jl=i.unstable_scheduleCallback,Ul=i.unstable_cancelCallback,wl=i.unstable_shouldYield,cl=i.unstable_requestPaint,Xt=i.unstable_now,bl=i.unstable_getCurrentPriorityLevel,Tl=i.unstable_ImmediatePriority,_l=i.unstable_UserBlockingPriority,Se=i.unstable_NormalPriority,Wl=i.unstable_LowPriority,$l=i.unstable_IdlePriority,Il=i.log,du=i.unstable_setDisableYieldValue,Pl=null,Ce=null;function sl(t){if(typeof Il=="function"&&du(t),Ce&&typeof Ce.setStrictMode=="function")try{Ce.setStrictMode(Pl,t)}catch{}}var Ae=Math.clz32?Math.clz32:vu,mu=Math.log,hu=Math.LN2;function vu(t){return t>>>=0,t===0?32:31-(mu(t)/hu|0)|0}var pa=256,Sa=262144,xa=4194304;function B(t){var e=t&42;if(e!==0)return e;switch(t&-t){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return t&-t;case 262144:case 524288:case 1048576:case 2097152:return t&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return t&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return t}}function U(t,e,l){var a=t.pendingLanes;if(a===0)return 0;var n=0,u=t.suspendedLanes,s=t.pingedLanes;t=t.warmLanes;var o=a&134217727;return o!==0?(a=o&~u,a!==0?n=B(a):(s&=o,s!==0?n=B(s):l||(l=o&~t,l!==0&&(n=B(l))))):(o=a&~u,o!==0?n=B(o):s!==0?n=B(s):l||(l=a&~t,l!==0&&(n=B(l)))),n===0?0:e!==0&&e!==n&&(e&u)===0&&(u=n&-n,l=e&-e,u>=l||u===32&&(l&4194048)!==0)?e:n}function $(t,e){return(t.pendingLanes&~(t.suspendedLanes&~t.pingedLanes)&e)===0}function N(t,e){(e&8)!==0&&(e|=e&32);var l=t.entangledLanes;if(l!==0)for(t=t.entanglements,l&=e;0<l;){var a=31-Ae(l),n=1<<a;e|=t[a],l&=~n}return e}function X(t,e){switch(t){case 1:case 2:case 4:case 8:case 64:return e+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function tt(){var t=xa;return xa<<=1,(xa&62914560)===0&&(xa=4194304),t}function Ut(t){for(var e=[],l=0;31>l;l++)e.push(t);return e}function yt(t,e){t.pendingLanes|=e,e!==268435456&&(t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0)}function Wt(t,e,l,a,n,u){var s=t.pendingLanes;t.pendingLanes=l,t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0,t.expiredLanes&=l,t.entangledLanes&=l,t.errorRecoveryDisabledLanes&=l,t.shellSuspendCounter=0;var o=t.entanglements,m=t.expirationTimes,j=t.hiddenUpdates;for(l=s&~l;0<l;){var C=31-Ae(l),k=1<<C;o[C]=0,m[C]=-1;var S=j[C];if(S!==null)for(j[C]=null,C=0;C<S.length;C++){var E=S[C];E!==null&&(E.lane&=-536870913)}l&=~k}a!==0&&rl(t,a,0),u!==0&&n===0&&t.tag!==0&&(t.suspendedLanes|=u&~(s&~e))}function rl(t,e,l){t.pendingLanes|=e,t.suspendedLanes&=~e;var a=31-Ae(e);t.entangledLanes|=e,t.entanglements[a]=t.entanglements[a]|1073741824|l&261930}function ja(t,e){var l=t.entangledLanes|=e;for(t=t.entanglements;l;){var a=31-Ae(l),n=1<<a;n&e|t[a]&e&&(t[a]|=e),l&=~n}}function oi(t,e){var l=e&-e;return l=(l&42)!==0?1:yu(l),(l&(t.suspendedLanes|e))!==0?0:l}function yu(t){switch(t){case 2:t=1;break;case 8:t=4;break;case 32:t=16;break;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:case 4194304:case 8388608:case 16777216:case 33554432:t=128;break;case 268435456:t=134217728;break;default:t=0}return t}function Fa(t){return t&=-t,2<t?8<t?(t&134217727)!==0?32:268435456:8:2}function ta(){var t=ft.p;return t!==0?t:(t=window.event,t===void 0?32:Nh(t.type))}function ea(t,e){var l=ft.p;try{return ft.p=t,e()}finally{ft.p=l}}var El=Math.random().toString(36).slice(2),he="__reactFiber$"+El,$t="__reactProps$"+El,xe="__reactContainer$"+El,fi="__reactEvents$"+El,Ph="__reactListeners$"+El,tv="__reactHandles$"+El,jo="__reactResources$"+El,gu="__reactMarker$"+El,di="__reactLoad$"+El;function mi(t){delete t[he],delete t[$t],delete t[Ph],delete t[tv]}function Wa(t){var e;if(e=t[he])return e;for(var l=t.parentNode;l;){if(e=l[xe]||l[he]){if(l=e.alternate,e.child!==null||l!==null&&l.child!==null)for(t=ih(t);t!==null;){if(l=t[he])return l;t=ih(t)}return e}t=l,l=t.parentNode}return null}function xn(t){if(t=t[he]||t[xe]){var e=t.tag;if(e===5||e===6||e===13||e===31||e===26||e===27||e===3)return t}return null}function pu(t){var e=t.tag;if(e===5||e===26||e===27||e===6)return t.stateNode;throw Error(f(33))}function jn(t){var e=t[jo];return e||(e=t[jo]={hoistableStyles:new Map,hoistableScripts:new Map}),e}function be(t){t[gu]=!0}function bo(t){t[di]=void 0}var To=new Set,_o={};function $a(t,e){bn(t,e),bn(t+"Capture",e)}function bn(t,e)Line truncated
|
||||
`).replace(G0,"")}function Ym(t,e){return e=qm(e),qm(t)===e}function Zt(t,e,l,a,n,u){switch(l){case"children":if(typeof a=="string")e==="body"||e==="textarea"&&a===""||_n(t,a);else if(typeof a=="number"||typeof a=="bigint")e!=="body"&&_n(t,""+a);else return;break;case"className":vi(t,"class",a);break;case"tabIndex":vi(t,"tabindex",a);break;case"dir":case"role":case"viewBox":case"width":case"height":vi(t,l,a);break;case"style":ko(t,a,u);return;case"data":if(e!=="object"){vi(t,"data",a);break}case"src":case"href":if(a===""&&(e!=="a"||l!=="href")){t.removeAttribute(l);break}if(a==null||typeof a=="function"||typeof a=="symbol"||typeof a=="boolean"){t.removeAttribute(l);break}a=yi(a),t.setAttribute(l,a);break;case"action":case"formAction":if(typeof a=="function"){t.setAttribute(l,"javascript:throw new Error('A React form was unexpectedly submitted. If you called form.submit() manually, consider using form.requestSubmit() instead. If you\\'re trying to use event.stopPropagation() in a submit event handler, consider also calling event.preventDefault().')");break}else typeof u=="function"&&(l==="formAction"?(e!=="input"&&Zt(t,e,"name",n.name,n,null),Zt(t,e,"formEncType",n.formEncType,n,null),Zt(t,e,"formMethod",n.formMethod,n,null),Zt(t,e,"formTarget",n.formTarget,n,null)):(Zt(t,e,"encType",n.encType,n,null),Zt(t,e,"method",n.method,n,null),Zt(t,e,"target",n.target,n,null)));if(a==null||typeof a=="symbol"||typeof a=="boolean"){t.removeAttribute(l);break}a=yi(a),t.setAttribute(l,a);break;case"onClick":a!=null&&(t.onclick=Hl);return;case"onScroll":a!=null&&zt("scroll",t);return;case"onScrollEnd":a!=null&&zt("scrollend",t);return;case"dangerouslySetInnerHTML":if(a!=null){if(typeof a!="object"||!("__html"in a))throw Error(f(61));if(l=a.__html,l!=null){if(n.children!=null)throw Error(f(60));(u!=null?u.__html:void 0)!==l&&(t.innerHTML=l)}}break;case"multiple":t.multiple=a&&typeof a!="function"&&typeof a!="symbol";break;case"muted":t.muted=a&&typeof a!="function"&&typeof a!="symbol";break;case"suppressContentEditableWarning":case"suppressHydrationWarning":case"defaultValue":case"defaultChecked":case"innerHTML":case"ref":break;case"autoFocus":break;case"xlinkHref":if(a==null||typeof a=="function"||typeof a=="boolean"||typeof a=="symbol"){t.removeAttribute("xlink:href");break}l=yi(a),t.setAttributeNS("http://www.w3.org/1999/xlink","xlink:href",l);break;case"contentEditable":case"spellCheck":case"draggable":case"value":case"autoReverse":case"externalResourcesRequired":case"focusable":case"preserveAlpha":a!=null&&typeof a!="function"&&typeof a!="symbol"?t.setAttribute(l,a):t.removeAttribute(l);break;case"inert":case"allowFullScreen":case"async":case"autoPlay":case"controls":case"credentialless":case"default":case"defer":case"disabled":case"disablePictureInPicture":case"disableRemotePlayback":case"formNoValidate":case"hidden":case"loop":case"noModule":case"noValidate":case"open":case"playsInline":case"readOnly":case"required":case"reversed":case"scoped":case"seamless":case"itemScope":a&&typeof a!="function"&&typeof a!="symbol"?t.setAttribute(l,""):t.removeAttribute(l);break;case"capture":case"download":a===!0?t.setAttribute(l,""):a!==!1&&a!=null&&typeof a!="function"&&typeof a!="symbol"?t.setAttribute(l,a):t.removeAttribute(l);break;case"cols":case"rows":case"size":case"span":a!=null&&typeof a!="function"&&typeof a!="symbol"&&!isNaN(a)&&1<=a?t.setAttribute(l,a):t.removeAttribute(l);break;case"rowSpan":case"start":a==null||typeof a=="function"||typeof a=="symbol"||isNaN(a)?t.removeAttribute(l):t.setAttribute(l,a);break;case"popover":zt("beforetoggle",t),zt("toggle",t),hi(t,"popover",a);break;case"xlinkActuate":la(t,"http://www.w3.org/1999/xlink","xlink:actuate",a);break;case"xlinkArcrole":la(t,"http://www.w3.org/1999/xlink","xlink:arcrole",a);break;case"xlinkRole":la(t,"http://www.w3.org/1999/xlink","xlink:role",a);break;case"xlinkShow":la(t,"http://www.w3.org/1999/xlink","xlink:show",a);break;case"xlinkTitle":la(t,"http://www.w3.org/1999/xlink","xlink:title",a);break;case"xlinkType":la(t,"http://www.w3.org/1999/xlink","xlink:type",a);break;case"xmlBase":la(t,"http://www.w3.org/XML/1998/namespace","xml:base",a);break;case"xmlLang":la(t,"http://www.w3.org/XML/1998/namespace","xml:lang",a);break;case"xmlSpace":la(t,"http://www.w3.org/XML/1998/namespace","xml:space",a);break;case"is":hi(t,"is",a);break;case"innerText":case"textContent":return;default:if(!(2<l.length)||l[0]!=="o"&&l[0]!=="O"||l[1]!=="n"&&l[1]!=="N")l=iv.get(l)||l,hi(t,l,a);else return}Bt=!0}function Br(t,e,l,a,n,u){switch(l){case"style":ko(t,a,u);return;case"dangerouslySetInnerHTML":if(a!=null){if(typeof a!="object"||!("__html"in a))throw Error(f(61));if(l=a.__html,l!=null){if(n.children!=null)throw Error(f(60));(u!=null?u.__html:void 0)!==l&&(t.innerHTML=l)}}break;case"children":if(typeof a=="string")_n(t,a);else if(typeof a=="number"||typeof a=="bigint")_n(t,""+a);else return;break;case"onScroll":a!=null&&zt("scroll",t);return;case"onScrollEnd":a!=null&&zt("scroLine truncated
|
||||
`;Ut&&(Wt+=Ut+`
|
||||
`),yt&&(Wt+=(Array.isArray(yt)?yt.join(`
|
||||
`):String(yt))+`
|
||||
`),Wt+=JSON.stringify(tt,null,2).slice(0,4e3),Qe(Wt),Ne("已返回登录信息,见下方")}catch(tt){Ne("发起失败:"+tt.message)}},[]),xa=d.useCallback(async()=>{const N=W.trim();if(!N){Z("请填要查配额的用户 id");return}try{const X=await F(J.adminQuotaUser(N));Z("用户 "+N.slice(0,8)+":本月用量 "+su(X.used_usd_micros)+" / 配额 "+su(X.quota_usd_micros)+(X.remaining_usd_micros!==null&&X.remaining_usd_micros!==void 0?"(剩余 "+su(X.remaining_usd_micros)+")":"")+" | 月份 "+(X.month||""))}catch(X){Z(kc(X))}},[W]),B=d.useCallback(async(N,X)=>{const tt=N.trim();if(!tt){Z("请填要设置配额的用户 id");return}let Ut;if(!X.trim())Ut=null;else{const yt=Number(X.trim());if(!Number.isFinite(yt)||yt<0){Z("配额金额无效:请输入 >= 0 的美元数,留空表示不限额");return}Ut=Math.round(yt*1e6)}try{const yt=await F(J.adminQuotas,"POST",{user_id:tt,quota_usd_micros:Ut});Z("配额已设置:用户 "+tt.slice(0,8)+":本月用量 "+su(yt.used_usd_micros)+" / 配额 "+su(yt.quota_usd_micros)+(yt.remaining_usd_micros!==null&&yt.remaining_usd_micros!==void 0?"(剩余 "+su(yt.remaining_usd_micros)+")":"")+" | 月份 "+(yt.month||""))}catch(yt){Z(kc(yt))}},[]),U=d.useCallback(async()=>{if(window.confirm("确定手动触发 sweep 吗?超阈值的陈旧 running 任务会被标为 interrupted。"))try{const X=(await F(J.adminSweep,"POST",{})).swept||[];Z("sweep 完成,清理 "+X.length+" 个任务"+(X.length?":"+X.join("、"):""))}catch(N){Z(kc(N))}},[]),$=d.useCallback(async()=>{try{const N=await F(J.adminBackup,"POST",{});Z("备份完成:backend="+N.backend+" | 路径 "+N.path+" | 大小 "+N.size_bytes+" 字节 | 任务 "+N.task_count+" 行")}catch(N){Z(kc(N))}},[]);return{keySaved:i,keyState:h,quotaText:y,healthText:M,adminGate:_,adminBody:Q,adminResult:H,adminUserId:W,setAdminUserId:ot,projects:rt,projectId:it,setProjectId:wt,projectMsg:L,githubRepos:O,workers:lt,workersSummary:Nt,workersError:Tt,members:ht,memberMsg:G,memberError:z,skills:pt,apps:fe,mcpServers:g,skillMsg:et,skillError:Mt,mcpMsg:kt,mcpOauthMsg:ft,mcpOauthBox:Ja,envs:Xe,envCount:Gt,envMsg:Jl,envRepoLine:ya,envRepoBar:Rl,envRepoMsg:ga,envSelect:Dl,setEnvSelect:de,models:me,modelCustomVisible:kl,refreshHealth:Ul,loadQuota:wl,probeAdmin:cl,refreshProjects:Xt,loadGithubRepos:bl,loadRelayModels:Tl,refreshWorkers:_l,refreshEnvs:Se,refreshMembers:Wl,refreshSkills:$l,saveKeyInput:Il,clearKeyInput:du,createProject:Pl,createEnv:Ce,archiveEnv:sl,saveEnvRepo:Ae,inviteMember:mu,changeMemberRole:hu,removeMember:vu,reloadMcp:pa,mcpOauth:Sa,adminQuota:xa,adminSetQuota:B,adminSweep:U,adminBackup:$}}const ho=20;function ag(){const[i,r]=d.useState([]),[h,f]=d.useState(""),[y,A]=d.useState(""),[M,p]=d.useState(""),[_,R]=d.useState(""),[Q,b]=d.useState(""),[H,Z]=d.useState(0),[W,ot]=d.useState(0),[rt,bt]=d.useState({}),it=d.useRef(0),wt=d.useRef({status:"",executor:"",q:"",kind:""});wt.current={status:y,executor:M,q:Q,kind:_};const L=d.useRef(""),ut=d.useCallback(async(Y,z,vt)=>{const pt=Y!==void 0?Y:z;if(!pt){r([]),ot(0),bt({});return}L.current=pt;const ee=vt!==void 0?vt:it.current,fe=wt.current;try{const ne=await F(J.tasksPage(pt,ho,ee*ho,fe.status,fe.executor,fe.q,fe.kind));r(ne.tasks||[]),ot(ne.total??(ne.tasks||[]).length),bt(ne.status_counts||{}),f("")}catch(ne){f("加载失败:"+ne.message)}},[]),O=d.useCallback(Y=>{const z=Math.max(0,Y);it.current=z,Z(z)},[]),V=d.useCallback(Y=>{O(Y),ut(L.current||void 0,void 0,Y)},[ut,O]);d.useEffect(()=>{if(!L.current)return;const Y=setTimeout(()=>{ut(L.current,void 0,0)},300);return()=>clearTimeout(Y)},[y,M,Q,_]);const lt=d.useCallback(Y=>{A(Y),O(0)},[O]),St=d.useCallback(Y=>{p(Y),O(0)},[O]),Nt=d.useCallback(Y=>{R(Y),O(0)},[O]),mt=d.useCallback(Y=>{b(Y),O(0)},[O]),Tt=d.useCallback(async Y=>{O(0),await ut(Y,void 0,0)},[ut,O]),q=Math.max(1,Math.ceil(W/ho)),ht=W===0?"共 0 个任务":"共 "+W+" 个任务,第 "+(H+1)+" / "+q+" 页",_t=i,G=d.useCallback(async(Y,z)=>{const vt={...Y};z.length&&(vt.upload_id=await tg(z));const pt=await F(J.tasksCreate,"POST",vt);return"任务已建:"+se(pt.id)},[]);return{tasks:i,taskError:h,statusFilter:y,setStatusFilter:lt,executorFilter:M,setExecutorFilter:St,kindFilter:_,setKindFilter:Nt,searchText:Q,setSearchText:mt,filteredTasks:_t,taskCountText:ht,refreshTasks:ut,refreshTasksFirstPage:Tt,page:H,totalPages:q,taskTotal:W,taskStatusCounts:rt,goToPage:V,dispatch:G}}function ng(i,r){const[h,f]=d.useState(null),[y,A]=d.useState(""),[M,p]=d.useState(""),[_,R]=d.useState(""),[Q,b]=d.useState(""),[H,Z]=d.useState(void 0),[W,ot]=d.useState(!1),[rt,bt]=d.useState(""),[it,wt]=d.useState([]),[L,ut]=d.useState([]),[O,V]=d.useState([]),[lt,St]=d.useState(""),[Nt,mt]=d.useState([]),[Tt,q]=d.useState(""),[ht,_t]=d.useState(""),[G,Y]=d.useState([]),[z,vt]=d.useState(""),[pt,ee]=d.useState(""),[fe,ne]=d.useState(""),[g,w]=d.useState(""),[et,nt]=d.useState([]),[Mt,Ct]=d.useState([]),[kt,P]=d.useState(""),[ft,Ne]=d.useState([]),[Ja,Qe]=d.useStatLine truncated
|
||||
|
||||
`);yn=Bl.pop()||"";for(const yu of Bl){const gu=yu.split(`
|
||||
`).filter(Ke=>Ke.indexOf("data: ")===0).map(Ke=>Ke.slice(6)).join(`
|
||||
`);if(gu)try{const Ke=JSON.parse(gu);if(Ke.type==="snapshot")nt=Ke.entries||[],Object.keys(qt).forEach(ne=>delete qt[ne]),nt.forEach((ne,pe)=>{ne.id&&(qt[ne.id]=pe)}),hl=Ke.note||"",U([...nt]),L(hl),$l(q);else if(Ke.type==="entry"){const ne=Ke.entry;ne.id&&ne.id in qt?nt[qt[ne.id]]=ne:(ne.id&&(qt[ne.id]=nt.length),nt.push(ne)),U([...nt]),$l(q)}else Ke.type==="done"&&(D=!0,K=Ke.status,jt(K))}catch{}}if(D)break}}catch(J){if(J&&J.name==="AbortError")return;L("链路流中断:"+J.message);return}D&&r()},[fe,$l,r]),be=d.useCallback(async q=>{fe();try{const w=await $(W.task(q));f(w);let P="";w.result_text&&(P+=`=== result ===
|
||||
`+w.result_text.slice(0,8e3)+`
|
||||
`),w.error_text&&(P+=`=== error ===
|
||||
`+w.error_text.slice(0,8e3)+`
|
||||
`),P||(P="(暂无 result/error 输出)"),C(P),x(""),V(void 0),kl(q),ft(!1),yt(""),re(""),ge(""),vn(w.status);const nt=i(w.project_id),qt=nt==="maintainer"||nt==="owner",hl=nt==="contributor"||qt,D=su.includes(w.status),K=[];["failed","interrupted","merge-failed"].includes(w.status)&&qt||K.push("重试:不可用——仅 failed/interrupted/merge-failed 可重试"+(qt?"":",且仅 maintainer 可重试")),!D&&hl||K.push("取消:不可用——"+(D?"终态任务不可取消":"仅项目成员(contributor 及以上)可取消")),w.status==="completed"&&qt||K.push("合并:不可用——仅 completed 可合并"+(qt?"":",且仅 maintainer 可合并")),w.status==="merged"&&qt||K.push("提 PR:不可用——仅 merged 可提 PR"+(qt?"":",且仅 maintainer 可提 PR")),M(K.join(";")),S(""),Va(q,w.status),fl(q),ya(w),tl(q),te(q),dl(q),$t(q),Hl(q),Be(q),Nl(q)}catch(w){f(null),C("加载失败:"+w.message)}},[fe,Va,fl,ya,tl,te,dl,$t,Hl,Be,Nl,kl,i]),Il=d.useCallback(async()=>{const q=oe.current;if(q){try{const w=await $(W.task(q.id));f(w);let P="";w.result_text&&(P+=`=== result ===
|
||||
`+w.result_text.slice(0,8e3)+`
|
||||
`),w.error_text&&(P+=`=== error ===
|
||||
`+w.error_text.slice(0,8e3)+`
|
||||
`),C(P||"(暂无 result/error 输出)")}catch{}te(q.id),tl(q.id),Be(q.id),Nl(q.id),ya(q),fl(q.id),dl(q.id),$t(q.id),Hl(q.id),kl(q.id)}},[te,tl,Be,Nl,ya,fl,dl,$t,Hl,kl]),ga=d.useCallback(async q=>{const w=oe.current;if(!w)return;const P={cancel:"确定取消该任务吗?",merge:"确定把 completed 任务标为 merged 吗?",pr:"确定为该任务提 PR 吗?(会在仓库真实建 PR)"};if(!(P[q]&&!window.confirm(P[q])))try{const nt=await $(W.taskAction(w.id,q),"POST",{});S(q+" 已提交:"+JSON.stringify(nt).slice(0,300)),r(),await be(w.id)}catch(nt){S(q+" 失败:"+nt.message)}},[be,r]),fu=d.useCallback(async()=>{const q=oe.current;if(q)try{const w=await $(W.taskCost(q.id));x(JSON.stringify(w,null,2).slice(0,4e3))}catch(w){x("查成本失败:"+w.message)}},[]),du=d.useCallback(async q=>{const w=oe.current;if(w){if(!q.trim()){yt("分支消息必填");return}if(!su.includes(w.status)){yt("仅终态任务可分支");return}try{const P=await $(W.taskFork(w.id),"POST",{message:q.trim()});yt("分支任务已建:"+P.id.slice(0,8)),r()}catch(P){yt("分支失败:"+P.message)}}},[r]),Pl=d.useCallback(async q=>{const w=oe.current;if(w)try{await $(W.taskReviews(w.id),"POST",q),mt("评审已登记(requested)"),await Be(w.id)}catch(P){mt("登记评审需求失败:"+P.message)}},[Be]),Ce=d.useCallback(q=>{const w=oe.current;if(!w)return;const P=window.prompt("输入该条评审的 findings(将写回记录并标为 completed):");P!==null&&$(W.taskReviewFindings(w.id,q),"POST",{findings:P}).then(()=>void Be(w.id)).catch(nt=>mt("写 findings 失败:"+nt.message))},[Be]),ml=d.useCallback((q,w)=>{const P=oe.current;if(!P)return;const nt=w==="approved"?"approve":"deny";$(W.taskApprovalDecision(P.id,q,nt),"POST",{}).then(()=>{Nt("已记录"+(w==="approved"?"允许":"拒绝")+"(当前版本只留回执,不拦截实时行为)"),Nl(P.id)}).catch(qt=>Nt("决定失败:"+qt.message))},[Nl]),Ae=d.useCallback(async(q,w,P)=>{const nt=oe.current;if(nt){if(!w.trim()){re("目标任务 id 不能为空");return}if(!P.trim()){re("消息正文不能为空");return}try{const qt=await $(W.taskMessagesTo(w.trim()),"POST",{from_task_id:nt.id,body:P.trim(),mode:q});re("已发送("+qt.delivery+")"),await dl(nt.id)}catch(qt){re("发送失败:"+qt.message)}}},[dl]),mu=d.useCallback((q,w)=>{const P=oe.current;if(!P)return;if(!["completed","failed","interrupted"].includes(P.status)){ge("仅 completed/failed/interrupted 任务可操作线程(merged/merge-failed 不可改,服务端同样 409)");return}const nt={};if(q==="rename"){if(!w.trim()){ge("新线程名不能为空");return}nt.name=w.trim()}$(W.taskThreadAction(P.id,q),"POST",nt).then(qt=>{ge(q+" 已提交:"+JSON.stringify(qt).slice(0,300)),$t(P.id),Hl(P.id)}).catch(qt=>ge(q+" 失败:"+qt.message))},[$t,Hl]),hu=d.useCallback(async q=>{const w=oe.current;if(!w)return;const P=q.trim();if(!P){Ze("消息不能为空");return}try{su.includes(w.status)&&w.status!=="merged"?(await $(W.taskContinue(w.id),"POST",{message:P}),Ze("已重新排队,同一会话继续"),r(),await be(w.id)):["queued","running"].includes(w.status)?(await $(W.taskFollowups(w.id),"POST",{body:P,mode:"direct"}),Ze("已直接发送"),await te(w.id),await tl(w.id)):Ze("merged/merge-failed 任务不可继续,也不可发后续消息")}catch(nt){Ze("发送失败:"+nt.message)}},[be,r,te,tl]),vu=d.useCallback(async()=>{const q=oe.current;if(!q){ce("先选一个任务");return}try{const w=await $(W.taskTrace(q.id)),P=new Blob([JSON.stringify({task_id:q.id,status:w.status,note:w.note||"",count:(w.entries||[]).length,entries:w.entries||[]},null,2)],{type:"application/json"}),nt=document.createElement("a");nt.href=URL.createObjectURL(P),nt.download="trace-"+q.id.slice(0,8)+".json",document.body.appendChild(nt),nt.click(),nt.remove(),setTimeout(()=>URL.revokeObjectURL(nt.href),1e3),ce("已下载完整链路 "+(w.entries||[]).length+" 条")}catch(w){ce("下载失败:"+w.message)}},[]);return d.useEffect(()=>()=>fe(),[fe]),{currentTask:h,detailBody:y,actionMsg:A,actionReasons:_,costBox:Y,costSummary:B,forkVisible:et,setForkVisible:ft,forkMsg:F,coordChildren:_t,agents:Q,agentsNote:R,reviews:lt,reviewMsg:Et,reviewError:Rt,traceEntries:ut,traceNote:G,traceDone:it,traceKind:St,setTraceKind:ye,traceMsg:we,followups:g,approvals:st,approvalMsg:Mt,peerMessages:wt,peerMsg:ht,threadText:La,threadGateNote:Ve,threadMsg:Qt,threadList:Wl,chatMsg:va,chatHistory:Ul,showDetail:be,refreshDetailLists:Il,doAction:ga,taskCost:fu,forkTask:du,postReview:Pl,submitFindings:Ce,decideApproval:ml,postPeer:Ae,threadAction:mu,sendChatMessage:hu,downloadTrace:vu,refreshReviews:()=>void Be(),refreshApprovals:()=>void Nl()}}function ug(i){const[r,h]=d.useState([]),[f,y]=d.useState(""),[C,A]=d.useState(null),[S,_]=d.useState(""),[M,Y]=d.useState(null),[x,B]=d.useState(""),V=d.useCallback(async Q=>{const dt=Q!==void 0?Q:i();if(!dt){h([]);return}try{const R=await $(W.projectAutomations(dt));h(R.automations||[])}catLine truncated
|
||||
(链路摘要已截断至 300 字,完整回复见右侧任务结果)`:f,label:"助手"}}if(i.kind==="exec"||i.kind==="file"||i.kind==="tool"||i.kind==="call"){const f=i.kind==="exec"?"命令":i.kind==="file"?"文件":i.kind==="tool"?"工具":"调用";return{id:h,role:"tool",kind:i.kind,text:yo(i),label:f}}return i.kind==="plan"?{id:h,role:"status",kind:i.kind,text:i.text||""}:i.kind==="reasoning"?{id:h,role:"status",kind:i.kind,text:"思考:"+(i.summary||"")}:i.kind==="agent-start"?{id:h,role:"status",kind:i.kind,text:"子代理开始:"+(i.name||"")+(i.prompt?"("+i.prompt+")":"")}:i.kind==="agent-done"?{id:h,role:"status",kind:i.kind,text:"子代理结束:"+(i.name||"")+" "+(i.status||"")}:{id:h,role:"status",kind:i.kind,text:yo(i)}}function pg(){const i=Lt(),r=i.currentTask,h=d.useMemo(()=>{if(!r)return[];let _;if(i.chatHistory){_=i.chatHistory.messages.map(et=>({id:et.id,role:et.role,text:et.text,label:et.label,kind:et.kind}));const B=new Set(_.map(et=>et.id)),V=i.traceEntries.filter(et=>et.id&&!B.has(et.id)&&!String(et.id).startsWith("fu-")).map((et,ft)=>Lh(et,ft));_=[..._,...V]}else{const B=r.prompt?[{id:"task-prompt",role:"user",text:r.prompt}]:[],V=i.traceEntries.filter(ft=>!String(ft.id||"").startsWith("fu-")).map((ft,F)=>Lh(ft,F)),et=i.followups.map((ft,F)=>({id:"followup-"+F,role:"user",text:String(ft.body||""),label:"后续消息"}));_=[...B,...V,...et]}const M=i.traceKind,Y=M?_.filter(B=>B.role==="user"||!B.kind||B.kind===M):_,x=i.traceDone?[{id:"trace-done",role:"status",text:"链路已结束("+i.traceDone+")"}]:[];return[...Y,...x]},[r,i.chatHistory,i.traceEntries,i.followups,i.traceKind,i.traceDone]);if(!r)return c.jsx("div",{className:"muted",children:"点任务行查看详情后,这里显示继续会话"});const f=su.includes(r.status),y=["queued","running"].includes(r.status),C=f||y,A=f?"终态任务:发送走继续接口(同一会话再跑一轮)":y?"运行中/排队中:发送走后续消息直接发送(跑完当前轮后优先执行)":"merged/merge-failed 任务不可继续,也不可发后续消息",S=i.chatHistory?i.chatHistory.legacy_continue_count??null:null;return c.jsxs("div",{className:"chat-column",children:[c.jsx("div",{className:"muted",children:"实时对话"}),c.jsxs(At,{children:[c.jsxs("select",{"aria-label":"链路类型",value:i.traceKind,onChange:_=>i.setTraceKind(_.target.value),children:[c.jsx("option",{value:"",children:"链路类型:全部"}),["exec","file","call","tool","message","reasoning","plan","agent-start","agent-done"].map(_=>c.jsx("option",{value:_,children:_==="exec"?"命令":_==="file"?"文件":_==="call"?"调用":_==="tool"?"工具":_==="message"?"消息":_==="reasoning"?"思考":_==="plan"?"计划":"子代理"},_))]}),c.jsx("button",{onClick:i.downloadTrace,children:"下载完整链路"}),c.jsx("span",{className:"muted",children:i.traceMsg})]}),i.traceNote?c.jsx("div",{children:c.jsx("span",{className:"err",children:i.traceNote})}):null,S!==null&&S>0?c.jsxs("div",{className:"muted",children:["此前有 ",S," 条继续消息发生在持久化之前,未入库,上面历史不完整;部署后新发的继续消息会正常显示。"]}):null,i.chatHistory&&i.chatHistory.trace_note?c.jsx("div",{className:"muted",children:i.chatHistory.trace_note}):null,c.jsx(yg,{messages:h,empty:"(暂无会话消息)"}),c.jsx(gg,{disabled:!C,placeholder:"消息",onSend:_=>void i.sendChatMessage(_)}),c.jsxs("div",{className:"muted",children:[A,"。",i.chatMsg]}),c.jsx("div",{className:"muted",children:"对话时间线:首条是本任务 prompt(用户);助手回复、命令/文件/工具/调用、计划/思考/子代理开始结束都在同一列里,不再另起一份链路名单。流式进行中时新条目实时追加;你在底部时自动跟随,往上翻时暂停跟随,点“回到最新”恢复。"})]})}function Sg(){return Lt().currentTask?c.jsx("div",{className:"conversation",children:c.jsx(pg,{})}):c.jsx("div",{className:"conversation",children:c.jsx("div",{className:"muted",children:"在任务列表里点一条任务,对话和详情就切到该任务;也可以在上面派发一个新任务。"})})}function xg(){const i=Lt(),r=i.currentTask,[h,f]=d.useState("");if(!r)return null;const y=su.includes(r.status),C=["failed","interrupted","merge-failed"].includes(r.status);return c.jsxs(c.Fragment,{children:[c.jsxs(At,{children:[c.jsx("button",{disabled:!C,title:C?"":"仅 failed/interrupted/merge-failed 可重试",onClick:()=>i.doAction("retry"),children:"重试"}),c.jsx("button",{className:"danger",disabled:y,title:y?"终态任务不可取消":"",onClick:()=>i.doAction("cancel"),children:"取消"}),c.jsx("button",{disabled:r.status!=="completed",title:r.status!=="completed"?"仅 completed 可合并":"",onClick:()=>i.doAction("merge"),children:"合并(completed → merged)"}),c.jsx("button",{disabled:r.status!=="merged",title:r.status!=="merged"?"仅 merged 可提 PR":"",onClick:()=>i.doAction("pr"),childrLine truncated
|
||||
`);Fa=ea.pop()||"";for(const El of ea){const he=El.split(`
|
||||
`).filter($t=>$t.indexOf("data: ")===0).map($t=>$t.slice(6)).join(`
|
||||
`);if(he)try{const $t=JSON.parse(he);if($t.type==="snapshot")N=$t.entries||[],Object.keys(X).forEach(xe=>delete X[xe]),N.forEach((xe,fi)=>{xe.id&&(X[xe.id]=fi)}),tt=$t.note||"",Y([...N]),vt(tt),Fl(B);else if($t.type==="entry"){const xe=$t.entry;xe.id&&xe.id in X?N[X[xe.id]]=xe:(xe.id&&(X[xe.id]=N.length),N.push(xe)),Y([...N]),Fl(B)}else $t.type==="done"&&(Ut=!0,yt=$t.status,ee(yt))}catch{}}if(Ut)break}}catch(Wt){if(Wt&&Wt.name==="AbortError")return;vt("链路流中断:"+Wt.message);return}Ut&&r()},[il,Fl,r]),Il=d.useCallback(async B=>{il();try{const U=await F(J.task(B));f(U);let $="";U.result_text&&($+=`=== result ===
|
||||
`+U.result_text.slice(0,8e3)+`
|
||||
`),U.error_text&&($+=`=== error ===
|
||||
`+U.error_text.slice(0,8e3)+`
|
||||
`),$||($="(暂无 result/error 输出)"),A($),b(""),Z(void 0),Ul(B),ot(!1),bt(""),Qe(""),re(""),Wl(U.status);const N=i(U.project_id),X=N==="maintainer"||N==="owner",tt=N==="contributor"||X,Ut=ou.includes(U.status),yt=[];["failed","interrupted","merge-failed"].includes(U.status)&&X||yt.push("重试:不可用——仅 failed/interrupted/merge-failed 可重试"+(X?"":",且仅 maintainer 可重试")),!Ut&&tt||yt.push("取消:不可用——"+(Ut?"终态任务不可取消":"仅项目成员(contributor 及以上)可取消")),U.status==="completed"&&X||yt.push("合并:不可用——仅 completed 可合并"+(X?"":",且仅 maintainer 可合并")),U.status==="merged"&&X||yt.push("提 PR:不可用——仅 merged 可提 PR"+(X?"":",且仅 maintainer 可提 PR")),R(yt.join(";")),p(""),$l(B,U.status),kl(B),ue(U),jl(U),cl(B),wl(B),Tl(B),_l(B),Se(B),Xt(B),bl(B)}catch(U){f(null),A("加载失败:"+U.message)}},[il,$l,kl,ue,jl,cl,wl,Tl,_l,Se,Xt,bl,Ul,i]),du=d.useCallback(async()=>{const B=me.current;if(B){try{const U=await F(J.task(B.id));f(U);let $="";U.result_text&&($+=`=== result ===
|
||||
`+U.result_text.slice(0,8e3)+`
|
||||
`),U.error_text&&($+=`=== error ===
|
||||
`+U.error_text.slice(0,8e3)+`
|
||||
`),A($||"(暂无 result/error 输出)")}catch{}wl(B.id),cl(B.id),Xt(B.id),bl(B.id),ue(B),jl(B),kl(B.id),Tl(B.id),_l(B.id),Se(B.id),Ul(B.id)}},[wl,cl,Xt,bl,ue,jl,kl,Tl,_l,Se,Ul]),Pl=d.useCallback(async B=>{const U=me.current;if(!U)return;const $={cancel:"确定取消该任务吗?",merge:"确定把 completed 任务标为 merged 吗?",pr:"确定为该任务提 PR 吗?(会在仓库真实建 PR)"};if(!($[B]&&!window.confirm($[B])))try{const N=await F(J.taskAction(U.id,B),"POST",{});p(B+" 已提交:"+JSON.stringify(N).slice(0,300)),r(),await Il(U.id)}catch(N){p(B+" 失败:"+N.message)}},[Il,r]),Ce=d.useCallback(async()=>{const B=me.current;if(B)try{const U=await F(J.taskCost(B.id));b(JSON.stringify(U,null,2).slice(0,4e3))}catch(U){b("查成本失败:"+U.message)}},[]),sl=d.useCallback(async B=>{const U=me.current;if(U){if(!B.trim()){bt("分支消息必填");return}if(!ou.includes(U.status)){bt("仅终态任务可分支");return}try{const $=await F(J.taskFork(U.id),"POST",{message:B.trim()});bt("分支任务已建:"+$.id.slice(0,8)),r()}catch($){bt("分支失败:"+$.message)}}},[r]),Ae=d.useCallback(async B=>{const U=me.current;if(U)try{await F(J.taskReviews(U.id),"POST",B),q("评审已登记(requested)"),await Xt(U.id)}catch($){q("登记评审需求失败:"+$.message)}},[Xt]),mu=d.useCallback(B=>{const U=me.current;if(!U)return;const $=window.prompt("输入该条评审的 findings(将写回记录并标为 completed):");$!==null&&F(J.taskReviewFindings(U.id,B),"POST",{findings:$}).then(()=>void Xt(U.id)).catch(N=>q("写 findings 失败:"+N.message))},[Xt]),hu=d.useCallback((B,U)=>{const $=me.current;if(!$)return;const N=U==="approved"?"approve":"deny";F(J.taskApprovalDecision($.id,B,N),"POST",{}).then(()=>{P("已记录"+(U==="approved"?"允许":"拒绝")+"(当前版本只留回执,不拦截实时行为)"),bl($.id)}).catch(X=>P("决定失败:"+X.message))},[bl]),vu=d.useCallback(async(B,U,$)=>{const N=me.current;if(N){if(!U.trim()){Qe("目标任务 id 不能为空");return}if(!$.trim()){Qe("消息正文不能为空");return}try{const X=await F(J.taskMessagesTo(U.trim()),"POST",{from_task_id:N.id,body:$.trim(),mode:B});Qe("已发送("+X.delivery+")"),await Tl(N.id)}catch(X){Qe("发送失败:"+X.message)}}},[Tl]),pa=d.useCallback((B,U)=>{const $=me.current;if(!$)return;if(!["completed","failed","interrupted"].includes($.status)){re("仅 completed/failed/interrupted 任务可操作线程(merged/merge-failed 不可改,服务端同样 409)");return}const N={};if(B==="rename"){if(!U.trim()){re("新线程名不能为空");return}N.name=U.trim()}F(J.taskThreadAction($.id,B),"POST",N).then(X=>{re(B+" 已提交:"+JSON.stringify(X).slice(0,300)),_l($.id),Se($.id)}).catch(X=>re(B+" 失败:"+X.message))},[_l,Se]),Sa=d.useCallback(async B=>{const U=me.current;if(!U)return;const $=B.trim();if(!$){Fe("消息不能为空");return}try{ou.includes(U.status)&&U.status!=="merged"?(await F(J.taskContinue(U.id),"POST",{message:$}),Fe("已重新排队,同一会话继续"),r(),await Il(U.id)):["queued","running"].includes(U.status)?(await F(J.taskFollowups(U.id),"POST",{body:$,mode:"direct"}),Fe("已直接发送"),await wl(U.id),await cl(U.id)):Fe("merged/merge-failed 任务不可继续,也不可发后续消息")}catch(N){Fe("发送失败:"+N.message)}},[Il,r,wl,cl]),xa=d.useCallback(async()=>{const B=me.current;if(!B){w("先选一个任务");return}try{const U=await F(J.taskTrace(B.id)),$=new Blob([JSON.stringify({task_id:B.id,status:U.status,note:U.note||"",count:(U.entries||[]).length,entries:U.entries||[]},null,2)],{type:"application/json"}),N=document.createElement("a");N.href=URL.createObjectURL($),N.download="trace-"+B.id.slice(0,8)+".json",document.body.appendChild(N),N.click(),N.remove(),setTimeout(()=>URL.revokeObjectURL(N.href),1e3),w("已下载完整链路 "+(U.entries||[]).length+" 条")}catch(U){w("下载失败:"+U.message)}},[]);return d.useEffect(()=>()=>il(),[il]),{currentTask:h,detailBody:y,actionMsg:M,actionReasons:_,costBox:Q,costSummary:H,forkVisible:W,setForkVisible:ot,forkMsg:rt,coordChildren:it,sideThreads:L,agents:O,agentsNote:lt,reviews:Nt,reviewMsg:Tt,reviewError:ht,traceEntries:G,traceNote:z,traceDone:pt,traceKind:fe,setTraceKind:ne,traceMsg:g,followups:et,approvals:Mt,approvalMsg:kt,peerMessages:ft,peerMsg:Ja,threadText:Xe,threadGateNote:Gt,threadMsg:Jl,threadList:ya,chatMsg:Rl,chatHistory:ga,showDetail:Il,refreshDetailLists:du,doAction:Pl,taskCost:Ce,forkTask:sl,postReview:Ae,submitFindings:mu,decideApproval:hu,postPeer:vu,threadAction:pa,sendChatMessage:Sa,downloadTrace:xa,refreshReviews:()=>void Xt(),refreshApprovals:()=>void bl()}}function ug(i){const[r,h]=d.useState([]),[f,y]=d.useState(""),[A,M]=d.useState(null),[p,_]=d.useState(""),[R,Q]=d.useState(null),[b,H]=d.useState(""),Z=d.useCallback(async L=>{const ut=L!==void 0?L:i();if(!ut){h([]);return}try{const O=await F(J.projectAutomations(ut));h(O.automations||[])}catch(O){y("自动化Line truncated
|
||||
(链路摘要已截断至 300 字,完整回复见右侧任务结果)`:f,label:"助手"}}if(i.kind==="exec"||i.kind==="file"||i.kind==="tool"||i.kind==="call"){const f=i.kind==="exec"?"命令":i.kind==="file"?"文件":i.kind==="tool"?"工具":"调用";return{id:h,role:"tool",kind:i.kind,text:go(i),label:f}}return i.kind==="plan"?{id:h,role:"status",kind:i.kind,text:i.text||""}:i.kind==="reasoning"?{id:h,role:"status",kind:i.kind,text:"思考:"+(i.summary||"")}:i.kind==="agent-start"?{id:h,role:"status",kind:i.kind,text:"子代理开始:"+(i.name||"")+(i.prompt?"("+i.prompt+")":"")}:i.kind==="agent-done"?{id:h,role:"status",kind:i.kind,text:"子代理结束:"+(i.name||"")+" "+(i.status||"")}:{id:h,role:"status",kind:i.kind,text:go(i)}}function pg(){const i=Yt(),r=i.currentTask,h=d.useMemo(()=>{if(!r)return[];let _;if(i.chatHistory){_=i.chatHistory.messages.map(W=>({id:W.id,role:W.role,text:W.text,label:W.label,kind:W.kind}));const H=new Set(_.map(W=>W.id)),Z=i.traceEntries.filter(W=>W.id&&!H.has(W.id)&&!String(W.id).startsWith("fu-")).map((W,ot)=>Lh(W,ot));_=[..._,...Z]}else{const H=r.prompt?[{id:"task-prompt",role:"user",text:r.prompt}]:[],Z=i.traceEntries.filter(ot=>!String(ot.id||"").startsWith("fu-")).map((ot,rt)=>Lh(ot,rt)),W=i.followups.map((ot,rt)=>({id:"followup-"+rt,role:"user",text:String(ot.body||""),label:"后续消息"}));_=[...H,...Z,...W]}const R=i.traceKind,Q=R?_.filter(H=>H.role==="user"||!H.kind||H.kind===R):_,b=i.traceDone?[{id:"trace-done",role:"status",text:"链路已结束("+i.traceDone+")"}]:[];return[...Q,...b]},[r,i.chatHistory,i.traceEntries,i.followups,i.traceKind,i.traceDone]);if(!r)return c.jsx("div",{className:"muted",children:"点任务行查看详情后,这里显示继续会话"});const f=ou.includes(r.status),y=["queued","running"].includes(r.status),A=f||y,M=f?"终态任务:发送走继续接口(同一会话再跑一轮)":y?"运行中/排队中:发送走后续消息直接发送(跑完当前轮后优先执行)":"merged/merge-failed 任务不可继续,也不可发后续消息",p=i.chatHistory?i.chatHistory.legacy_continue_count??null:null;return c.jsxs("div",{className:"chat-column",children:[c.jsx("div",{className:"muted",children:"实时对话"}),c.jsxs(At,{children:[c.jsxs("select",{"aria-label":"链路类型",value:i.traceKind,onChange:_=>i.setTraceKind(_.target.value),children:[c.jsx("option",{value:"",children:"链路类型:全部"}),["exec","file","call","tool","message","reasoning","plan","agent-start","agent-done"].map(_=>c.jsx("option",{value:_,children:_==="exec"?"命令":_==="file"?"文件":_==="call"?"调用":_==="tool"?"工具":_==="message"?"消息":_==="reasoning"?"思考":_==="plan"?"计划":"子代理"},_))]}),c.jsx("button",{onClick:i.downloadTrace,children:"下载完整链路"}),c.jsx("span",{className:"muted",children:i.traceMsg})]}),i.traceNote?c.jsx("div",{children:c.jsx("span",{className:"err",children:i.traceNote})}):null,p!==null&&p>0?c.jsxs("div",{className:"muted",children:["此前有 ",p," 条继续消息发生在持久化之前,未入库,上面历史不完整;部署后新发的继续消息会正常显示。"]}):null,i.chatHistory&&i.chatHistory.trace_note?c.jsx("div",{className:"muted",children:i.chatHistory.trace_note}):null,c.jsx(yg,{messages:h,empty:"(暂无会话消息)"}),c.jsx(gg,{disabled:!A,placeholder:"消息",onSend:_=>void i.sendChatMessage(_)}),c.jsxs("div",{className:"muted",children:[M,"。",i.chatMsg]}),c.jsx("div",{className:"muted",children:"对话时间线:首条是本任务 prompt(用户);助手回复、命令/文件/工具/调用、计划/思考/子代理开始结束都在同一列里,不再另起一份链路名单。流式进行中时新条目实时追加;你在底部时自动跟随,往上翻时暂停跟随,点“回到最新”恢复。"})]})}function Sg(){return Yt().currentTask?c.jsx("div",{className:"conversation",children:c.jsx(pg,{})}):c.jsx("div",{className:"conversation",children:c.jsx("div",{className:"muted",children:"在任务列表里点一条任务,对话和详情就切到该任务;也可以在上面派发一个新任务。"})})}function xg(){const i=Yt(),r=i.currentTask,[h,f]=d.useState("");if(!r)return null;const y=ou.includes(r.status),A=["failed","interrupted","merge-failed"].includes(r.status),M=r.kind==="side";return c.jsxs(c.Fragment,{children:[c.jsxs(At,{children:[c.jsx("button",{disabled:!A,title:A?"":"仅 failed/interrupted/merge-failed 可重试",onClick:()=>i.doAction("retry"),children:"重试"}),c.jsx("button",{className:"danger",disabled:y,title:y?"终态任务不可取消":"",onClick:()=>i.doAction("cancel"),children:"取消"}),M?null:c.jsxs(c.Fragment,{children:[c.jsx("button",{disabled:r.status!=="completed",title:r.status!=="completed"?"仅 completed 可合并":"",onClick:()=>i.doAction("merge"),children:"合并(completed → merged)"}),c.jsx("button",{disabled:r.status!=="merged",title:r.status!=="merged"?"仅 merged 可提 Line truncated
|
||||
|
||||
`);for(;ut>=0;){const U=mt.slice(0,ut);mt=mt.slice(ut+2);for(const G of U.split(`
|
||||
`))if(G.startsWith("data: "))try{const L=JSON.parse(G.slice(6));L.waiting?S(L.task_status==="interrupted"?"任务已中断,等待续跑…":L.task_status==="paused"?"任务已暂停…":"等待会话…"):L.image&&(f("data:image/jpeg;base64,"+L.image),S("直播中"));const it={};L.holder&&(it.holder=L.holder),L.url&&(it.url=L.url),L.title!==void 0&&(it.title=L.title),Object.keys(it).length>0&&C(jt=>({...jt||{},...it}))}catch{}ut=mt.indexOf(`
|
||||
`);for(;ht>=0;){const _t=mt.slice(0,ht);mt=mt.slice(ht+2);for(const G of _t.split(`
|
||||
`))if(G.startsWith("data: "))try{const Y=JSON.parse(G.slice(6));Y.waiting?p(Y.task_status==="interrupted"?"任务已中断,等待续跑…":Y.task_status==="paused"?"任务已暂停…":"等待会话…"):Y.image&&(f("data:image/jpeg;base64,"+Y.image),p("直播中"));const z={};Y.holder&&(z.holder=Y.holder),Y.url&&(z.url=Y.url),Y.title!==void 0&&(z.title=Y.title),Object.keys(z).length>0&&A(vt=>({...vt||{},...z}))}catch{}ht=mt.indexOf(`
|
||||
|
||||
`)}}S("流已断开")}catch(lt){if(lt.name==="AbortError")return;M(lt instanceof Error?lt.message:String(lt)),S("未开始")}finally{x(!1)}},[r]),F=d.useCallback(()=>{var R;(R=B.current)==null||R.abort(),x(!1),S("已停止")},[]),yt=d.useCallback(async R=>{if(r)try{const X=await $(W.taskBrowserControl(r.id),"POST",{holder:R});C(X),M("")}catch(X){M(X instanceof Error?X.message:String(X))}},[r]),_t=d.useCallback(async R=>{if(!r||(y==null?void 0:y.holder)!=="user"||!V.current)return;const X=V.current.getBoundingClientRect(),lt=Math.round((R.clientX-X.left)/X.width*1280),pt=Math.round((R.clientY-X.top)/X.height*800);try{await $(W.taskBrowserInput(r.id),"POST",{type:"mouse_click",x:lt,y:pt})}catch(Et){M(Et instanceof Error?Et.message:String(Et))}},[r,y]),kt=d.useCallback(async R=>{if(!(!r||(y==null?void 0:y.holder)!=="user")){R.preventDefault();try{R.key.length===1?await $(W.taskBrowserInput(r.id),"POST",{type:"type",text:R.key}):await $(W.taskBrowserInput(r.id),"POST",{type:"key",key:R.key})}catch(X){M(X instanceof Error?X.message:String(X))}}},[r,y]);if(!r)return c.jsx("div",{className:"muted",children:"点任务行查看详情"});const Q=(y==null?void 0:y.holder)||"—",dt=y!==null;return c.jsxs("div",{children:[c.jsxs("div",{style:{display:"flex",gap:8,flexWrap:"wrap",alignItems:"center"},children:[c.jsxs("span",{children:["状态 ",c.jsx("b",{children:A})]}),c.jsxs("span",{className:"muted",children:["会话 ",dt?"活跃":"未激活(轮次结束即消失)"]}),c.jsxs("span",{className:"muted",children:["控制权 ",Q]}),y!=null&&y.url?c.jsx("span",{className:"muted",title:y.url,children:y.url.slice(0,80)}):null]}),c.jsxs("div",{style:{display:"flex",gap:8,flexWrap:"wrap",marginTop:8},children:[Y?c.jsx("button",{type:"button",onClick:F,children:"停止观看"}):c.jsx("button",{type:"button",onClick:()=>void ft(),children:"开始观看"}),c.jsx("button",{type:"button",onClick:()=>void et(),children:"刷新状态"}),Q!=="user"?c.jsx("button",{type:"button",onClick:()=>void yt("user"),disabled:!dt,children:"接管"}):c.jsx("button",{type:"button",onClick:()=>void yt("agent"),children:"交还给 Agent"})]}),_?c.jsx("div",{className:"muted",style:{marginTop:8},children:_}):null,Q==="user"?c.jsx("div",{className:"muted",style:{marginTop:8},children:"你已接管:点画面即点击,键盘输入直达页面。交还后 Agent 续跑前会先快照确认你的改动。"}):null,c.jsx("div",{style:{marginTop:8},children:h?c.jsx("img",{ref:V,src:h,alt:"浏览器实时画面",tabIndex:0,onClick:R=>void _t(R),onKeyDown:R=>void kt(R),style:{width:"100%",maxWidth:1280,cursor:Q==="user"?"crosshair":"default",border:"1px solid var(--border, #ccc)"}}):c.jsx("div",{className:"muted",children:dt?"点“开始观看”接入画面流(接管后点画面即点击)":"浏览器会话未激活:任务本轮未开浏览器,或轮次已结束。"})})]})}function bg(){const i=Lt(),[r,h]=d.useState("uncommittedChanges"),[f,y]=d.useState(""),[C,A]=d.useState(""),[S,_]=d.useState("");return c.jsxs(c.Fragment,{children:[c.jsx("h3",{children:"评审队列"}),c.jsxs("select",{"aria-label":"评审目标",value:r,onChange:M=>h(M.target.value),children:[c.jsx("option",{value:"uncommittedChanges",children:"未提交改动(工作区)"}),c.jsx("option",{value:"baseBranch",children:"与分支的差异"}),c.jsx("option",{value:"commit",children:"指定提交"}),c.jsx("option",{value:"custom",children:"自定义评审指令"})]}),c.jsx("input",{className:"stacked",placeholder:"与分支的差异用:分支名(如 main)",style:{width:"100%"},value:f,onChange:M=>y(M.target.value)}),c.jsx("input",{className:"stacked",placeholder:"指定提交用:提交 sha",style:{width:"100%"},value:C,onChange:M=>A(M.target.value)}),c.jsx("textarea",{placeholder:"自定义评审指令用:评审指令正文",value:S,onChange:M=>_(M.target.value)}),c.jsxs(At,{children:[c.jsx("button",{className:"primary",onClick:()=>void i.postReview({target_type:r,branch:f.trim()||void 0,sha:C.trim()||void 0,instructions:S.trim()||void 0}),children:"登记评审需求"}),c.jsx("button",{onClick:i.refreshReviews,children:"刷新队列"}),c.jsx("span",{className:"muted",children:i.reviewMsg})]}),c.jsx("div",{className:"muted",children:"本基线只登记评审需求(requested),不自动跑评审;评审是否执行、结果如何,以评审队列里的记录为准。"}),i.reviewError?c.jsx("div",{className:"muted",children:i.reviewError}):i.reviews.length===0?c.jsx("div",{className:"muted",children:"点任务行查看详情后,点“刷新队列”查看评审记录"}):c.jsx(ou,{children:i.reviews.map(M=>{let Y=M.target_type||"";return M.branch&&(Y+=":"+M.branch),M.sha&&(Y+=":"+String(M.sha).slice(0,12)),M.instructions&&(Y+=":"+String(M.instructions).slice(0,80)),c.jsxs("li",{children:[c.jsxs("span",{className:"row-main",children:[c.jsx("b",{children:"评审"})," ",xe(M.id)," | ",Y," | ",c.jsx("span",{className:"st-"+(M.status||""),children:M.sLine truncated
|
||||
`)}}p("流已断开")}catch(lt){if(lt.name==="AbortError")return;R(lt instanceof Error?lt.message:String(lt)),p("未开始")}finally{b(!1)}},[r]),rt=d.useCallback(()=>{var O;(O=H.current)==null||O.abort(),b(!1),p("已停止")},[]),bt=d.useCallback(async O=>{if(r)try{const V=await F(J.taskBrowserControl(r.id),"POST",{holder:O});A(V),R("")}catch(V){R(V instanceof Error?V.message:String(V))}},[r]),it=d.useCallback(async O=>{if(!r||(y==null?void 0:y.holder)!=="user"||!Z.current)return;const V=Z.current.getBoundingClientRect(),lt=Math.round((O.clientX-V.left)/V.width*1280),St=Math.round((O.clientY-V.top)/V.height*800);try{await F(J.taskBrowserInput(r.id),"POST",{type:"mouse_click",x:lt,y:St})}catch(Nt){R(Nt instanceof Error?Nt.message:String(Nt))}},[r,y]),wt=d.useCallback(async O=>{if(!(!r||(y==null?void 0:y.holder)!=="user")){O.preventDefault();try{O.key.length===1?await F(J.taskBrowserInput(r.id),"POST",{type:"type",text:O.key}):await F(J.taskBrowserInput(r.id),"POST",{type:"key",key:O.key})}catch(V){R(V instanceof Error?V.message:String(V))}}},[r,y]);if(!r)return c.jsx("div",{className:"muted",children:"点任务行查看详情"});const L=(y==null?void 0:y.holder)||"—",ut=y!==null;return c.jsxs("div",{children:[c.jsxs("div",{style:{display:"flex",gap:8,flexWrap:"wrap",alignItems:"center"},children:[c.jsxs("span",{children:["状态 ",c.jsx("b",{children:M})]}),c.jsxs("span",{className:"muted",children:["会话 ",ut?"活跃":"未激活(轮次结束即消失)"]}),c.jsxs("span",{className:"muted",children:["控制权 ",L]}),y!=null&&y.url?c.jsx("span",{className:"muted",title:y.url,children:y.url.slice(0,80)}):null]}),c.jsxs("div",{style:{display:"flex",gap:8,flexWrap:"wrap",marginTop:8},children:[Q?c.jsx("button",{type:"button",onClick:rt,children:"停止观看"}):c.jsx("button",{type:"button",onClick:()=>void ot(),children:"开始观看"}),c.jsx("button",{type:"button",onClick:()=>void W(),children:"刷新状态"}),L!=="user"?c.jsx("button",{type:"button",onClick:()=>void bt("user"),disabled:!ut,children:"接管"}):c.jsx("button",{type:"button",onClick:()=>void bt("agent"),children:"交还给 Agent"})]}),_?c.jsx("div",{className:"muted",style:{marginTop:8},children:_}):null,L==="user"?c.jsx("div",{className:"muted",style:{marginTop:8},children:"你已接管:点画面即点击,键盘输入直达页面。交还后 Agent 续跑前会先快照确认你的改动。"}):null,c.jsx("div",{style:{marginTop:8},children:h?c.jsx("img",{ref:Z,src:h,alt:"浏览器实时画面",tabIndex:0,onClick:O=>void it(O),onKeyDown:O=>void wt(O),style:{width:"100%",maxWidth:1280,cursor:L==="user"?"crosshair":"default",border:"1px solid var(--border, #ccc)"}}):c.jsx("div",{className:"muted",children:ut?"点“开始观看”接入画面流(接管后点画面即点击)":"浏览器会话未激活:任务本轮未开浏览器,或轮次已结束。"})})]})}function bg(){const i=Yt(),[r,h]=d.useState("uncommittedChanges"),[f,y]=d.useState(""),[A,M]=d.useState(""),[p,_]=d.useState("");return c.jsxs(c.Fragment,{children:[c.jsx("h3",{children:"评审队列"}),c.jsxs("select",{"aria-label":"评审目标",value:r,onChange:R=>h(R.target.value),children:[c.jsx("option",{value:"uncommittedChanges",children:"未提交改动(工作区)"}),c.jsx("option",{value:"baseBranch",children:"与分支的差异"}),c.jsx("option",{value:"commit",children:"指定提交"}),c.jsx("option",{value:"custom",children:"自定义评审指令"})]}),c.jsx("input",{className:"stacked",placeholder:"与分支的差异用:分支名(如 main)",style:{width:"100%"},value:f,onChange:R=>y(R.target.value)}),c.jsx("input",{className:"stacked",placeholder:"指定提交用:提交 sha",style:{width:"100%"},value:A,onChange:R=>M(R.target.value)}),c.jsx("textarea",{placeholder:"自定义评审指令用:评审指令正文",value:p,onChange:R=>_(R.target.value)}),c.jsxs(At,{children:[c.jsx("button",{className:"primary",onClick:()=>void i.postReview({target_type:r,branch:f.trim()||void 0,sha:A.trim()||void 0,instructions:p.trim()||void 0}),children:"登记评审需求"}),c.jsx("button",{onClick:i.refreshReviews,children:"刷新队列"}),c.jsx("span",{className:"muted",children:i.reviewMsg})]}),c.jsx("div",{className:"muted",children:"本基线只登记评审需求(requested),不自动跑评审;评审是否执行、结果如何,以评审队列里的记录为准。"}),i.reviewError?c.jsx("div",{className:"muted",children:i.reviewError}):i.reviews.length===0?c.jsx("div",{className:"muted",children:"点任务行查看详情后,点“刷新队列”查看评审记录"}):c.jsx(Sn,{children:i.reviews.map(R=>{let Q=R.target_type||"";return R.branch&&(Q+=":"+R.branch),R.sha&&(Q+=":"+String(R.sha).slice(0,12)),R.instructions&&(Q+=":"+String(R.instructions).slice(0,80)),c.jsxs("li",{children:[c.jsxs("span",{className:"row-main",children:[c.jsx("b",{children:"评审"})," ",se(R.id)," | ",Q," | ",c.jsx("span",{className:"st-"+(R.status||""),children:R.Line truncated
|
||||
<style rel="stylesheet" crossorigin>:root{--text:#1f2328;--muted:#656d76;--subtle:#6e7781;--line:#d0d7de;--line-muted:#d8dee4;--paper:#ffffff;--card:#ffffff;--subtle-bg:#f6f8fa;--inset-bg:#f6f8fa;--accent:#0969da;--accent-emphasis:#0969da;--green:#2da44e;--green-hover:#2c974b;--success:#1a7f37;--danger:#d1242f;--danger-bg:#ffebe9;--attention:#9a6700;--done:#8250df;--selected-bg:#ddf4ff;--radius:6px;--shadow:0 1px 0 rgba(27,31,36,.04);--sans:-apple-system,BlinkMacSystemFont,"Segoe UI","Noto Sans",Helvetica,Arial,sans-serif;--mono:ui-monospace,SFMono-Regular,SFMono-Regular,Menlo,Consolas,"Liberation Mono",monospace;--ctl:32px}*{box-sizing:border-box}html,body,#root{height:100%}body{font-family:var(--sans);margin:0;color:var(--text);background:#fff;line-height:1.5;font-size:14px}a{color:var(--accent);text-underline-offset:3px}:focus-visible{outline:2px solid var(--accent);outline-offset:2px}@media(prefers-reduced-motion:reduce){*{scroll-behavior:auto!important;transition:none!important}}.muted{color:var(--muted);font-size:12px;line-height:1.5}.err{color:var(--danger)}.ok{color:var(--success)}button{cursor:pointer;font-weight:500;border-radius:var(--radius);border:1px solid var(--line);background:#fff;color:var(--text);box-shadow:var(--shadow)}button:hover{background:var(--subtle-bg)}button:disabled{opacity:.45;cursor:not-allowed;box-shadow:none}button.primary,button.seal{background:var(--green);border-color:#1b1f2426;color:#fff}button.primary:hover,button.seal:hover{background:var(--green-hover)}button.danger{color:var(--danger);border-color:var(--line);background:#fff}button.danger:hover{background:var(--danger-bg);color:var(--danger)}.icon-btn{width:32px;height:32px;padding:0;display:grid;place-items:center;border:1px solid var(--line);border-radius:var(--radius);background:#fff;color:var(--text);font-size:16px;line-height:1}.icon-btn:hover{background:var(--subtle-bg)}.dash-topbar{position:sticky;top:0;z-index:10;background:#24292f;color:#fff;border-bottom:1px solid rgba(255,255,255,.15);padding:10px 20px 8px}.dash-topbar .muted{color:#ffffffbf}.dash-topbar-row{display:flex;gap:14px;align-items:center;flex-wrap:wrap}.dash-brand{font-weight:600;font-size:16px;color:#fff}.dash-status{font-size:13px;font-weight:600;color:#ffffffbf}.dash-status.ok{color:#2da44e}.dash-stamp{margin-left:auto}.dash-nav{display:flex;gap:4px;flex-wrap:wrap;margin-top:8px}.dash-nav a{color:#fff;text-decoration:none;font-size:13px;font-weight:600;padding:6px 12px;border-radius:var(--radius)}.dash-nav a:hover{background:#ffffff1f;text-decoration:none}.tab-nav button{background:transparent;border:0;box-shadow:none;color:#fff;font-size:13px;font-weight:600;padding:6px 12px;border-radius:var(--radius)}.tab-nav button:hover{background:#ffffff1f}.tab-nav button.active{background:#ffffff2e}.dash-page-tabs{display:flex}.dash-page-tabs>.tab-content{grid-column:1 / -1;min-width:0;display:flex;flex-direction:column;gap:20px}.tab-workbench{display:grid;grid-template-columns:320px minmax(0,1fr);gap:20px;align-items:start}.tab-workbench>*{min-width:0}@media(max-width:899px){.tab-workbench{grid-template-columns:1fr}.tab-workbench #sec-dispatch{order:1}.tab-workbench #sec-tasks{order:2}.tab-workbench #sec-conv{order:3}}.dash-page{max-width:1180px;margin:0 auto;padding:20px;display:flex;flex-direction:column;gap:20px}.dash-page>*{min-width:0}.dash-section>*{min-width:0}.dash-section{scroll-margin-top:130px;display:flex;flex-direction:column;gap:16px}.dash-section.card{padding:16px}.dash-cards{display:grid;grid-template-columns:repeat(3,1fr);gap:12px}.dash-card{background:#fff;border:1px solid var(--line);border-radius:var(--radius);padding:16px;min-width:0}.dash-card-label{color:var(--muted);font-size:12px;font-weight:600}.dash-card-value{font-size:24px;font-weight:600;line-height:1.3;overflow-wrap:anywhere}.card{background:var(--card);border:1px solid var(--line);border-radius:var(--radius);box-shadow:none;padding:16px}.card h2{font-size:14px;margin:0 0 12px;font-weight:600}.card h3{font-size:14px;margin:24px 0 12px;padding-top:16px;border-top:1px solid var(--line);font-weight:600}.card h3:first-of-type{border-top:0;padding-top:0}.composer h2{font-size:16px}.composer textarea.prompt{min-height:148px;font-size:14px;padding:12px;border-radius:var(--radius)}.composer textarea.coord{font-family:var(--mono);font-size:13px}.bar{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin:12px 0}.bar>button,.bar>label{display:inline-flex;align-items:center;height:var(--ctl);min-height:var(--ctl);padding:0 16px;box-sizing:border-box;white-space:nowrap}.bar>input,.bar>select{height:var(--ctl);min-height:var(--ctl)}input,select,textarea,button{font:inherit;font-size:14px;padding:5px 12px;border-radius:var(--radius);border:1px solid var(--line);background:#fff;color:var(--text)}input:focus,select:focus,textarea:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px #0969da4d}select{-moz-appearance:none;appearance:none;-webkit-appearance:none;cursLine truncated
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -944,6 +944,7 @@ POSTGRES_SCHEMA = (
|
||||
"coordinator_parts TEXT, "
|
||||
"parent_task_id TEXT, "
|
||||
"coordinator_part TEXT, "
|
||||
"kind TEXT, "
|
||||
"goal_resume_pending INTEGER DEFAULT 0, "
|
||||
"agent_name TEXT, "
|
||||
"agent_version TEXT, "
|
||||
@@ -954,6 +955,8 @@ POSTGRES_SCHEMA = (
|
||||
"audit_loop_packet TEXT, "
|
||||
"UNIQUE(project_id, idempotency_key))",
|
||||
"CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_tasks_parent"
|
||||
" ON tasks(parent_task_id)",
|
||||
# 附件包与任务附件:与 server._ensure_uploads_tables 同款,
|
||||
# POSTGRES_SCHEMA 在每次初始化时全量执行,新表自动补建。
|
||||
"CREATE TABLE IF NOT EXISTS uploads("
|
||||
|
||||
@@ -199,6 +199,8 @@ def _task_to_dict(row, attachments=None, total_tokens=None):
|
||||
if _row_get(row, "coordinator_parts") else None),
|
||||
"parent_task_id": _row_get(row, "parent_task_id"),
|
||||
"coordinator_part": _row_get(row, "coordinator_part"),
|
||||
# 旁聊(Muse 角色对齐 P2):NULL = 老行 = 主聊。
|
||||
"kind": _row_get(row, "kind") or "main",
|
||||
"goal_resume_pending": _row_get(row, "goal_resume_pending") or 0,
|
||||
# agent 定义 v2:身份与指令落库后直接回读(老行缺列
|
||||
# 时为 None;指令是定义文本、非密钥,任务详情里可见)。
|
||||
|
||||
+24
-6
@@ -346,8 +346,15 @@ def extract_task_creates(text):
|
||||
executor = payload.get("executor")
|
||||
if executor is not None and executor not in EXECUTORS:
|
||||
continue
|
||||
# 旁聊(P2 G7):块里可显式指定 kind("main"|"side"),
|
||||
# 非法值整块跳过(与 executor 非法同款);不指定时
|
||||
# worker 按父任务 kind 继承(旁聊父 → 旁聊子)。
|
||||
kind = payload.get("kind")
|
||||
if kind is not None and kind not in ("main", "side"):
|
||||
continue
|
||||
out.append({"name": name, "prompt": prompt, "model": model,
|
||||
"effort": effort, "executor": executor})
|
||||
"effort": effort, "executor": executor,
|
||||
"kind": kind})
|
||||
return out
|
||||
|
||||
|
||||
@@ -1011,18 +1018,20 @@ def parent_spawn_generation(conn, parent_id):
|
||||
|
||||
只数 parent_task_id 非空且 coordinator_part 为空的祖先链(即
|
||||
派生链);协调员子任务(coordinator_part 非空)本身不是派生
|
||||
任务,代数为 0,链到此为止。走 32 步仍未到头时按已数到的算,
|
||||
绝不死循环。"""
|
||||
任务,代数为 0,链到此为止;旁聊任务(kind='side')同款——
|
||||
旁聊不是 marker 派生,链到旁聊为止。走 32 步仍未到头时按已数
|
||||
到的算,绝不死循环。"""
|
||||
generation = 0
|
||||
cur = parent_id
|
||||
for _ in range(32):
|
||||
if not cur:
|
||||
break
|
||||
row = conn.execute(
|
||||
"SELECT parent_task_id, coordinator_part FROM tasks"
|
||||
" WHERE id = ?", (cur,)).fetchone()
|
||||
"SELECT * FROM tasks WHERE id = ?", (cur,)).fetchone()
|
||||
if row is None:
|
||||
break
|
||||
if row_get(row, "kind") == "side":
|
||||
break
|
||||
parent = row_get(row, "parent_task_id")
|
||||
part = row_get(row, "coordinator_part")
|
||||
if parent is not None and part is None:
|
||||
@@ -1034,7 +1043,16 @@ def parent_spawn_generation(conn, parent_id):
|
||||
|
||||
|
||||
def count_direct_spawn_children(conn, parent_id):
|
||||
"""单个父任务的直接派生子任务数(不含协调员子任务)。"""
|
||||
"""单个父任务的直接派生子任务数(不含协调员子任务与旁聊)。"""
|
||||
try:
|
||||
row = conn.execute(
|
||||
"SELECT COUNT(*) FROM tasks WHERE parent_task_id = ?"
|
||||
" AND coordinator_part IS NULL"
|
||||
" AND (kind IS NULL OR kind != 'side')",
|
||||
(parent_id,)).fetchone()
|
||||
except Exception:
|
||||
# 老库/最小测试表缺 kind 列:当时不可能有旁聊行,
|
||||
# 不带 kind 条件的结果与带条件等价。
|
||||
row = conn.execute(
|
||||
"SELECT COUNT(*) FROM tasks WHERE parent_task_id = ?"
|
||||
" AND coordinator_part IS NULL", (parent_id,)).fetchone()
|
||||
|
||||
+6
-1
@@ -236,7 +236,12 @@ def migrate_sqlite_to_postgres(sqlite_path, pg_conn=None, tables=None):
|
||||
# 的 `or 0` 兜底拦不住,audit_loop=0 的
|
||||
# 历史任务会被悄悄当成启用(Audit batch 1)。
|
||||
("audit_loop", "INTEGER"),
|
||||
("audit_loop_packet", "TEXT")):
|
||||
("audit_loop_packet", "TEXT"),
|
||||
# 旁聊(Muse 角色对齐 P2):kind 列须与
|
||||
# db.py POSTGRES_SCHEMA、server
|
||||
# migrate_tasks_table 同步;NULL = 老行
|
||||
# = 主聊,无回填。
|
||||
("kind", "TEXT")):
|
||||
if _col not in _existing:
|
||||
try:
|
||||
pg_conn.execute(
|
||||
|
||||
+81
-9
@@ -13,6 +13,7 @@
|
||||
- CloudExecutor:占位,未接通云端执行。
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
@@ -43,6 +44,39 @@ from cloud.peer_messages import extract_peer_messages as _extract_peer_messages
|
||||
|
||||
MAX_STDOUT_CHARS = 2000
|
||||
|
||||
# 租户 Docker 网络子网划段(2026-11-07 B 方案)。
|
||||
#
|
||||
# 背景:宿主 iptables 只放行 172.16.0.0/12 与 127.0.0.1 访问
|
||||
# Tinyproxy(8889),容器内 codex 必须经该代理到模型网关。
|
||||
# 以前建网不传 --subnet,子网由 Docker 默认池分配,曾分到
|
||||
# 192.168.32.0/20(放行范围外),容器零次补全、单轮 600 秒
|
||||
# 超时两轮后任务失败。划段区选 172.16.0.0/16:它在放行范围
|
||||
# 内,且 Docker 默认池从 172.17.0.0/16 起分,永远不会自动分
|
||||
# 到 172.16 段,与现存网络、宿主 LAN、Tailscale 均不重叠
|
||||
# (2026-11-07 主机实测:10 个网络,172.16.0.0/16 空闲)。
|
||||
# 每个租户网占一个 /24(254 个地址足够);同一网络名每次
|
||||
# 算出的候选顺序相同(sha256 首字节起算 + 线性探测),不依
|
||||
# 赖任何持久化状态;候选子网已被别的租户网占用时顺延下一
|
||||
# 个。容量上限 256 个租户网/宿主(32 次探测封顶),满了就
|
||||
# 明确返回失败,不静默降级——扩容需另行决策(删残留旧网或
|
||||
# 扩大放行范围),不要在这里偷偷换段。
|
||||
TENANT_SUBNET_PREFIX = "172.16"
|
||||
TENANT_SUBNET_SLOTS = 256
|
||||
TENANT_SUBNET_MAX_PROBES = 32
|
||||
|
||||
|
||||
def tenant_subnet_candidates(name):
|
||||
"""按网络名确定性排出候选租户子网(/24,172.16.0.0/16 内)。
|
||||
|
||||
首候选由 sha256(网络名) 首字节决定,其余按槽位顺延;
|
||||
不同网络名一般落在不同槽,撞槽时靠 ensure_network 的
|
||||
overlap 探测顺延。纯函数,不碰 Docker、不读状态。
|
||||
"""
|
||||
seed = hashlib.sha256(name.encode("utf-8")).digest()[0]
|
||||
for i in range(TENANT_SUBNET_MAX_PROBES):
|
||||
slot = (seed + i) % TENANT_SUBNET_SLOTS
|
||||
yield "%s.%d.0/24" % (TENANT_SUBNET_PREFIX, slot)
|
||||
|
||||
|
||||
def tmp_isolation_status():
|
||||
"""各层 /tmp 隔离现状的唯一口径(README 与单测都以它为准)。
|
||||
@@ -558,7 +592,7 @@ class LocalExecutor(Executor):
|
||||
return re.sub(r"[^A-Za-z0-9_.-]", "-", raw)
|
||||
|
||||
def dispatch_task(self, task, seed_dir=None, extra_env=None,
|
||||
model_provider=None) -> dict:
|
||||
model_provider=None, no_worktree=False) -> dict:
|
||||
"""调 host.py dispatch:建 host 任务并跑一轮 turn。
|
||||
|
||||
命令:host.py [--config CFG] [--model M] dispatch --name <host 名>
|
||||
@@ -571,6 +605,8 @@ class LocalExecutor(Executor):
|
||||
model 换成 fallback 模型,第二次 dispatch/run 就跑在新模型上)。
|
||||
seed_dir 非空时追加 --seed-dir:host.py 在建 worktree 后、
|
||||
跑 turn 前把暂存附件拷进去(run 续跑不走这条路,不重复拷)。
|
||||
no_worktree(旁聊默认):追加 --no-worktree,host.py 不建
|
||||
worktree/分支,无 repo 时以 per-task scratch 目录为 cwd。
|
||||
"""
|
||||
if not isinstance(task, dict) or task.get("id") is None:
|
||||
raise ValueError("任务缺少 id")
|
||||
@@ -591,6 +627,8 @@ class LocalExecutor(Executor):
|
||||
cmd += ["--repo", repo]
|
||||
if seed_dir:
|
||||
cmd += ["--seed-dir", seed_dir]
|
||||
if no_worktree:
|
||||
cmd += ["--no-worktree"]
|
||||
return self._result_from_runner(
|
||||
task["id"], self._run_local(cmd, extra_env))
|
||||
|
||||
@@ -639,7 +677,7 @@ class LocalExecutor(Executor):
|
||||
task["id"], self._run_local(cmd, extra_env))
|
||||
|
||||
def goal_task(self, task, seed_dir=None, extra_env=None,
|
||||
model_provider=None) -> dict:
|
||||
model_provider=None, no_worktree=False) -> dict:
|
||||
"""调 host.py goal:目标循环(turn→验测→喂回重试)。
|
||||
|
||||
命令:host.py [--config CFG] [--model M] goal --name <host 名>
|
||||
@@ -686,6 +724,8 @@ class LocalExecutor(Executor):
|
||||
cmd += ["--repo", repo]
|
||||
if seed_dir:
|
||||
cmd += ["--seed-dir", seed_dir]
|
||||
if no_worktree:
|
||||
cmd += ["--no-worktree"]
|
||||
return self._goal_result_from_runner(
|
||||
task["id"], self._run_local(cmd, extra_env))
|
||||
|
||||
@@ -833,7 +873,8 @@ class LocalExecutor(Executor):
|
||||
}
|
||||
|
||||
def popen_task(self, task, kind="dispatch", seed_dir=None,
|
||||
extra_env=None, model_provider=None):
|
||||
extra_env=None, model_provider=None,
|
||||
no_worktree=False):
|
||||
"""进程组启动 host.py 子进程,返回 Popen(供真取消用)。
|
||||
|
||||
start_new_session=True 让子进程自成进程组,worker 取消时
|
||||
@@ -886,6 +927,8 @@ class LocalExecutor(Executor):
|
||||
cmd += ["--repo", repo]
|
||||
if seed_dir:
|
||||
cmd += ["--seed-dir", seed_dir]
|
||||
if no_worktree:
|
||||
cmd += ["--no-worktree"]
|
||||
elif kind == "goal-resume-breakpoint":
|
||||
# G10 断点:无 --goal-text,不开新循环;host 侧按
|
||||
# goal_rounds_done 的下一轮接着跑,轮次上限沿用原
|
||||
@@ -915,6 +958,8 @@ class LocalExecutor(Executor):
|
||||
cmd += ["--repo", repo]
|
||||
if seed_dir:
|
||||
cmd += ["--seed-dir", seed_dir]
|
||||
if no_worktree:
|
||||
cmd += ["--no-worktree"]
|
||||
if extra_env:
|
||||
merged = dict(os.environ)
|
||||
merged.update(extra_env)
|
||||
@@ -1471,13 +1516,17 @@ class DockerExecutor(Executor):
|
||||
|
||||
def dispatch_task(self, task, timeout=None, extra_mounts=None,
|
||||
network=None, extra_env=None,
|
||||
seed_host_dir=None, model_provider=None) -> dict:
|
||||
seed_host_dir=None, model_provider=None,
|
||||
no_worktree=False) -> dict:
|
||||
"""容器内调 host.py dispatch:建 host 任务并跑一轮 turn。
|
||||
|
||||
seed_host_dir 非空时把暂存目录只读挂载进容器
|
||||
(/tmp/codex-seed),内层 host.py 用 --seed-dir
|
||||
/tmp/codex-seed 拷进 worktree;挂载整个目录(不是单个
|
||||
文件)是为了一包多文件与子目录。"""
|
||||
文件)是为了一包多文件与子目录。no_worktree(旁聊):
|
||||
内层命令追加 --no-worktree,容器内 host.py 不建
|
||||
worktree/分支(worker 只在本地默认旁聊上接线,显式
|
||||
docker 旁聊走既有语义;此处只做透传)。"""
|
||||
if not isinstance(task, dict) or task.get("id") is None:
|
||||
raise ValueError("任务缺少 id")
|
||||
if not task.get("prompt"):
|
||||
@@ -1496,6 +1545,8 @@ class DockerExecutor(Executor):
|
||||
if seed_host_dir:
|
||||
inner += ["--seed-dir", "/tmp/codex-seed"]
|
||||
mounts.append("%s:/tmp/codex-seed:ro" % seed_host_dir)
|
||||
if no_worktree:
|
||||
inner += ["--no-worktree"]
|
||||
return self._run_in_container(task, inner, timeout=timeout,
|
||||
extra_mounts=mounts or None,
|
||||
network=network,
|
||||
@@ -1539,11 +1590,13 @@ class DockerExecutor(Executor):
|
||||
|
||||
def goal_task(self, task, timeout=None, extra_mounts=None,
|
||||
network=None, extra_env=None,
|
||||
seed_host_dir=None, model_provider=None) -> dict:
|
||||
seed_host_dir=None, model_provider=None,
|
||||
no_worktree=False) -> dict:
|
||||
"""容器内调 host.py goal:目标循环(turn→验测→喂回重试)。
|
||||
|
||||
seed_host_dir 语义同 dispatch_task(只读挂载 + 内层
|
||||
--seed-dir /tmp/codex-seed)。
|
||||
--seed-dir /tmp/codex-seed)。no_worktree 语义同
|
||||
dispatch_task(内层命令追加 --no-worktree)。
|
||||
|
||||
不走 _run_in_container:它经 _parse_host_result 截头
|
||||
stdout,而 goal 结论在末尾,截头会把结论截掉;这里直接
|
||||
@@ -1584,6 +1637,8 @@ class DockerExecutor(Executor):
|
||||
if seed_host_dir:
|
||||
inner += ["--seed-dir", "/tmp/codex-seed"]
|
||||
mounts.append("%s:/tmp/codex-seed:ro" % seed_host_dir)
|
||||
if no_worktree:
|
||||
inner += ["--no-worktree"]
|
||||
cmd = self.build_run_command(task, inner,
|
||||
extra_mounts=mounts or None,
|
||||
network=network,
|
||||
@@ -1957,6 +2012,15 @@ class DockerExecutor(Executor):
|
||||
def ensure_network(self, name):
|
||||
"""确保 Docker 网络存在:不存在就创建,已存在就忽略。
|
||||
|
||||
创建时显式 --subnet 固定在 172.16.0.0/12 内划段(见
|
||||
tenant_subnet_candidates):宿主 iptables 只放行该范围
|
||||
访问 egress 代理,不传子网时 Docker 默认池可能分到范
|
||||
围外(如 192.168.32.0/20),容器连不上代理、任务空等
|
||||
600 秒超时。候选子网已被占用时按 overlap 报错顺延;
|
||||
已存在(同名)直接复用——注意:这意味着 2026-11-07
|
||||
之前建好的范围外旧网(如 codex-smoke-20261107)不会
|
||||
被自动纠正,须人工删除后重建,见部署清单。
|
||||
|
||||
返回 True 表示网络可用(创建成功或本来就存在),
|
||||
False 表示创建失败(调用方继续派发,docker run 时会
|
||||
因网络不存在而明确失败,不在这里静默降级默认网络——
|
||||
@@ -1966,8 +2030,11 @@ class DockerExecutor(Executor):
|
||||
"""
|
||||
if not name:
|
||||
return False
|
||||
for subnet in tenant_subnet_candidates(name):
|
||||
try:
|
||||
result = self._call_runner(["docker", "network", "create", name],
|
||||
result = self._call_runner(
|
||||
["docker", "network", "create",
|
||||
"--subnet", subnet, name],
|
||||
timeout=30)
|
||||
except Exception as exc:
|
||||
return "already exists" in str(exc)
|
||||
@@ -1976,9 +2043,14 @@ class DockerExecutor(Executor):
|
||||
return True
|
||||
text = (getattr(result, "stderr", "") or "") + \
|
||||
(getattr(result, "stdout", "") or "")
|
||||
return "already exists" in text
|
||||
if "already exists" in text:
|
||||
return True
|
||||
if "overlap" in text.lower():
|
||||
continue
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
return False
|
||||
|
||||
def execute(self, task) -> dict:
|
||||
"""只读执行:容器内调 host.py status,不直接跑模型回合。
|
||||
|
||||
+145
-5
@@ -1234,6 +1234,11 @@ def migrate_tasks_table(conn):
|
||||
"coordinator_parts": "TEXT",
|
||||
"parent_task_id": "TEXT",
|
||||
"coordinator_part": "TEXT",
|
||||
"kind": "TEXT",
|
||||
# 旁聊(Muse 角色对齐 P2):kind 记任务类型
|
||||
# ('main' | 'side',NULL = 老行 = 主聊,与 goal_mode
|
||||
# NULL=0、plan NULL=0 同款惯例);parent_task_id
|
||||
# 的语义从“协调员/派生父任务”扩为“父会话”。
|
||||
# G10:goal 断点续跑待处理标记(1 = 已调 /goal-resume、
|
||||
# 待 worker 按断点续跑;worker 认领后清掉,跑完不再
|
||||
# 重复触发)。与 resume_message 不同:后者是“给旧会
|
||||
@@ -1725,6 +1730,9 @@ def init_db(db_path):
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_tasks_status"
|
||||
" ON tasks(status)")
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_tasks_parent"
|
||||
" ON tasks(parent_task_id)")
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
@@ -2320,6 +2328,20 @@ def _goal_signal_status(status):
|
||||
return "pending_mirror_to_host_loop"
|
||||
|
||||
|
||||
def _filter_tasks_by_kind(rows, kind_f):
|
||||
"""旁聊(Muse 角色对齐 P2):GET /tasks 的 kind 过滤。
|
||||
|
||||
kind_f 为空时原样返回(缺省 = 全部,老行为);否则只留
|
||||
`_task_to_dict` 的 kind 等于 kind_f 的行。抽成独立函数,
|
||||
不让 `_list_tasks` 再长(长方法清单 pin 在 18 个,见
|
||||
test_longfunc_inventory)。
|
||||
"""
|
||||
if not kind_f:
|
||||
return rows
|
||||
return [r for r in rows
|
||||
if _task_to_dict(r, [], 0)["kind"] == kind_f]
|
||||
|
||||
|
||||
class CloudControlPlaneHandler(OAuthMixin, IdentityMemoryMixin, ProjectMixin, AutomationMixin, FullAutoMixin,
|
||||
BaseHTTPRequestHandler):
|
||||
"""控制面请求处理:除 /health 与 /webhooks/* 外一律要求 X-API-Key。"""
|
||||
@@ -3812,6 +3834,69 @@ class CloudControlPlaneHandler(OAuthMixin, IdentityMemoryMixin, ProjectMixin, Au
|
||||
"error": "协调员任务不跑模型回合,不能与"
|
||||
" goal/plan/验收命令同用"})
|
||||
return
|
||||
# 父任务血缘(Muse 角色对齐 P1):创建请求可携带
|
||||
# parent_task_id;父任务是协调员且本次新建也是协调员
|
||||
# 时拒绝(深度恒 ≤1)。父不存在 404、跨项目 400。
|
||||
# 不带 coordinator_parts 时 parent_task_id 仅作血缘记录
|
||||
# (第二阶段旁聊的父子关系用同一入参)。
|
||||
parent_task_id = body.get("parent_task_id")
|
||||
if parent_task_id is not None:
|
||||
if not isinstance(parent_task_id, str):
|
||||
self._send_json(400, {
|
||||
"error": "parent_task_id must be a string"})
|
||||
return
|
||||
parent_row = self._fetch_task(parent_task_id)
|
||||
if parent_row is None:
|
||||
self._send_json(404, {"error": "parent task not found"})
|
||||
return
|
||||
parent_task = _task_to_dict(parent_row)
|
||||
if parent_task["project_id"] != project_id:
|
||||
self._send_json(400, {
|
||||
"error": "parent task belongs to another project"})
|
||||
return
|
||||
if coord_parts and parent_task.get("coordinator_parts"):
|
||||
self._send_json(400, {
|
||||
"error": "coordinator task cannot be created"
|
||||
" under a coordinator task"})
|
||||
return
|
||||
# 旁聊(Muse 角色对齐 P2):kind 只认 "main" | "side",
|
||||
# 缺省 = 主聊;旁聊须带 parent_task_id(已在上段校验
|
||||
# 存在与同项目),不得带 coordinator_parts(协调员
|
||||
# 子任务与旁聊是两种父子关系),不得带 upload_id /
|
||||
# environment_id(附件与环境语义都要求 worktree,
|
||||
# 旁聊默认没有);父任务不得是协调员任务(协调员
|
||||
# 本身不跑模型回合,旁聊挂上去没有父会话语义,且
|
||||
# 会污染协调员的子任务同步快照)。
|
||||
kind = body.get("kind")
|
||||
if kind is None:
|
||||
kind = "main"
|
||||
if kind not in ("main", "side"):
|
||||
self._send_json(400, {
|
||||
"error": "kind must be 'main' or 'side'"})
|
||||
return
|
||||
if kind == "side":
|
||||
if parent_task_id is None:
|
||||
self._send_json(400, {
|
||||
"error": "side thread requires parent_task_id"})
|
||||
return
|
||||
if coord_parts:
|
||||
self._send_json(400, {
|
||||
"error": "side thread cannot carry coordinator_parts"})
|
||||
return
|
||||
if parent_task is not None and parent_task.get(
|
||||
"coordinator_parts"):
|
||||
self._send_json(400, {
|
||||
"error": "side thread parent cannot be a"
|
||||
" coordinator task"})
|
||||
return
|
||||
if body.get("upload_id") is not None:
|
||||
self._send_json(400, {
|
||||
"error": "side thread cannot carry upload_id"})
|
||||
return
|
||||
if body.get("environment_id") is not None:
|
||||
self._send_json(400, {
|
||||
"error": "side thread cannot carry environment_id"})
|
||||
return
|
||||
idempotency_key = body.get("idempotency_key")
|
||||
if idempotency_key is not None and not isinstance(idempotency_key, str):
|
||||
self._send_json(400, {"error": "idempotency_key must be a string"})
|
||||
@@ -3893,7 +3978,10 @@ class CloudControlPlaneHandler(OAuthMixin, IdentityMemoryMixin, ProjectMixin, Au
|
||||
# 未指定且无环境继承时默认 docker:空 executor(任意
|
||||
# worker 可认领)会让任务落到隔离较弱的 local worker,
|
||||
# 是多用户下的隔离缺口;显式 local/cloud 不受影响。
|
||||
executor = _default_executor()
|
||||
# 旁聊例外(P2 G1):旁聊是会话式使用,默认 local、
|
||||
# 不拉容器;docker/cloud 仅显式要求时才走。
|
||||
executor = "local" if kind == "side" \
|
||||
else _default_executor()
|
||||
# 附件绑定(upload_id 由 POST /uploads 发出):包必须存在、
|
||||
# 归属当前用户、未使用、24 小时内、暂存目录还在;cloud
|
||||
# 执行器没有 worktree 可 seed,直接拒绝。校验在 INSERT
|
||||
@@ -4019,19 +4107,19 @@ class CloudControlPlaneHandler(OAuthMixin, IdentityMemoryMixin, ProjectMixin, Au
|
||||
" goal, verify_command, goal_max_rounds,"
|
||||
" model, effort, approval, plan, goal_mode,"
|
||||
" environment_id, coordinator_parts,"
|
||||
" parent_task_id, coordinator_part,"
|
||||
" parent_task_id, coordinator_part, kind,"
|
||||
" child_model, child_effort,"
|
||||
" agent_name, agent_version, agent_instructions,"
|
||||
" agent_tools, agent_tool_approvals, audit_loop)"
|
||||
" VALUES(?, ?, ?, 'queued', ?, ?, ?, ?, ?, ?, NULL, ?,"
|
||||
" ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
" ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
(task_id, project_id, prompt, idempotency_key,
|
||||
json.dumps(depends_on), priority, user_id, now, now,
|
||||
executor, goal, verify_command, goal_max_rounds,
|
||||
model, effort, approval, plan, goal_mode,
|
||||
environment_id,
|
||||
json.dumps(coord_parts) if coord_parts else None,
|
||||
None, None, child_model, child_effort,
|
||||
parent_task_id, None, kind, child_model, child_effort,
|
||||
agent_name_val, agent_version_val,
|
||||
agent_instructions_val, agent_tools_val,
|
||||
agent_tool_approvals_val, audit_loop_val),
|
||||
@@ -4120,6 +4208,13 @@ class CloudControlPlaneHandler(OAuthMixin, IdentityMemoryMixin, ProjectMixin, Au
|
||||
parent_ids = query.get("parent_task_id")
|
||||
if parent_ids:
|
||||
parent_task_id = parent_ids[0]
|
||||
# 旁聊(Muse 角色对齐 P2):kind=main|side 可选,缺省 =
|
||||
# 全部(老行为不变);非法值 400。
|
||||
kind_f = query["kind"][0] if query.get("kind") else None
|
||||
if kind_f is not None and kind_f not in ("main", "side"):
|
||||
self._send_json(400, {
|
||||
"error": "kind must be 'main' or 'side'"})
|
||||
return
|
||||
limit = None
|
||||
offset = 0
|
||||
if query.get("limit"):
|
||||
@@ -4156,13 +4251,15 @@ class CloudControlPlaneHandler(OAuthMixin, IdentityMemoryMixin, ProjectMixin, Au
|
||||
s = r["status"]
|
||||
status_counts[s] = status_counts.get(s, 0) + 1
|
||||
if parent_task_id:
|
||||
filtered = rows
|
||||
filtered = _filter_tasks_by_kind(rows, kind_f)
|
||||
else:
|
||||
filtered = []
|
||||
for r in rows:
|
||||
d = _task_to_dict(r, [], 0)
|
||||
if status_f and d["status"] != status_f:
|
||||
continue
|
||||
if kind_f and d["kind"] != kind_f:
|
||||
continue
|
||||
if executor_f and (d["executor"] or "") != executor_f:
|
||||
continue
|
||||
if q_f and q_f not in ((d["id"] or "") + " " + (d["prompt"] or "")).lower():
|
||||
@@ -4440,6 +4537,15 @@ class CloudControlPlaneHandler(OAuthMixin, IdentityMemoryMixin, ProjectMixin, Au
|
||||
return
|
||||
if self._reject_if_over_quota_for_task(user_id, task):
|
||||
return
|
||||
# 协调员任务不接受后续消息(Muse 角色对齐 P1):写入侧
|
||||
# 会 201 成功、消费侧没有任何读点,宁可入口 409 明拒。
|
||||
# 改分工请取消后重建协调员;给分工追加指令请直接发给
|
||||
# 该子任务。
|
||||
if task.get("coordinator_parts"):
|
||||
self._send_json(409, {
|
||||
"error": "coordinator tasks do not accept"
|
||||
" follow-up messages"})
|
||||
return
|
||||
text, mode, body_err = _validate_followup_body(body)
|
||||
if body_err is not None:
|
||||
self._send_json(body_err[0], body_err[1])
|
||||
@@ -5922,6 +6028,12 @@ class CloudControlPlaneHandler(OAuthMixin, IdentityMemoryMixin, ProjectMixin, Au
|
||||
(http_code, payload),不直接 _send_json;配额检查
|
||||
由调用方在调本方法前做。"""
|
||||
task_id = task["id"]
|
||||
# 协调员任务不接受后续消息(Muse 角色对齐 P1):与
|
||||
# _create_followup 同款入口 409,见该处注释。
|
||||
if task.get("coordinator_parts"):
|
||||
return 409, {
|
||||
"error": "coordinator tasks do not accept"
|
||||
" follow-up messages"}
|
||||
goal_target = _parse_goal_command(text)
|
||||
# /goal 前置判定见 _classify_goal_followup(纯计算,与
|
||||
# _create_followup 共用,但只用于 goal_target 非空时:
|
||||
@@ -6444,6 +6556,14 @@ class CloudControlPlaneHandler(OAuthMixin, IdentityMemoryMixin, ProjectMixin, Au
|
||||
if err is not None:
|
||||
self._send_json(err[0], {"error": err[1]})
|
||||
return
|
||||
# 旁聊(P2 G5):旁聊没有 worktree,/worktree 按
|
||||
# “没有本地 worktree”口径回 404,不回 exists=false
|
||||
# 的摘要(/files 与 /tree 同款分支已在
|
||||
# _task_worktree_for_files 里回 404)。
|
||||
if task.get("kind") == "side":
|
||||
self._send_json(404, {
|
||||
"error": "no local worktree for this task"})
|
||||
return
|
||||
summary = self._worktree_summary(task)
|
||||
self._send_json(200, dict(summary, task_id=task_id))
|
||||
|
||||
@@ -6739,6 +6859,13 @@ class CloudControlPlaneHandler(OAuthMixin, IdentityMemoryMixin, ProjectMixin, Au
|
||||
return
|
||||
if self._reject_if_over_quota(user_id):
|
||||
return
|
||||
# 旁聊(P2 G6):旁聊无 worktree/分支,分叉 thread
|
||||
# 历史的变体不在首版,直接 409,不等 worker/host 层
|
||||
# 以 500/ValueError 收尾。
|
||||
if task.get("kind") == "side":
|
||||
self._send_json(409, {
|
||||
"error": "side thread has no worktree/branch to fork"})
|
||||
return
|
||||
message = body.get("message")
|
||||
if not message or not isinstance(message, str) \
|
||||
or not message.strip():
|
||||
@@ -6987,6 +7114,13 @@ class CloudControlPlaneHandler(OAuthMixin, IdentityMemoryMixin, ProjectMixin, Au
|
||||
user_id, task["project_id"], "approve_merge"):
|
||||
self._send_json(403, {"error": "forbidden"})
|
||||
return
|
||||
# 旁聊(P2 G6):旁聊无 worktree/分支,合并门禁的五项
|
||||
# 证据(CI/目录/契约/批准/评审)无一有落点,直接 409,
|
||||
# 不进门禁检查。
|
||||
if task.get("kind") == "side":
|
||||
self._send_json(409, {
|
||||
"error": "side thread has no worktree/branch to merge"})
|
||||
return
|
||||
_merge_ok, _merge_reason = _merge_state_decision(
|
||||
task["status"])
|
||||
if not _merge_ok:
|
||||
@@ -8820,6 +8954,12 @@ pid 须为整数、address 须为字符串,否则 400(注册表里的
|
||||
user_id, task["project_id"], "approve_merge"):
|
||||
self._send_json(403, {"error": "forbidden"})
|
||||
return
|
||||
# 旁聊(P2 G6):旁聊无 worktree/分支,建 PR 无分支可推,
|
||||
# 直接 409,不等 host 层以 500/ValueError 收尾。
|
||||
if task.get("kind") == "side":
|
||||
self._send_json(409, {
|
||||
"error": "side thread has no worktree/branch for a PR"})
|
||||
return
|
||||
if task["status"] != "merged":
|
||||
self._send_json(409, {
|
||||
"error": "only merged tasks can create a PR",
|
||||
|
||||
@@ -367,16 +367,22 @@ G0 的六问缺一问,G0 就不算过。用户没回复时,协调员等,
|
||||
角色模板的权威来源是本文件末尾的附录。附录随 SKILL.md 一起播种,
|
||||
所以租户任务内一定能看到。`templates/roles/` 下的文件是编辑用的源文件,
|
||||
发布前必须核对附录和源文件一致,不一致时以附录为准并先修源文件。
|
||||
协调员派生子任务时,必须把对应角色模板全文贴进子任务提示词,
|
||||
并补上本任务的输入、目录、分支和完成定义。
|
||||
协调员派生子任务时,主聊人工派生须把对应角色模板全文贴进子任务提示词,
|
||||
并补上本任务的输入、目录、分支和完成定义;平台协调员任务的分工 prompt
|
||||
即子任务 prompt 全文,角色模板不由平台注入,如需模板内容,创建人须自己
|
||||
写进分工 prompt。
|
||||
只写一句“按 software-engineering skill 做”是不够的,
|
||||
子任务不一定会读到本技能。
|
||||
术语:本技能说的“线程”就是平台的“任务(task)”;“协调员任务”就是
|
||||
`coordinator_parts` 非空的任务;“分工”就是 `coordinator_parts` 的一项;
|
||||
“子任务”就是 `parent_task_id` 指向协调员的任务;`forked_from` 链与
|
||||
`parent_task_id` 是两条血缘,不要混用。
|
||||
|
||||
## 协调员派生子任务的必填字段
|
||||
|
||||
每个子任务提示词必须包含,缺一项就不派生:
|
||||
|
||||
1. 角色模板全文(来自本文件附录,租户任务内可直接引用)。
|
||||
1. 角色模板内容(主聊人工派生时贴模板全文,来自本文件附录,租户任务内可直接引用;平台协调员任务的分工 prompt 即子任务 prompt 全文,角色模板不由平台注入,如需模板内容,创建人须自己写进分工 prompt)。
|
||||
2. 本阶段输入:PRD 哪个版本、契约哪个版本、设计稿哪个版本。
|
||||
3. 拥有目录:只能写这一个目录;只读目录:契约、他人目录。
|
||||
4. 分支名:`feat/swe/<阶段>-<角色>-<简称>`,例如 `feat/swe/client-web`、
|
||||
@@ -400,6 +406,8 @@ G0 的六问缺一问,G0 就不算过。用户没回复时,协调员等,
|
||||
| 完成定义 | 产出文件清单 + 必须跑过的自测命令 + 必须提交的证据 |
|
||||
| TODO-UPDATE 要求 | 结尾报告必须带 TODO-UPDATE 小节,格式见“待办机制” |
|
||||
|
||||
校验表里的拥有目录 / 分支名 / 版本 / TODO-UPDATE 是写入分工 prompt 的约定内容,平台只校验分工字段(`coordinator.validate_parts`),不校验、不解析这些项;“缺一项就不派生”在平台侧不成立——平台只校验 `validate_parts` 的字段。
|
||||
|
||||
子任务精简包(裁剪规则写死,不是“尽量精简”):只给本端相关的
|
||||
契约章节、设计页、PRD 故事和冻结决策摘要,绝不把 PRD、契约、
|
||||
设计全文复制给每个线程。精简包由四部分组成,缺一部分就补齐再发:
|
||||
@@ -442,13 +450,19 @@ TODO-UPDATE
|
||||
- 冻结前:架构线程可以自由改契约,改完要通知协调员更新版本号。
|
||||
- 冻结物(G2 必须齐):API 契约、数据模型、agent 协议、
|
||||
任务状态机、错误 taxonomy、技术栈表、每端能力矩阵。
|
||||
(7 项齐全由协调员人工核对;平台只校验契约版本号与冻结标志,
|
||||
不校验冻结物清单、不发回归任务。)
|
||||
任务状态机至少包含:创建、排队、运行中、等审批、已完成、
|
||||
已取消、失败、超时;错误 taxonomy 至少区分:模型失败、工具失败、
|
||||
用户取消、超时、权限拒绝、配额超限。同一错误在各端不得换一个名字。
|
||||
- 冻结后:任何线程想改契约,必须给协调员发任务间消息,写清:
|
||||
改什么、为什么、影响哪些客户端、是否向后兼容。
|
||||
(收件方是扮演协调员的模型/人工;平台协调员任务本身暂不支持
|
||||
后续指令,见“任务间问答默认策略与决策记录”一节。)
|
||||
- 协调员批准后:契约版本号加一(v1 -> v2),架构线程改契约,
|
||||
受影响的客户端各收到一条变更通知和一个回归任务。
|
||||
(版本号加一与回归任务派发是人工/协调员操作,平台不执行;
|
||||
平台只校验版本号与冻结标志。)
|
||||
- 协调员不批准:提议线程按旧契约做,差异记入已知问题,不悄悄分叉。
|
||||
|
||||
## 任务间问答默认策略与决策记录(v0.3 新增)
|
||||
@@ -458,6 +472,7 @@ TODO-UPDATE
|
||||
协调员在下一轮答复:确认假设、纠正假设(发回归任务)、或升级为契约变更;
|
||||
答复本身也要记入决策记录。等答复期间线程不阻塞在原地,先做契约内
|
||||
无争议的部分。
|
||||
- 平台协调员任务暂不支持后续指令:给进行中的协调员任务发后续消息会被入口直接拒绝(409),因为平台循环内没有消费点。替代方案见附录 00“任务间问答与决策记录”一节。
|
||||
- 决策记录落盘路径:`docs/decisions.md`,由协调员维护,子线程只在报告里
|
||||
提议、不直接改。只追加不改写历史:一条决策写清日期、议题、决定、理由、
|
||||
影响端;推翻旧决策时追加新条写明“取代 D-n”,绝不改旧条原文、不删旧条。
|
||||
@@ -531,6 +546,8 @@ tests/ # QA 线程专属,含跨端一致性用例
|
||||
(只改拥有目录,契约变更经批准除外;检查命令与输出文件清单必须
|
||||
记入合并记录,见“目录与分支规则”);③ 契约版本是冻结版;
|
||||
④ 协调员评审通过;⑤ 无未解决评审意见。有一项不过就不合并。
|
||||
协调员批准与契约冻结版本是每次调合并门禁时提交的证据,平台不持久化;
|
||||
谎报由审计与 PR/Checks 只读核对事后发现。
|
||||
- CI 怎么查:用只读方式查,例如本平台的 GET /tasks/{id}/pr/checks。
|
||||
它是只读诊断,绝不重跑检查、不批准、不合并;还没建 PR 时它会 409
|
||||
(查无可查),这时先补 PR,不要把它当成 CI 通过。
|
||||
@@ -538,9 +555,8 @@ tests/ # QA 线程专属,含跨端一致性用例
|
||||
要求 PR、要求 CI 通过、禁止直接推。目标仓库没开保护时,
|
||||
协调员要在决策记录里写明风险并去开通,不能假装已保护,
|
||||
不能靠“大家自觉”代替保护。
|
||||
- 合并门禁(2026-10-30 补,代码在 cloud/merge_gate.py,服务端
|
||||
POST /tasks/{id}/merge 已接线;2026-10-31 硬门禁轨加
|
||||
cloud/merge_gate_hard.py):保护状态只能由调用方或环境变量
|
||||
- 合并门禁(服务端 POST /tasks/{id}/merge 已接线,门禁实现在
|
||||
cloud/merge_gate_hard.py,默认 hard):保护状态只能由调用方或环境变量
|
||||
CLOUD_BRANCH_PROTECTION 显式声明,代码不去 GitHub 核实、不声称
|
||||
已保护。模式只由环境变量 CLOUD_MERGE_GATE_MODE 控制,默认
|
||||
hard:上面五项全查,缺一 409 且任务状态不变,并写
|
||||
@@ -624,8 +640,10 @@ tests/ # QA 线程专属,含跨端一致性用例
|
||||
- G5 缺陷回路:缺陷回对应客户端或架构线程修,修完回 G5 回归,
|
||||
回归范围按契约变更影响面定,不是每次都全量重测。
|
||||
- 成本控制:分阶段本身就是成本控制。G4a 先验证,G4b / G4c 复用已验证物;
|
||||
协调员、产品、架构用较强模型,重复性强的客户端实现和 QA 可用
|
||||
较经济的模型,具体以项目实际可用模型和配额为准;
|
||||
模型与强度按平台字段走:分工不写 model/effort 时,继承协调员任务的
|
||||
child_model/child_effort 或自身模型;“协调员、产品、架构用较强模型,
|
||||
重复性强的客户端实现和 QA 可用较经济的模型”只是建议,具体以项目
|
||||
实际可用模型和配额为准;
|
||||
配额将满时先停新线程,不先透支再解释。
|
||||
|
||||
|
||||
@@ -750,6 +768,8 @@ tests/ # QA 线程专属,含跨端一致性用例
|
||||
出一份单独签字的审计结论。谁写、谁测、谁审、谁执行、谁签字
|
||||
五职分开:写是开发线程,测是 QA 线程,审是审计线程,
|
||||
执行合并/发布是人或宿主操作,签字是审计签字人。
|
||||
- 平台的 `audit_loop` 是协调员任务的自动重排循环,不是独立审计线程;
|
||||
独立审计仍须另开线程,两者阻塞关系均为“与”,不可互换。
|
||||
- 同一人不兼两职,可核查:审计记录的签字人字段必须与任务执行人
|
||||
字段、协调员批准人字段、QA 测试人字段都不同,有一项相同,
|
||||
审计结论本身无效,按审计 fail 处理。代码里这就是
|
||||
@@ -1263,6 +1283,31 @@ full-auto-executor + installation id。质量门禁全部自动且阻塞合并
|
||||
建仓分支要诚实(组织仓可经 App token 建,用户个人仓需安装会话里先建空仓,
|
||||
记为 needs_user_once,不说成已自动建仓)。
|
||||
|
||||
## 平台协调员(以平台代码为准,skill 不得另说一套)
|
||||
|
||||
- 平台协调员任务(`coordinator_parts` 非空的任务)不跑模型回合:
|
||||
worker 按分工自动扇出并行子任务、等齐、合流成一份汇总报告写回
|
||||
父任务 `result_text`,汇总报告就是协调员任务的终局产物;合并决定
|
||||
仍由你(模型/人工)在评审环节给,但评审发生在子任务终态之后,
|
||||
报告之前没有“逐个放行”。
|
||||
- 分工一次建齐、并行认领;分工最多 20 个;子任务 prompt 即分工 prompt
|
||||
原文,平台不贴角色模板、不贴校验表、不贴精简包。
|
||||
- 有界委派(建任务时声明,事后不可改):单分工超时 `timeout_seconds`
|
||||
(1..86400 秒,超时只取消该分工、其余继续等)、失败容忍
|
||||
`on_failure`(`fail` 默认 / `tolerate`:容忍分工失败不拖垮整体,
|
||||
重跑时其 `failed` 子任务不重排、`interrupted` 的仍重排)、单项截断
|
||||
`result_chars`(默认 500、上限 2000)。全局超时 3600 秒:到时取消
|
||||
挂起子任务、父任务失败。平台不提供分工 token/费用预算、分工轮数
|
||||
上限、按分工隔离写范围——不要许诺这三类边界。
|
||||
- 配额预留:分工数超过 1 时,剩余配额须覆盖分工数 × $1(可经环境变量
|
||||
调),不足建任务即 402;子任务费用记父任务创建人账。
|
||||
- 取消与孤儿收口由平台自动做:取消/超时终局取消未终态子任务并标
|
||||
`interrupted`,常驻循环每轮扫尾父已终态、子仍未终态的孤儿;你不需、
|
||||
也不能手动“取消子任务”。
|
||||
- 模型与强度:分工不写 `model`/`effort` 时,继承协调员任务的
|
||||
`child_model`/`child_effort` 或自身模型;“协调员、产品、架构用
|
||||
较强模型”只是建议,不是平台字段。
|
||||
|
||||
## 你必须先做的事
|
||||
|
||||
1. 按 `software-engineering` 技能的 G0 问卷问清:目标用户、核心能力、
|
||||
@@ -1281,12 +1326,17 @@ full-auto-executor + installation id。质量门禁全部自动且阻塞合并
|
||||
完成定义。之后所有状态以这份待办为准,不以对话为准。
|
||||
你是待办文件的唯一写作者,子线程只在结尾报告里写 TODO-UPDATE,
|
||||
你核对证据后才勾选。不要虚构任何待办标记语法,不要依赖模型会自动写标记块。
|
||||
3. 每个子任务提示词必须包含:角色模板全文(来自 SKILL.md 附录)、
|
||||
本阶段输入的版本号、拥有目录、只读目录、分支名、完成定义、
|
||||
禁止事项、TODO-UPDATE 要求。缺一项不派生。
|
||||
3. 每个子任务提示词必须包含:本阶段输入的版本号、拥有目录、
|
||||
只读目录、分支名、完成定义、禁止事项、TODO-UPDATE 要求,
|
||||
以及角色模板内容——主聊人工派生时贴模板全文;平台协调员任务
|
||||
的分工 prompt 即子任务 prompt 全文,角色模板不由平台注入,
|
||||
如需模板内容,创建人须自己写进分工 prompt。缺一项不派生。
|
||||
派生前校验表(必填表,填好的表原样贴进提示词,缺一项就不发任务):
|
||||
客户端名、拥有目录、分支名、契约版本、PRD 版本、设计版本、
|
||||
完成定义、TODO-UPDATE 要求,共 8 项。
|
||||
完成定义、TODO-UPDATE 要求,共 8 项。校验表里的拥有目录 /
|
||||
分支名 / 版本 / TODO-UPDATE 是写入分工 prompt 的约定内容,
|
||||
平台只校验分工字段(`coordinator.validate_parts`),不校验、
|
||||
不解析这些项。
|
||||
子任务精简包(裁剪规则写死):只给本端相关的契约章节、设计页、
|
||||
PRD 故事和冻结决策摘要(不超过 10 条),绝不把 PRD、契约、设计全文
|
||||
复制给每个线程;提示词里必须列出精简包所含章节清单,只写“已精简”不算数;
|
||||
@@ -1298,7 +1348,8 @@ full-auto-executor + installation id。质量门禁全部自动且阻塞合并
|
||||
- G2:架构师和 UI/UX 设计 2 个线程并行,中间加一次联合评审:
|
||||
架构的状态机和设计的审批界面对得上才能冻结;收敛后你判定契约是否冻结,
|
||||
冻结物必须齐:API 契约、数据模型、agent 协议、任务状态机、错误 taxonomy、
|
||||
技术栈表、每端能力矩阵。
|
||||
技术栈表、每端能力矩阵。(7 项齐全由你人工核对;平台只校验契约版本号
|
||||
与冻结标志,不校验冻结物清单、不发回归任务。)
|
||||
- G3:共享核心 1 个线程(含 mock 服务和契约测试);后端线程如有,
|
||||
在 G3 阶段就参与契约测试,不要等到 G4 才对接。
|
||||
- G3.5:可行性 spike。核心链路真跑通(agent 工具就是任务能创建、能跑出进度、
|
||||
@@ -1327,6 +1378,9 @@ full-auto-executor + installation id。质量门禁全部自动且阻塞合并
|
||||
(问题清单、热修复记录、回滚触发是否命中)+ v1.1 交接清单齐全,
|
||||
两项缺一不进 G8;G8 的完成不是下一版已发布,是下一轮 G0-G6 的输入
|
||||
已冻结齐全。
|
||||
(阶段顺序是流程编排;平台协调员任务建出后各分工并行执行,不按 G1–G8
|
||||
分阶段放行——需要分阶段时,分多次建协调员任务,上一阶段的协调员终态
|
||||
后再建下一阶段。)
|
||||
|
||||
## 你的合并规则(评审与执行拆分)
|
||||
|
||||
@@ -1357,6 +1411,11 @@ full-auto-executor + installation id。质量门禁全部自动且阻塞合并
|
||||
- 决策记录落盘 `docs/decisions.md`,由你维护,只追加不改写历史;
|
||||
一条决策写清日期、议题、决定、理由、影响端;推翻旧决策追加新条
|
||||
写明“取代 D-n”,绝不改旧条原文、不删旧条。
|
||||
- 平台协调员任务暂不支持后续指令:给进行中的协调员任务发后续消息
|
||||
会被入口直接拒绝(409),因为平台循环内没有消费点,写了也等于没发。
|
||||
替代方案:改分工请取消协调员任务后重建一个新的;给某个进行中分工
|
||||
追加指令请直接发给该子任务;已终态的协调员不要走复活,重新建一个
|
||||
协调员任务,或对相关子任务复活后重跑。
|
||||
|
||||
## 跨任务产物传递(协调员)
|
||||
|
||||
@@ -1376,7 +1435,12 @@ full-auto-executor + installation id。质量门禁全部自动且阻塞合并
|
||||
|
||||
- 有进度的中断任务:断点续跑。
|
||||
- 从未开始的任务:全新重跑,不直接判死。
|
||||
- 单个客户端失败:只重开它,不重开全部。
|
||||
- 单个客户端失败:只重开它,不重开全部——这是流程口径;平台语义
|
||||
(`cloud/coordinator.py` 的 `evaluate_outcome`)是:任一非容忍分工
|
||||
未完成,协调员任务即失败;`on_failure=tolerate` 的分工失败不拖垮
|
||||
整体,重跑时其 `failed` 子任务不重排、`interrupted` 的仍重排;
|
||||
未知分工名按失败处理;`merged` 视同完成。`on_failure=tolerate`
|
||||
须在建协调员任务时声明,事后不可改。
|
||||
|
||||
## 你的交付物
|
||||
|
||||
@@ -1387,6 +1451,12 @@ G8 的交付物是 v1.1 交接清单终版与下一轮 G0-G6 输入冻结清单
|
||||
|
||||
---
|
||||
|
||||
## 独立审计与平台 audit_loop 的区分
|
||||
|
||||
- 平台的 `audit_loop` 是协调员任务的自动重排循环(推回包拆分到分工、
|
||||
子任务重排),不是独立审计线程;独立审计仍须另开线程(附录 11-auditor),
|
||||
审计 fail 自动推回并阻塞合并/发布,签字人与执行人、协调员批准人、
|
||||
QA 测试人全不同。两者阻塞关系均为“与”,不可互换。
|
||||
|
||||
## GitHub 保证(协调员)
|
||||
|
||||
@@ -1401,13 +1471,15 @@ G8 的交付物是 v1.1 交接清单终版与下一轮 G0-G6 输入冻结清单
|
||||
测试证据、已知问题、关联待办或 PRD 项;评审意见逐条处理,
|
||||
未解决的评审意见不能合并。
|
||||
- 合并门:GitHub Checks 全绿、目录归属检查通过、契约版本是冻结版、
|
||||
你评审通过、无未解决评审意见,五项缺一不可。
|
||||
你评审通过、无未解决评审意见,五项缺一不可。协调员批准与契约冻结
|
||||
版本是每次调合并门禁时提交的证据,平台不持久化;谎报由审计与
|
||||
PR/Checks 只读核对事后发现。
|
||||
CI 用只读方式查,例如 GET /tasks/{id}/pr/checks,它绝不重跑检查、
|
||||
不批准、不合并;无 PR 时它会 409,先补 PR。
|
||||
- 主分支保护:要求 PR、要求 CI 通过、禁止直接推,这是 GitHub 仓库设置,
|
||||
不是本平台代码能替你开的。没开保护时记为风险并去开通,不假装已保护。
|
||||
- 合并门禁:同正文,代码在 cloud/merge_gate.py 与
|
||||
cloud/merge_gate_hard.py,模式由 CLOUD_MERGE_GATE_MODE 控制,
|
||||
- 合并门禁:同正文,门禁实现在 cloud/merge_gate_hard.py(默认 hard),
|
||||
模式由 CLOUD_MERGE_GATE_MODE 控制,
|
||||
默认 hard:五项全查、缺一 409、状态不变、写 merge_gate_blocked
|
||||
审计,无证据不放行,unknown 不等于 protected,未保护即使带
|
||||
显式确认也不放行;record / enforce / off 须显式设置才生效,
|
||||
|
||||
@@ -14,6 +14,31 @@ full-auto-executor + installation id。质量门禁全部自动且阻塞合并
|
||||
建仓分支要诚实(组织仓可经 App token 建,用户个人仓需安装会话里先建空仓,
|
||||
记为 needs_user_once,不说成已自动建仓)。
|
||||
|
||||
## 平台协调员(以平台代码为准,skill 不得另说一套)
|
||||
|
||||
- 平台协调员任务(`coordinator_parts` 非空的任务)不跑模型回合:
|
||||
worker 按分工自动扇出并行子任务、等齐、合流成一份汇总报告写回
|
||||
父任务 `result_text`,汇总报告就是协调员任务的终局产物;合并决定
|
||||
仍由你(模型/人工)在评审环节给,但评审发生在子任务终态之后,
|
||||
报告之前没有“逐个放行”。
|
||||
- 分工一次建齐、并行认领;分工最多 20 个;子任务 prompt 即分工 prompt
|
||||
原文,平台不贴角色模板、不贴校验表、不贴精简包。
|
||||
- 有界委派(建任务时声明,事后不可改):单分工超时 `timeout_seconds`
|
||||
(1..86400 秒,超时只取消该分工、其余继续等)、失败容忍
|
||||
`on_failure`(`fail` 默认 / `tolerate`:容忍分工失败不拖垮整体,
|
||||
重跑时其 `failed` 子任务不重排、`interrupted` 的仍重排)、单项截断
|
||||
`result_chars`(默认 500、上限 2000)。全局超时 3600 秒:到时取消
|
||||
挂起子任务、父任务失败。平台不提供分工 token/费用预算、分工轮数
|
||||
上限、按分工隔离写范围——不要许诺这三类边界。
|
||||
- 配额预留:分工数超过 1 时,剩余配额须覆盖分工数 × $1(可经环境变量
|
||||
调),不足建任务即 402;子任务费用记父任务创建人账。
|
||||
- 取消与孤儿收口由平台自动做:取消/超时终局取消未终态子任务并标
|
||||
`interrupted`,常驻循环每轮扫尾父已终态、子仍未终态的孤儿;你不需、
|
||||
也不能手动“取消子任务”。
|
||||
- 模型与强度:分工不写 `model`/`effort` 时,继承协调员任务的
|
||||
`child_model`/`child_effort` 或自身模型;“协调员、产品、架构用
|
||||
较强模型”只是建议,不是平台字段。
|
||||
|
||||
## 你必须先做的事
|
||||
|
||||
1. 按 `software-engineering` 技能的 G0 问卷问清:目标用户、核心能力、
|
||||
@@ -32,12 +57,17 @@ full-auto-executor + installation id。质量门禁全部自动且阻塞合并
|
||||
完成定义。之后所有状态以这份待办为准,不以对话为准。
|
||||
你是待办文件的唯一写作者,子线程只在结尾报告里写 TODO-UPDATE,
|
||||
你核对证据后才勾选。不要虚构任何待办标记语法,不要依赖模型会自动写标记块。
|
||||
3. 每个子任务提示词必须包含:角色模板全文(来自 SKILL.md 附录)、
|
||||
本阶段输入的版本号、拥有目录、只读目录、分支名、完成定义、
|
||||
禁止事项、TODO-UPDATE 要求。缺一项不派生。
|
||||
3. 每个子任务提示词必须包含:本阶段输入的版本号、拥有目录、
|
||||
只读目录、分支名、完成定义、禁止事项、TODO-UPDATE 要求,
|
||||
以及角色模板内容——主聊人工派生时贴模板全文;平台协调员任务
|
||||
的分工 prompt 即子任务 prompt 全文,角色模板不由平台注入,
|
||||
如需模板内容,创建人须自己写进分工 prompt。缺一项不派生。
|
||||
派生前校验表(必填表,填好的表原样贴进提示词,缺一项就不发任务):
|
||||
客户端名、拥有目录、分支名、契约版本、PRD 版本、设计版本、
|
||||
完成定义、TODO-UPDATE 要求,共 8 项。
|
||||
完成定义、TODO-UPDATE 要求,共 8 项。校验表里的拥有目录 /
|
||||
分支名 / 版本 / TODO-UPDATE 是写入分工 prompt 的约定内容,
|
||||
平台只校验分工字段(`coordinator.validate_parts`),不校验、
|
||||
不解析这些项。
|
||||
子任务精简包(裁剪规则写死):只给本端相关的契约章节、设计页、
|
||||
PRD 故事和冻结决策摘要(不超过 10 条),绝不把 PRD、契约、设计全文
|
||||
复制给每个线程;提示词里必须列出精简包所含章节清单,只写“已精简”不算数;
|
||||
@@ -49,7 +79,8 @@ full-auto-executor + installation id。质量门禁全部自动且阻塞合并
|
||||
- G2:架构师和 UI/UX 设计 2 个线程并行,中间加一次联合评审:
|
||||
架构的状态机和设计的审批界面对得上才能冻结;收敛后你判定契约是否冻结,
|
||||
冻结物必须齐:API 契约、数据模型、agent 协议、任务状态机、错误 taxonomy、
|
||||
技术栈表、每端能力矩阵。
|
||||
技术栈表、每端能力矩阵。(7 项齐全由你人工核对;平台只校验契约版本号
|
||||
与冻结标志,不校验冻结物清单、不发回归任务。)
|
||||
- G3:共享核心 1 个线程(含 mock 服务和契约测试);后端线程如有,
|
||||
在 G3 阶段就参与契约测试,不要等到 G4 才对接。
|
||||
- G3.5:可行性 spike。核心链路真跑通(agent 工具就是任务能创建、能跑出进度、
|
||||
@@ -78,6 +109,9 @@ full-auto-executor + installation id。质量门禁全部自动且阻塞合并
|
||||
(问题清单、热修复记录、回滚触发是否命中)+ v1.1 交接清单齐全,
|
||||
两项缺一不进 G8;G8 的完成不是下一版已发布,是下一轮 G0-G6 的输入
|
||||
已冻结齐全。
|
||||
(阶段顺序是流程编排;平台协调员任务建出后各分工并行执行,不按 G1–G8
|
||||
分阶段放行——需要分阶段时,分多次建协调员任务,上一阶段的协调员终态
|
||||
后再建下一阶段。)
|
||||
|
||||
## 你的合并规则(评审与执行拆分)
|
||||
|
||||
@@ -108,6 +142,11 @@ full-auto-executor + installation id。质量门禁全部自动且阻塞合并
|
||||
- 决策记录落盘 `docs/decisions.md`,由你维护,只追加不改写历史;
|
||||
一条决策写清日期、议题、决定、理由、影响端;推翻旧决策追加新条
|
||||
写明“取代 D-n”,绝不改旧条原文、不删旧条。
|
||||
- 平台协调员任务暂不支持后续指令:给进行中的协调员任务发后续消息
|
||||
会被入口直接拒绝(409),因为平台循环内没有消费点,写了也等于没发。
|
||||
替代方案:改分工请取消协调员任务后重建一个新的;给某个进行中分工
|
||||
追加指令请直接发给该子任务;已终态的协调员不要走复活,重新建一个
|
||||
协调员任务,或对相关子任务复活后重跑。
|
||||
|
||||
## 跨任务产物传递(协调员)
|
||||
|
||||
@@ -127,7 +166,12 @@ full-auto-executor + installation id。质量门禁全部自动且阻塞合并
|
||||
|
||||
- 有进度的中断任务:断点续跑。
|
||||
- 从未开始的任务:全新重跑,不直接判死。
|
||||
- 单个客户端失败:只重开它,不重开全部。
|
||||
- 单个客户端失败:只重开它,不重开全部——这是流程口径;平台语义
|
||||
(`cloud/coordinator.py` 的 `evaluate_outcome`)是:任一非容忍分工
|
||||
未完成,协调员任务即失败;`on_failure=tolerate` 的分工失败不拖垮
|
||||
整体,重跑时其 `failed` 子任务不重排、`interrupted` 的仍重排;
|
||||
未知分工名按失败处理;`merged` 视同完成。`on_failure=tolerate`
|
||||
须在建协调员任务时声明,事后不可改。
|
||||
|
||||
## 你的交付物
|
||||
|
||||
@@ -138,6 +182,12 @@ G8 的交付物是 v1.1 交接清单终版与下一轮 G0-G6 输入冻结清单
|
||||
|
||||
---
|
||||
|
||||
## 独立审计与平台 audit_loop 的区分
|
||||
|
||||
- 平台的 `audit_loop` 是协调员任务的自动重排循环(推回包拆分到分工、
|
||||
子任务重排),不是独立审计线程;独立审计仍须另开线程(附录 11-auditor),
|
||||
审计 fail 自动推回并阻塞合并/发布,签字人与执行人、协调员批准人、
|
||||
QA 测试人全不同。两者阻塞关系均为“与”,不可互换。
|
||||
|
||||
## GitHub 保证(协调员)
|
||||
|
||||
@@ -152,13 +202,15 @@ G8 的交付物是 v1.1 交接清单终版与下一轮 G0-G6 输入冻结清单
|
||||
测试证据、已知问题、关联待办或 PRD 项;评审意见逐条处理,
|
||||
未解决的评审意见不能合并。
|
||||
- 合并门:GitHub Checks 全绿、目录归属检查通过、契约版本是冻结版、
|
||||
你评审通过、无未解决评审意见,五项缺一不可。
|
||||
你评审通过、无未解决评审意见,五项缺一不可。协调员批准与契约冻结
|
||||
版本是每次调合并门禁时提交的证据,平台不持久化;谎报由审计与
|
||||
PR/Checks 只读核对事后发现。
|
||||
CI 用只读方式查,例如 GET /tasks/{id}/pr/checks,它绝不重跑检查、
|
||||
不批准、不合并;无 PR 时它会 409,先补 PR。
|
||||
- 主分支保护:要求 PR、要求 CI 通过、禁止直接推,这是 GitHub 仓库设置,
|
||||
不是本平台代码能替你开的。没开保护时记为风险并去开通,不假装已保护。
|
||||
- 合并门禁:同正文,代码在 cloud/merge_gate.py 与
|
||||
cloud/merge_gate_hard.py,模式由 CLOUD_MERGE_GATE_MODE 控制,
|
||||
- 合并门禁:同正文,门禁实现在 cloud/merge_gate_hard.py(默认 hard),
|
||||
模式由 CLOUD_MERGE_GATE_MODE 控制,
|
||||
默认 hard:五项全查、缺一 409、状态不变、写 merge_gate_blocked
|
||||
审计,无证据不放行,unknown 不等于 protected,未保护即使带
|
||||
显式确认也不放行;record / enforce / off 须显式设置才生效,
|
||||
|
||||
@@ -238,6 +238,25 @@ class WorkerCoordinatorTestCase(unittest.TestCase):
|
||||
self.assertEqual(backend["effort"], "high")
|
||||
self.assertEqual(backend["status"], "queued")
|
||||
|
||||
def test_sync_children_never_carry_coordinator_parts(self):
|
||||
# 纵深防御(Muse 角色对齐 P1):_coord_sync_children 建出
|
||||
# 的子任务行 coordinator_parts 恒为 NULL,子任务永为普通任务。
|
||||
_insert_task(self.db_path, "coord", coordinator_parts=self._parts())
|
||||
worker = self._worker()
|
||||
task = worker.get_task("coord")
|
||||
worker._coord_sync_children(task, task["coordinator_parts"])
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
try:
|
||||
rows = conn.execute(
|
||||
"SELECT id, coordinator_parts FROM tasks"
|
||||
" WHERE parent_task_id = ?",
|
||||
("coord",)).fetchall()
|
||||
finally:
|
||||
conn.close()
|
||||
self.assertEqual(len(rows), 2)
|
||||
for task_id, parts in rows:
|
||||
self.assertIsNone(parts, task_id)
|
||||
|
||||
def test_sync_requeues_failed_keeps_completed(self):
|
||||
_insert_task(self.db_path, "coord", coordinator_parts=self._parts())
|
||||
_insert_task(self.db_path, "c-old", status="failed",
|
||||
@@ -469,6 +488,103 @@ class ServerCoordinatorTestCase(unittest.TestCase):
|
||||
self.assertEqual(status, 200)
|
||||
self.assertEqual(len(data["tasks"]), 3)
|
||||
|
||||
def _create_task(self, extra=None):
|
||||
body = {"project_id": self.project["id"], "prompt": "做点事"}
|
||||
if extra:
|
||||
body.update(extra)
|
||||
return self._req("POST", "/tasks", body, self.owner_key)
|
||||
|
||||
def test_create_child_coordinator_under_plain_parent_ok(self):
|
||||
status, parent = self._create_task()
|
||||
self.assertEqual(status, 201)
|
||||
status, child = self._create_coord(
|
||||
extra={"parent_task_id": parent["id"]})
|
||||
self.assertEqual(status, 201)
|
||||
self.assertEqual(child["parent_task_id"], parent["id"])
|
||||
self.assertEqual(len(child["coordinator_parts"]), 2)
|
||||
|
||||
def test_create_coordinator_under_coordinator_rejected(self):
|
||||
status, coord = self._create_coord()
|
||||
self.assertEqual(status, 201)
|
||||
status, body = self._create_coord(
|
||||
extra={"parent_task_id": coord["id"]})
|
||||
self.assertEqual(status, 400)
|
||||
self.assertIn("coordinator task cannot be created under a"
|
||||
" coordinator task", body["error"])
|
||||
status, data = self._req(
|
||||
"GET", "/tasks?project_id=" + self.project["id"],
|
||||
key=self.owner_key)
|
||||
self.assertEqual(status, 200)
|
||||
self.assertEqual(len(data["tasks"]), 1)
|
||||
|
||||
def test_create_parent_task_checks(self):
|
||||
status, _body = self._create_coord(
|
||||
extra={"parent_task_id": "no-such-task"})
|
||||
self.assertEqual(status, 404)
|
||||
status, other = self._req(
|
||||
"POST", "/projects", {"name": "other"}, self.owner_key)
|
||||
self.assertEqual(status, 201)
|
||||
status, parent = self._create_task(
|
||||
extra={"project_id": other["id"]})
|
||||
self.assertEqual(status, 201)
|
||||
status, body = self._create_coord(
|
||||
extra={"parent_task_id": parent["id"]})
|
||||
self.assertEqual(status, 400)
|
||||
status, body = self._create_coord(
|
||||
extra={"parent_task_id": 123})
|
||||
self.assertEqual(status, 400)
|
||||
|
||||
def test_create_lineage_without_coordinator_ok(self):
|
||||
status, coord = self._create_coord()
|
||||
self.assertEqual(status, 201)
|
||||
status, child = self._create_task(
|
||||
extra={"parent_task_id": coord["id"]})
|
||||
self.assertEqual(status, 201)
|
||||
self.assertEqual(child["parent_task_id"], coord["id"])
|
||||
|
||||
def test_create_idempotent_replay_keeps_single_task(self):
|
||||
status, parent = self._create_task()
|
||||
self.assertEqual(status, 201)
|
||||
extra = {"idempotency_key": "idem-parent",
|
||||
"parent_task_id": parent["id"]}
|
||||
status, first = self._create_task(extra=extra)
|
||||
self.assertEqual(status, 201)
|
||||
status, second = self._create_task(extra=extra)
|
||||
self.assertEqual(status, 200)
|
||||
self.assertEqual(second["id"], first["id"])
|
||||
status, data = self._req(
|
||||
"GET", "/tasks?project_id=" + self.project["id"],
|
||||
key=self.owner_key)
|
||||
self.assertEqual(len(data["tasks"]), 2)
|
||||
|
||||
def test_followup_to_coordinator_rejected(self):
|
||||
status, coord = self._create_coord()
|
||||
self.assertEqual(status, 201)
|
||||
status, body = self._req(
|
||||
"POST", "/tasks/" + coord["id"] + "/followups",
|
||||
{"body": "改一下分工", "mode": "queued"}, self.owner_key)
|
||||
self.assertEqual(status, 409)
|
||||
self.assertIn("coordinator tasks do not accept follow-up",
|
||||
body["error"])
|
||||
status, body = self._req(
|
||||
"POST", "/tasks/" + coord["id"] + "/thread/message",
|
||||
{"body": "改一下分工", "mode": "queued"}, self.owner_key)
|
||||
self.assertEqual(status, 409)
|
||||
self.assertIn("coordinator tasks do not accept follow-up",
|
||||
body["error"])
|
||||
|
||||
def test_followup_to_plain_task_ok(self):
|
||||
status, task = self._create_task()
|
||||
self.assertEqual(status, 201)
|
||||
status, _body = self._req(
|
||||
"POST", "/tasks/" + task["id"] + "/followups",
|
||||
{"body": "继续", "mode": "queued"}, self.owner_key)
|
||||
self.assertEqual(status, 201)
|
||||
status, body = self._req(
|
||||
"POST", "/tasks/" + task["id"] + "/thread/message",
|
||||
{"body": "继续", "mode": "queued"}, self.owner_key)
|
||||
self.assertEqual(status, 201)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -185,7 +185,8 @@ class LongtaskAutoResumeTestCase(unittest.TestCase):
|
||||
captured = {}
|
||||
|
||||
def fake_execute(task, kind="dispatch", deadline_seconds=None,
|
||||
seed_host_dir=None, quota_check=None):
|
||||
seed_host_dir=None, no_worktree=False,
|
||||
quota_check=None):
|
||||
captured["deadline"] = deadline_seconds
|
||||
return {"status": "completed", "stdout": "", "stderr": ""}
|
||||
|
||||
|
||||
@@ -240,6 +240,48 @@ class TestMigrateNewTables(unittest.TestCase):
|
||||
"result_text", "error_text", "created_at"):
|
||||
self.assertIn(col, attempts_inserts[0])
|
||||
|
||||
def test_kind_column_backfilled_when_missing(self):
|
||||
# P2:PG 侧 tasks 尚无 kind 列时,迁移必须先 ALTER 补列
|
||||
# 再直插,且插入列清单里含 kind;kind 已存在时不重复
|
||||
# ALTER(与 agent_* 列同款 backfill 惯例)。
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
src = os.path.join(tmp, "src.db")
|
||||
_make_sqlite(src)
|
||||
fake = FakePgConn(pragma_columns={
|
||||
"projects": {"repo_url"},
|
||||
"tasks": {"id", "project_id"},
|
||||
"task_followups": {"id", "task_id", "project_id",
|
||||
"body", "mode", "status",
|
||||
"created_by", "sender_task_id",
|
||||
"created_at", "updated_at"},
|
||||
})
|
||||
result = migrate_sqlite_to_postgres(src, pg_conn=fake)
|
||||
self.assertEqual(result.get("tasks"), 1, "tasks")
|
||||
self.assertTrue(
|
||||
any("tasks" in a and "kind" in a for a in fake.alters),
|
||||
"未发出 tasks.kind 补列 ALTER:%s" % fake.alters)
|
||||
# 注:插入列清单取自源 SQLite 表的列,夹具源表无 kind
|
||||
# 列故清单里无 kind;生产源库(init_db 建表)有 kind,
|
||||
# 迁移时按列原样搬运(与 agent_* 列同款)。
|
||||
|
||||
def test_kind_column_present_skips_alter(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
src = os.path.join(tmp, "src.db")
|
||||
_make_sqlite(src)
|
||||
fake = FakePgConn(pragma_columns={
|
||||
"projects": {"repo_url"},
|
||||
"tasks": {"id", "project_id", "kind"},
|
||||
"task_followups": {"id", "task_id", "project_id",
|
||||
"body", "mode", "status",
|
||||
"created_by", "sender_task_id",
|
||||
"created_at", "updated_at"},
|
||||
})
|
||||
result = migrate_sqlite_to_postgres(src, pg_conn=fake)
|
||||
self.assertEqual(result.get("tasks"), 1, "tasks")
|
||||
self.assertFalse(
|
||||
any("tasks" in a and "kind" in a for a in fake.alters),
|
||||
"kind 已存在却仍发补列 ALTER:%s" % fake.alters)
|
||||
|
||||
def test_sender_column_present_skips_alter(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
src = os.path.join(tmp, "src.db")
|
||||
|
||||
@@ -0,0 +1,730 @@
|
||||
"""旁聊任务类型(Muse 角色对齐 P2)单测:创建/列表/执行分流/回写/恢复。
|
||||
|
||||
不跑真实模型回合、不联网:服务端侧本地临时端口 + 临时 SQLite;
|
||||
worker/sandbox 侧注入假 runner 录命令;host.py dispatch 用假
|
||||
server + 临时 HERE。
|
||||
"""
|
||||
|
||||
import http.client
|
||||
import json
|
||||
import os
|
||||
import sqlite3
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest import mock
|
||||
|
||||
try:
|
||||
import host as host_mod
|
||||
from cloud.auth import AuthStore
|
||||
from cloud.marker_tools import (
|
||||
count_direct_spawn_children,
|
||||
extract_task_creates,
|
||||
parent_spawn_generation,
|
||||
)
|
||||
from cloud.sandbox import DockerExecutor, LocalExecutor
|
||||
from cloud.server import create_server, init_db, migrate_tasks_table
|
||||
from cloud.worker import Worker
|
||||
except ImportError: # discover -s cloud/tests 时把项目根目录补进 sys.path
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
||||
import host as host_mod
|
||||
from cloud.auth import AuthStore
|
||||
from cloud.marker_tools import (
|
||||
count_direct_spawn_children,
|
||||
extract_task_creates,
|
||||
parent_spawn_generation,
|
||||
)
|
||||
from cloud.sandbox import DockerExecutor, LocalExecutor
|
||||
from cloud.server import create_server, init_db, migrate_tasks_table
|
||||
from cloud.worker import Worker
|
||||
|
||||
|
||||
def _recording(calls):
|
||||
def fake(cmd):
|
||||
calls.append(list(cmd))
|
||||
return SimpleNamespace(returncode=0, stdout="ok", stderr="")
|
||||
return fake
|
||||
|
||||
|
||||
def _insert_task(db_path, task_id, **cols):
|
||||
base = {"project_id": "p1", "prompt": "do work", "status": "queued",
|
||||
"created_by": "tester"}
|
||||
base.update(cols)
|
||||
keys = ["id"] + list(base.keys())
|
||||
vals = [task_id] + list(base.values())
|
||||
conn = sqlite3.connect(db_path)
|
||||
try:
|
||||
conn.execute(
|
||||
"INSERT INTO tasks(%s) VALUES(%s)"
|
||||
% (", ".join(keys), ", ".join("?" * len(keys))), vals)
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def _task_row(db_path, task_id, columns="id, status, kind, executor,"
|
||||
" parent_task_id, coordinator_part"):
|
||||
conn = sqlite3.connect(db_path)
|
||||
try:
|
||||
row = conn.execute(
|
||||
"SELECT %s FROM tasks WHERE id = ?" % columns,
|
||||
(task_id,)).fetchone()
|
||||
finally:
|
||||
conn.close()
|
||||
return row
|
||||
|
||||
|
||||
def _followups(db_path, task_id):
|
||||
conn = sqlite3.connect(db_path)
|
||||
try:
|
||||
rows = conn.execute(
|
||||
"SELECT id, body, mode, status, sender_task_id"
|
||||
" FROM task_followups WHERE task_id = ?"
|
||||
" ORDER BY created_at, id", (task_id,)).fetchall()
|
||||
finally:
|
||||
conn.close()
|
||||
return [{"id": r[0], "body": r[1], "mode": r[2], "status": r[3],
|
||||
"sender_task_id": r[4]} for r in rows]
|
||||
|
||||
|
||||
def _set_status(db_path, task_id, status):
|
||||
conn = sqlite3.connect(db_path)
|
||||
try:
|
||||
conn.execute("UPDATE tasks SET status = ? WHERE id = ?",
|
||||
(status, task_id))
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
class _ServerBase(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.db_path = os.path.join(self.tmp.name, "control.db")
|
||||
self.auth = AuthStore(self.db_path)
|
||||
self.owner_id, self.owner_key = self.auth.create_user("owner")
|
||||
self.server = create_server(self.db_path, "127.0.0.1", 0)
|
||||
self.port = self.server.server_address[1]
|
||||
self.thread = threading.Thread(
|
||||
target=self.server.serve_forever, daemon=True)
|
||||
self.thread.start()
|
||||
status, self.project = self._req(
|
||||
"POST", "/projects", {"name": "demo"}, self.owner_key)
|
||||
self.assertEqual(status, 201)
|
||||
|
||||
def tearDown(self):
|
||||
self.server.shutdown()
|
||||
self.server.server_close()
|
||||
self.thread.join(timeout=5)
|
||||
try:
|
||||
self.auth.conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
self.tmp.cleanup()
|
||||
|
||||
def _req(self, method, path, body=None, key=None):
|
||||
conn = http.client.HTTPConnection(
|
||||
"127.0.0.1", self.port, timeout=10)
|
||||
hdrs = {}
|
||||
payload = None
|
||||
if body is not None:
|
||||
payload = json.dumps(body).encode("utf-8")
|
||||
hdrs["Content-Type"] = "application/json"
|
||||
if key is not None:
|
||||
hdrs["X-API-Key"] = key
|
||||
try:
|
||||
conn.request(method, path, body=payload, headers=hdrs)
|
||||
resp = conn.getresponse()
|
||||
data = resp.read()
|
||||
finally:
|
||||
conn.close()
|
||||
try:
|
||||
parsed = json.loads(data.decode("utf-8")) if data else None
|
||||
except ValueError:
|
||||
parsed = None
|
||||
return resp.status, parsed
|
||||
|
||||
def _create_task(self, extra=None):
|
||||
body = {"project_id": self.project["id"], "prompt": "做点事"}
|
||||
if extra:
|
||||
body.update(extra)
|
||||
return self._req("POST", "/tasks", body, self.owner_key)
|
||||
|
||||
def _create_side(self, parent_id, extra=None):
|
||||
body = {"project_id": self.project["id"], "prompt": "旁聊一下",
|
||||
"kind": "side", "parent_task_id": parent_id}
|
||||
if extra:
|
||||
body.update(extra)
|
||||
return self._req("POST", "/tasks", body, self.owner_key)
|
||||
|
||||
|
||||
class ServerSideThreadTestCase(_ServerBase):
|
||||
def test_create_side_ok_defaults_local(self):
|
||||
status, parent = self._create_task()
|
||||
self.assertEqual(status, 201)
|
||||
status, side = self._create_side(parent["id"])
|
||||
self.assertEqual(status, 201, side)
|
||||
self.assertEqual(side["kind"], "side")
|
||||
self.assertEqual(side["parent_task_id"], parent["id"])
|
||||
self.assertEqual(side["executor"], "local")
|
||||
status, fetched = self._req(
|
||||
"GET", "/tasks/" + side["id"], key=self.owner_key)
|
||||
self.assertEqual(status, 200)
|
||||
self.assertEqual(fetched["kind"], "side")
|
||||
|
||||
def test_create_side_validation(self):
|
||||
status, parent = self._create_task()
|
||||
self.assertEqual(status, 201)
|
||||
# 缺 parent_task_id
|
||||
status, body = self._req(
|
||||
"POST", "/tasks",
|
||||
{"project_id": self.project["id"], "prompt": "x",
|
||||
"kind": "side"}, self.owner_key)
|
||||
self.assertEqual(status, 400)
|
||||
# 父不存在
|
||||
status, _body = self._create_side("no-such-parent")
|
||||
self.assertEqual(status, 404)
|
||||
# kind 非法值
|
||||
status, _body = self._create_task(
|
||||
{"kind": "weird", "parent_task_id": parent["id"]})
|
||||
self.assertEqual(status, 400)
|
||||
# 带 coordinator_parts
|
||||
status, _body = self._create_side(parent["id"], {
|
||||
"coordinator_parts": [{"name": "a", "prompt": "x"}]})
|
||||
self.assertEqual(status, 400)
|
||||
# 带 upload_id / environment_id
|
||||
status, _body = self._create_side(
|
||||
parent["id"], {"upload_id": "up-nonexistent"})
|
||||
self.assertEqual(status, 400)
|
||||
status, _body = self._create_side(
|
||||
parent["id"], {"environment_id": "env-nonexistent"})
|
||||
self.assertEqual(status, 400)
|
||||
|
||||
def test_create_side_cross_project_rejected(self):
|
||||
status, parent = self._create_task()
|
||||
self.assertEqual(status, 201)
|
||||
status, other_project = self._req(
|
||||
"POST", "/projects", {"name": "other"}, self.owner_key)
|
||||
self.assertEqual(status, 201)
|
||||
status, other_task = self._req(
|
||||
"POST", "/tasks",
|
||||
{"project_id": other_project["id"], "prompt": "别处的事"},
|
||||
self.owner_key)
|
||||
self.assertEqual(status, 201)
|
||||
status, _body = self._create_side(other_task["id"])
|
||||
self.assertEqual(status, 400)
|
||||
|
||||
def test_create_side_under_coordinator_rejected(self):
|
||||
status, coord = self._req(
|
||||
"POST", "/tasks",
|
||||
{"project_id": self.project["id"], "prompt": "协调干活",
|
||||
"coordinator_parts": [{"name": "a", "prompt": "做 a"}]},
|
||||
self.owner_key)
|
||||
self.assertEqual(status, 201)
|
||||
status, body = self._create_side(coord["id"])
|
||||
self.assertEqual(status, 400)
|
||||
self.assertIn("coordinator", body["error"])
|
||||
|
||||
def test_create_side_explicit_docker_kept(self):
|
||||
status, parent = self._create_task()
|
||||
self.assertEqual(status, 201)
|
||||
status, side = self._create_side(
|
||||
parent["id"], {"executor": "docker"})
|
||||
self.assertEqual(status, 201, side)
|
||||
self.assertEqual(side["executor"], "docker")
|
||||
self.assertEqual(side["kind"], "side")
|
||||
|
||||
def test_create_main_defaults(self):
|
||||
status, task = self._create_task()
|
||||
self.assertEqual(status, 201)
|
||||
self.assertEqual(task["kind"], "main")
|
||||
status, task2 = self._create_task({"kind": "main"})
|
||||
self.assertEqual(status, 201)
|
||||
self.assertEqual(task2["kind"], "main")
|
||||
|
||||
def test_list_kind_filter(self):
|
||||
status, parent = self._create_task()
|
||||
self.assertEqual(status, 201)
|
||||
_s, side1 = self._create_side(parent["id"])
|
||||
_s, side2 = self._create_side(parent["id"])
|
||||
_s, main_child = self._create_task(
|
||||
{"parent_task_id": parent["id"]})
|
||||
self.assertEqual(main_child["kind"], "main")
|
||||
pid = self.project["id"]
|
||||
status, data = self._req(
|
||||
"GET", "/tasks?project_id=" + pid +
|
||||
"&parent_task_id=" + parent["id"] + "&kind=side",
|
||||
key=self.owner_key)
|
||||
self.assertEqual(status, 200)
|
||||
self.assertEqual(
|
||||
sorted(t["id"] for t in data["tasks"]),
|
||||
sorted([side1["id"], side2["id"]]))
|
||||
status, data = self._req(
|
||||
"GET", "/tasks?project_id=" + pid +
|
||||
"&parent_task_id=" + parent["id"], key=self.owner_key)
|
||||
self.assertEqual(status, 200)
|
||||
self.assertEqual(len(data["tasks"]), 3)
|
||||
status, data = self._req(
|
||||
"GET", "/tasks?project_id=" + pid + "&kind=side",
|
||||
key=self.owner_key)
|
||||
self.assertEqual(status, 200)
|
||||
self.assertEqual(
|
||||
sorted(t["id"] for t in data["tasks"]),
|
||||
sorted([side1["id"], side2["id"]]))
|
||||
status, data = self._req(
|
||||
"GET", "/tasks?project_id=" + pid + "&kind=main",
|
||||
key=self.owner_key)
|
||||
self.assertEqual(status, 200)
|
||||
self.assertEqual(
|
||||
sorted(t["id"] for t in data["tasks"]),
|
||||
sorted([parent["id"], main_child["id"]]))
|
||||
status, _body = self._req(
|
||||
"GET", "/tasks?project_id=" + pid + "&kind=bogus",
|
||||
key=self.owner_key)
|
||||
self.assertEqual(status, 400)
|
||||
|
||||
def test_side_files_tree_worktree_404(self):
|
||||
status, parent = self._create_task()
|
||||
self.assertEqual(status, 201)
|
||||
status, side = self._create_side(parent["id"])
|
||||
self.assertEqual(status, 201)
|
||||
for suffix in ("/files", "/tree", "/worktree"):
|
||||
status, body = self._req(
|
||||
"GET", "/tasks/" + side["id"] + suffix,
|
||||
key=self.owner_key)
|
||||
self.assertEqual(status, 404, suffix)
|
||||
self.assertIn("no local worktree", body["error"])
|
||||
|
||||
def test_side_fork_merge_pr_409(self):
|
||||
status, parent = self._create_task()
|
||||
self.assertEqual(status, 201)
|
||||
status, side = self._create_side(parent["id"])
|
||||
self.assertEqual(status, 201)
|
||||
status, body = self._req(
|
||||
"POST", "/tasks/" + side["id"] + "/fork",
|
||||
{"message": "分一个"}, self.owner_key)
|
||||
self.assertEqual(status, 409)
|
||||
self.assertIn("side thread", body["error"])
|
||||
status, body = self._req(
|
||||
"POST", "/tasks/" + side["id"] + "/merge", {},
|
||||
self.owner_key)
|
||||
self.assertEqual(status, 409)
|
||||
self.assertIn("side thread", body["error"])
|
||||
status, body = self._req(
|
||||
"POST", "/tasks/" + side["id"] + "/pr", {},
|
||||
self.owner_key)
|
||||
self.assertEqual(status, 409)
|
||||
self.assertIn("side thread", body["error"])
|
||||
|
||||
def test_side_continue_and_followups(self):
|
||||
status, parent = self._create_task()
|
||||
self.assertEqual(status, 201)
|
||||
status, side = self._create_side(parent["id"])
|
||||
self.assertEqual(status, 201)
|
||||
_set_status(self.db_path, side["id"], "completed")
|
||||
status, task = self._req(
|
||||
"POST", "/tasks/" + side["id"] + "/continue",
|
||||
{"message": "再聊聊"}, self.owner_key)
|
||||
self.assertEqual(status, 200, task)
|
||||
self.assertEqual(task["status"], "queued")
|
||||
self.assertEqual(task["resume_message"], "再聊聊")
|
||||
status, _body = self._req(
|
||||
"POST", "/tasks/" + side["id"] + "/followups",
|
||||
{"body": "继续", "mode": "queued"}, self.owner_key)
|
||||
self.assertEqual(status, 201)
|
||||
|
||||
def test_side_writeback_visible_on_terminal_parent(self):
|
||||
# 父终态时回写只作记录:followups 可见,父不复活。
|
||||
status, parent = self._create_task()
|
||||
self.assertEqual(status, 201)
|
||||
status, side = self._create_side(parent["id"])
|
||||
self.assertEqual(status, 201)
|
||||
_set_status(self.db_path, parent["id"], "completed")
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
try:
|
||||
conn.execute(
|
||||
"INSERT INTO task_followups(id, task_id, project_id,"
|
||||
" body, mode, status, created_by, sender_task_id,"
|
||||
" created_at, updated_at)"
|
||||
" VALUES(?, ?, ?, ?, 'queued', 'pending', ?, ?, ?, ?)",
|
||||
("fu-1", parent["id"], self.project["id"],
|
||||
"[旁聊结论] 查到了", "tester", side["id"],
|
||||
time.time(), time.time()))
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
status, data = self._req(
|
||||
"GET", "/tasks/" + parent["id"] + "/followups",
|
||||
key=self.owner_key)
|
||||
self.assertEqual(status, 200)
|
||||
self.assertEqual(len(data["followups"]), 1)
|
||||
self.assertEqual(data["followups"][0]["sender_task_id"],
|
||||
side["id"])
|
||||
status, fetched = self._req(
|
||||
"GET", "/tasks/" + parent["id"], key=self.owner_key)
|
||||
self.assertEqual(status, 200)
|
||||
self.assertEqual(fetched["status"], "completed")
|
||||
|
||||
|
||||
class _WorkerBase(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.db_path = os.path.join(self.tmp.name, "control.db")
|
||||
init_db(self.db_path)
|
||||
self.env = {"XCM_API_KEY": "test-placeholder"}
|
||||
|
||||
def tearDown(self):
|
||||
self.tmp.cleanup()
|
||||
|
||||
def _worker(self, calls):
|
||||
return Worker(self.db_path, "/fake/host.py",
|
||||
runner=_recording(calls), env=self.env)
|
||||
|
||||
|
||||
class WorkerSideThreadTestCase(_WorkerBase):
|
||||
def test_run_one_side_no_repo_no_worktree_and_writeback(self):
|
||||
_insert_task(self.db_path, "parent1", executor="local")
|
||||
_insert_task(self.db_path, "side1", kind="side",
|
||||
parent_task_id="parent1", executor="local")
|
||||
calls = []
|
||||
worker = self._worker(calls)
|
||||
result = worker.run_one(worker.get_task("side1"))
|
||||
self.assertEqual(result["status"], "completed")
|
||||
dispatch_cmds = [c for c in calls if "dispatch" in c]
|
||||
self.assertEqual(len(dispatch_cmds), 1)
|
||||
cmd = dispatch_cmds[0]
|
||||
self.assertIn("--no-worktree", cmd)
|
||||
self.assertNotIn("--repo", cmd)
|
||||
self.assertNotIn("--seed-dir", cmd)
|
||||
row = _task_row(self.db_path, "side1")
|
||||
self.assertEqual(row[1], "completed")
|
||||
fus = _followups(self.db_path, "parent1")
|
||||
self.assertEqual(len(fus), 1)
|
||||
self.assertEqual(fus[0]["sender_task_id"], "side1")
|
||||
self.assertEqual(fus[0]["mode"], "queued")
|
||||
self.assertEqual(fus[0]["status"], "pending")
|
||||
self.assertTrue(fus[0]["body"].startswith("[旁聊结论]"))
|
||||
|
||||
def test_run_attempt_side_with_repo_forces_plan(self):
|
||||
# worker 层直接调 _run_attempt:有 repo 的旁聊强制 plan=1,
|
||||
# argv 同时含 --no-worktree 与 --sandbox read-only。
|
||||
calls = []
|
||||
worker = self._worker(calls)
|
||||
task = {"id": "side-repo", "prompt": "聊聊 repo",
|
||||
"project_id": "p1", "created_by": "tester",
|
||||
"kind": "side", "executor": "local",
|
||||
"repo": "/tmp/fake-repo",
|
||||
"_gateway_provider": None, "_gateway_env": {}}
|
||||
result, error = worker._run_attempt(task)
|
||||
self.assertIsNone(error)
|
||||
self.assertEqual(result["status"], "completed")
|
||||
dispatch_cmds = [c for c in calls if "dispatch" in c]
|
||||
self.assertEqual(len(dispatch_cmds), 1)
|
||||
cmd = dispatch_cmds[0]
|
||||
self.assertIn("--no-worktree", cmd)
|
||||
self.assertIn("--sandbox", cmd)
|
||||
self.assertEqual(cmd[cmd.index("--sandbox") + 1], "read-only")
|
||||
self.assertIn("--repo", cmd)
|
||||
self.assertEqual(cmd[cmd.index("--repo") + 1], "/tmp/fake-repo")
|
||||
|
||||
def test_run_attempt_main_task_untouched(self):
|
||||
calls = []
|
||||
worker = self._worker(calls)
|
||||
task = {"id": "main1", "prompt": "做事",
|
||||
"project_id": "p1", "created_by": "tester",
|
||||
"executor": "local",
|
||||
"_gateway_provider": None, "_gateway_env": {}}
|
||||
result, error = worker._run_attempt(task)
|
||||
self.assertIsNone(error)
|
||||
self.assertEqual(result["status"], "completed")
|
||||
dispatch_cmds = [c for c in calls if "dispatch" in c]
|
||||
self.assertEqual(len(dispatch_cmds), 1)
|
||||
self.assertNotIn("--no-worktree", dispatch_cmds[0])
|
||||
self.assertNotIn("--sandbox", dispatch_cmds[0])
|
||||
|
||||
def test_writeback_direct_shapes(self):
|
||||
_insert_task(self.db_path, "parent1", executor="local")
|
||||
worker = Worker(self.db_path, "/fake/host.py", env=self.env)
|
||||
task = {"id": "side1", "project_id": "p1",
|
||||
"created_by": "tester", "kind": "side",
|
||||
"parent_task_id": "parent1"}
|
||||
self.assertTrue(worker._writeback_side_thread(
|
||||
task, "completed", "结论文本"))
|
||||
self.assertTrue(worker._writeback_side_thread(
|
||||
task, "failed", "出错了"))
|
||||
self.assertTrue(worker._writeback_side_thread(
|
||||
task, "interrupted", ""))
|
||||
fus = _followups(self.db_path, "parent1")
|
||||
self.assertEqual(len(fus), 3)
|
||||
self.assertTrue(fus[0]["body"].startswith("[旁聊结论]"))
|
||||
self.assertTrue(fus[1]["body"].startswith("[旁聊失败]"))
|
||||
self.assertTrue(fus[2]["body"].startswith("[旁聊中断]"))
|
||||
# 超长正文截断到 2000 字 + 截断标记。
|
||||
self.assertTrue(worker._writeback_side_thread(
|
||||
task, "completed", "x" * 3000))
|
||||
fus = _followups(self.db_path, "parent1")
|
||||
self.assertLessEqual(len(fus[3]["body"]), 2010)
|
||||
self.assertIn("已截断", fus[3]["body"])
|
||||
# 无父任务不写。
|
||||
orphan = dict(task, parent_task_id=None)
|
||||
self.assertFalse(worker._writeback_side_thread(
|
||||
orphan, "completed", "x"))
|
||||
|
||||
def test_parent_running_drain_consumes_writeback(self):
|
||||
_insert_task(self.db_path, "parent1", executor="local")
|
||||
_insert_task(self.db_path, "side1", kind="side",
|
||||
parent_task_id="parent1", executor="local")
|
||||
worker = Worker(self.db_path, "/fake/host.py", env=self.env)
|
||||
self.assertTrue(worker._writeback_side_thread(
|
||||
{"id": "side1", "project_id": "p1", "created_by": "tester",
|
||||
"kind": "side", "parent_task_id": "parent1"},
|
||||
"completed", "结论文本"))
|
||||
calls = []
|
||||
worker = self._worker(calls)
|
||||
result = worker.run_one(worker.get_task("parent1"))
|
||||
self.assertEqual(result["status"], "completed")
|
||||
# 首轮 + 排空一轮 = 两次 dispatch。
|
||||
dispatch_cmds = [c for c in calls if "dispatch" in c]
|
||||
self.assertEqual(len(dispatch_cmds), 2)
|
||||
fus = _followups(self.db_path, "parent1")
|
||||
self.assertEqual(len(fus), 1)
|
||||
self.assertEqual(fus[0]["status"], "done")
|
||||
|
||||
|
||||
class MarkerSideThreadTestCase(_WorkerBase):
|
||||
def _deliver(self, parent_id, creates):
|
||||
worker = Worker(self.db_path, "/fake/host.py", env=self.env)
|
||||
worker._deliver_marker_tools(worker.get_task(parent_id), {
|
||||
"status": "completed", "task_creates": creates,
|
||||
"task_polls": [], "wait_threads": [], "task_cancels": [],
|
||||
"task_retries": []})
|
||||
|
||||
def test_extract_kind(self):
|
||||
text = (
|
||||
"```task-create\n"
|
||||
"{\"prompt\": \"做子事\", \"kind\": \"side\"}\n```\n"
|
||||
"```task-create\n"
|
||||
"{\"prompt\": \"做子事\", \"kind\": \"weird\"}\n```\n"
|
||||
"```task-create\n{\"prompt\": \"做子事\"}\n```\n")
|
||||
creates = extract_task_creates(text)
|
||||
self.assertEqual(len(creates), 2)
|
||||
self.assertEqual(creates[0]["kind"], "side")
|
||||
self.assertIsNone(creates[1]["kind"])
|
||||
|
||||
def test_marker_child_inherits_side(self):
|
||||
_insert_task(self.db_path, "side-parent", kind="side",
|
||||
executor="local")
|
||||
self._deliver("side-parent", [{
|
||||
"name": "子旁聊", "prompt": "继续聊",
|
||||
"model": None, "effort": None, "executor": None,
|
||||
"kind": None}])
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
try:
|
||||
child = conn.execute(
|
||||
"SELECT kind, executor FROM tasks"
|
||||
" WHERE parent_task_id = ?",
|
||||
("side-parent",)).fetchone()
|
||||
finally:
|
||||
conn.close()
|
||||
self.assertIsNotNone(child)
|
||||
self.assertEqual(child[0], "side")
|
||||
self.assertEqual(child[1], "local")
|
||||
|
||||
def test_marker_child_explicit_kind_wins(self):
|
||||
_insert_task(self.db_path, "side-parent", kind="side",
|
||||
executor="local")
|
||||
self._deliver("side-parent", [{
|
||||
"name": "转主聊", "prompt": "正经做",
|
||||
"model": None, "effort": None, "executor": None,
|
||||
"kind": "main"}])
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
try:
|
||||
child = conn.execute(
|
||||
"SELECT kind FROM tasks WHERE parent_task_id = ?",
|
||||
("side-parent",)).fetchone()
|
||||
finally:
|
||||
conn.close()
|
||||
self.assertIsNotNone(child)
|
||||
self.assertEqual(child[0], "main")
|
||||
|
||||
def test_marker_counts_exclude_side(self):
|
||||
_insert_task(self.db_path, "main-parent", executor="local")
|
||||
_insert_task(self.db_path, "side-kid", kind="side",
|
||||
parent_task_id="main-parent", executor="local")
|
||||
_insert_task(self.db_path, "main-kid",
|
||||
parent_task_id="main-parent", executor="local")
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
conn.row_factory = sqlite3.Row
|
||||
try:
|
||||
self.assertEqual(
|
||||
count_direct_spawn_children(conn, "main-parent"), 1)
|
||||
self.assertEqual(
|
||||
parent_spawn_generation(conn, "side-kid"), 0)
|
||||
self.assertEqual(
|
||||
parent_spawn_generation(conn, "main-kid"), 1)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
class MigrateSideThreadTestCase(unittest.TestCase):
|
||||
def test_migrate_adds_kind_idempotent(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
db_path = os.path.join(tmp, "old.db")
|
||||
conn = sqlite3.connect(db_path)
|
||||
try:
|
||||
conn.execute(
|
||||
"CREATE TABLE tasks(id TEXT PRIMARY KEY,"
|
||||
" project_id TEXT, prompt TEXT, status TEXT)")
|
||||
conn.commit()
|
||||
migrate_tasks_table(conn)
|
||||
cols = {r[1] for r in conn.execute(
|
||||
"PRAGMA table_info(tasks)").fetchall()}
|
||||
self.assertIn("kind", cols)
|
||||
# 第二次是 no-op,不炸、不重复。
|
||||
migrate_tasks_table(conn)
|
||||
cols2 = {r[1] for r in conn.execute(
|
||||
"PRAGMA table_info(tasks)").fetchall()}
|
||||
self.assertEqual(cols, cols2)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
class SandboxSideThreadTestCase(unittest.TestCase):
|
||||
def test_local_dispatch_no_worktree_flag(self):
|
||||
calls = []
|
||||
ex = LocalExecutor("/fake/host.py", runner=_recording(calls))
|
||||
ex.dispatch_task({"id": "t1", "prompt": "hi"},
|
||||
no_worktree=True)
|
||||
self.assertIn("--no-worktree", calls[0])
|
||||
calls.clear()
|
||||
ex.dispatch_task({"id": "t1", "prompt": "hi"})
|
||||
self.assertNotIn("--no-worktree", calls[0])
|
||||
|
||||
def test_local_goal_no_worktree_flag(self):
|
||||
calls = []
|
||||
ex = LocalExecutor("/fake/host.py", runner=_recording(calls))
|
||||
ex.goal_task({"id": "t1", "prompt": "目标"}, no_worktree=True)
|
||||
self.assertIn("--no-worktree", calls[0])
|
||||
self.assertIn("goal", calls[0])
|
||||
|
||||
def test_local_plan_passthrough_readonly(self):
|
||||
calls = []
|
||||
ex = LocalExecutor("/fake/host.py", runner=_recording(calls))
|
||||
ex.dispatch_task({"id": "t1", "prompt": "hi", "plan": 1})
|
||||
self.assertIn("--sandbox", calls[0])
|
||||
self.assertEqual(calls[0][calls[0].index("--sandbox") + 1],
|
||||
"read-only")
|
||||
calls.clear()
|
||||
ex.dispatch_task({"id": "t1", "prompt": "hi"})
|
||||
self.assertNotIn("--sandbox", calls[0])
|
||||
|
||||
def test_local_popen_no_worktree_flag(self):
|
||||
calls = []
|
||||
|
||||
class FakePopen(object):
|
||||
def __init__(self, cmd, **kwargs):
|
||||
calls.append(list(cmd))
|
||||
self.returncode = 0
|
||||
|
||||
def poll(self):
|
||||
return 0
|
||||
|
||||
def communicate(self, timeout=None):
|
||||
return "", ""
|
||||
|
||||
ex = LocalExecutor("/fake/host.py", runner=_recording([]))
|
||||
import cloud.sandbox as sandbox_mod
|
||||
with mock.patch.object(sandbox_mod.subprocess, "Popen",
|
||||
FakePopen):
|
||||
ex.popen_task({"id": "t1", "prompt": "hi"},
|
||||
kind="dispatch", no_worktree=True)
|
||||
ex.popen_task({"id": "t1", "prompt": "目标"},
|
||||
kind="goal", no_worktree=True)
|
||||
dispatch_cmds = [c for c in calls if "dispatch" in c]
|
||||
goal_cmds = [c for c in calls if "goal" in c]
|
||||
self.assertEqual(len(dispatch_cmds), 1)
|
||||
self.assertEqual(len(goal_cmds), 1)
|
||||
self.assertIn("--no-worktree", dispatch_cmds[0])
|
||||
self.assertIn("--no-worktree", goal_cmds[0])
|
||||
|
||||
def test_docker_dispatch_no_worktree_flag(self):
|
||||
calls = []
|
||||
ex = DockerExecutor(
|
||||
"/fake/host.py", runner=_recording(calls),
|
||||
env={"XCM_API_KEY": "placeholder-not-a-real-key"})
|
||||
with mock.patch("cloud.sandbox.shutil.which",
|
||||
return_value="/usr/bin/docker"):
|
||||
ex.dispatch_task({"id": "t1", "prompt": "hi"},
|
||||
no_worktree=True)
|
||||
self.assertIn("--no-worktree", calls[0])
|
||||
calls.clear()
|
||||
with mock.patch("cloud.sandbox.shutil.which",
|
||||
return_value="/usr/bin/docker"):
|
||||
ex.dispatch_task({"id": "t1", "prompt": "hi"})
|
||||
self.assertNotIn("--no-worktree", calls[0])
|
||||
|
||||
def test_docker_goal_no_worktree_flag(self):
|
||||
calls = []
|
||||
ex = DockerExecutor(
|
||||
"/fake/host.py", runner=_recording(calls),
|
||||
env={"XCM_API_KEY": "placeholder-not-a-real-key"})
|
||||
with mock.patch("cloud.sandbox.shutil.which",
|
||||
return_value="/usr/bin/docker"):
|
||||
ex.goal_task({"id": "t1", "prompt": "目标"},
|
||||
no_worktree=True)
|
||||
self.assertIn("--no-worktree", calls[0])
|
||||
|
||||
|
||||
class _FakeHostServer(object):
|
||||
def __init__(self):
|
||||
self.created = []
|
||||
|
||||
def create_thread(self, cwd, model=None, sandbox="workspace-write",
|
||||
approval_policy="never", instructions=None):
|
||||
self.created.append((cwd, sandbox))
|
||||
return "thread-1"
|
||||
|
||||
|
||||
class HostSideThreadTestCase(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self._old_here = host_mod.HERE
|
||||
host_mod.HERE = self.tmp.name
|
||||
|
||||
def tearDown(self):
|
||||
host_mod.HERE = self._old_here
|
||||
self.tmp.cleanup()
|
||||
|
||||
def test_dispatch_no_worktree_no_repo_uses_scratch(self):
|
||||
srv = _FakeHostServer()
|
||||
task = host_mod.dispatch(
|
||||
srv, {"tasks": {}}, "side-abc", "聊聊", None, None, None,
|
||||
None, "never", use_worktree=False)
|
||||
expected = os.path.join(self.tmp.name, "work", "side-threads",
|
||||
"side-abc")
|
||||
self.assertEqual(task["cwd"], expected)
|
||||
self.assertTrue(os.path.isdir(expected))
|
||||
self.assertFalse(os.path.exists(os.path.join(expected, ".git")))
|
||||
self.assertIsNone(task["branch"])
|
||||
self.assertEqual(srv.created[0][0], expected)
|
||||
# 没有建 host-worktrees 目录。
|
||||
self.assertFalse(os.path.exists(
|
||||
os.path.join(self.tmp.name, "work", "host-worktrees")))
|
||||
|
||||
def test_dispatch_no_worktree_with_repo_uses_repo(self):
|
||||
srv = _FakeHostServer()
|
||||
task = host_mod.dispatch(
|
||||
srv, {"tasks": {}}, "side-def", "聊聊", "/tmp/some-repo",
|
||||
None, None, None, "never", use_worktree=False)
|
||||
self.assertEqual(task["cwd"], "/tmp/some-repo")
|
||||
self.assertIsNone(task["branch"])
|
||||
self.assertFalse(os.path.exists(
|
||||
os.path.join(self.tmp.name, "work", "side-threads")))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -12,7 +12,7 @@ seeded file -- a dropped middle chapter (01..07) would pass CI silently.
|
||||
The bidirectional assertion previously discussed on the #209 track is
|
||||
unmerged, so main does not have it; this file supplies it on the main
|
||||
base (skill text v0.2, 9 appendix chapters 00..08;
|
||||
integrated text is v0.5, 11 appendix chapters 00..10).
|
||||
integrated text is v0.5, 12 appendix chapters 00..11).
|
||||
|
||||
What "bidirectional" means here, concretely (all derived from SKILL.md
|
||||
itself, no new semantics invented):
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
执行器 per-call 挂载/网络覆盖、网络创建失败不降级。
|
||||
"""
|
||||
|
||||
import ipaddress
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
@@ -15,12 +16,12 @@ from types import SimpleNamespace
|
||||
from unittest import mock
|
||||
|
||||
try:
|
||||
from cloud.sandbox import DockerExecutor
|
||||
from cloud.sandbox import DockerExecutor, tenant_subnet_candidates
|
||||
from cloud.server import init_db
|
||||
from cloud.worker import Worker
|
||||
except ImportError:
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
||||
from cloud.sandbox import DockerExecutor
|
||||
from cloud.sandbox import DockerExecutor, tenant_subnet_candidates
|
||||
from cloud.server import init_db
|
||||
from cloud.worker import Worker
|
||||
|
||||
@@ -118,6 +119,34 @@ class TenantDirsTestCase(unittest.TestCase):
|
||||
worker._tenant_dirs("projA")
|
||||
|
||||
|
||||
class TenantSubnetCandidatesTestCase(unittest.TestCase):
|
||||
def test_candidates_inside_allowlist_distinct_and_24(self):
|
||||
allow = ipaddress.IPv4Network("172.16.0.0/12")
|
||||
seen = list(tenant_subnet_candidates("codex-p"))
|
||||
self.assertEqual(len(seen), 32)
|
||||
self.assertEqual(len(set(seen)), 32)
|
||||
for item in seen:
|
||||
net = ipaddress.IPv4Network(item)
|
||||
self.assertEqual(net.prefixlen, 24)
|
||||
self.assertTrue(net.subnet_of(allow))
|
||||
|
||||
def test_candidates_deterministic_and_name_dependent(self):
|
||||
first = list(tenant_subnet_candidates("codex-a"))
|
||||
self.assertEqual(first, list(tenant_subnet_candidates("codex-a")))
|
||||
# sha256("codex-a") 首字节 59;顺延一格是 60。
|
||||
self.assertEqual(first[0], "172.16.59.0/24")
|
||||
self.assertEqual(first[1], "172.16.60.0/24")
|
||||
self.assertNotEqual(
|
||||
first[0], list(tenant_subnet_candidates("codex-b"))[0])
|
||||
|
||||
def test_candidates_wrap_around_last_slot(self):
|
||||
# sha256("codex-zz") 首字节 250:槽位到 255 后绕回 0。
|
||||
seen = list(tenant_subnet_candidates("codex-zz"))
|
||||
self.assertEqual(seen[0], "172.16.250.0/24")
|
||||
self.assertEqual(seen[5], "172.16.255.0/24")
|
||||
self.assertEqual(seen[6], "172.16.0.0/24")
|
||||
|
||||
|
||||
class DockerTenantCommandTestCase(unittest.TestCase):
|
||||
def test_per_call_mounts_network_and_drop_caps(self):
|
||||
seen = []
|
||||
@@ -165,6 +194,68 @@ class DockerTenantCommandTestCase(unittest.TestCase):
|
||||
env={"XCM_API_KEY": "test-placeholder"})
|
||||
self.assertFalse(ex.ensure_network("codex-p"))
|
||||
|
||||
def test_ensure_network_creates_with_pinned_subnet(self):
|
||||
seen = []
|
||||
with mock.patch("cloud.sandbox.shutil.which",
|
||||
return_value="/usr/bin/docker"):
|
||||
ex = DockerExecutor(
|
||||
"/fake/host.py",
|
||||
runner=_ok_runner_factory(seen),
|
||||
env={"XCM_API_KEY": "test-placeholder"})
|
||||
self.assertTrue(ex.ensure_network("codex-p"))
|
||||
self.assertEqual(len(seen), 1)
|
||||
# sha256("codex-p") 首字节 138:首候选 172.16.138.0/24。
|
||||
self.assertEqual(seen[0], ["docker", "network", "create",
|
||||
"--subnet", "172.16.138.0/24",
|
||||
"codex-p"])
|
||||
|
||||
def test_ensure_network_overlap_probes_next_subnet(self):
|
||||
seen = []
|
||||
|
||||
def overlap_then_ok(cmd, timeout=None):
|
||||
seen.append(list(cmd))
|
||||
if len(seen) == 1:
|
||||
return SimpleNamespace(
|
||||
returncode=1, stdout="",
|
||||
stderr="Error response from daemon: Pool overlaps "
|
||||
"with other one on this address space")
|
||||
return SimpleNamespace(returncode=0, stdout="", stderr="")
|
||||
with mock.patch("cloud.sandbox.shutil.which",
|
||||
return_value="/usr/bin/docker"):
|
||||
ex = DockerExecutor("/fake/host.py", runner=overlap_then_ok,
|
||||
env={"XCM_API_KEY": "test-placeholder"})
|
||||
self.assertTrue(ex.ensure_network("codex-p"))
|
||||
self.assertEqual(len(seen), 2)
|
||||
self.assertEqual(seen[0][4], "172.16.138.0/24")
|
||||
self.assertEqual(seen[1][4], "172.16.139.0/24")
|
||||
self.assertEqual(seen[1][5], "codex-p")
|
||||
|
||||
def test_ensure_network_overlap_exhausted_is_false(self):
|
||||
seen = []
|
||||
|
||||
def always_overlap(cmd, timeout=None):
|
||||
seen.append(list(cmd))
|
||||
return SimpleNamespace(
|
||||
returncode=1, stdout="",
|
||||
stderr="Error response from daemon: Pool overlaps "
|
||||
"with other one on this address space")
|
||||
with mock.patch("cloud.sandbox.shutil.which",
|
||||
return_value="/usr/bin/docker"):
|
||||
ex = DockerExecutor("/fake/host.py", runner=always_overlap,
|
||||
env={"XCM_API_KEY": "test-placeholder"})
|
||||
self.assertFalse(ex.ensure_network("codex-p"))
|
||||
self.assertEqual(len(seen), 32)
|
||||
|
||||
def test_ensure_network_empty_name_is_false(self):
|
||||
def must_not_run(cmd, timeout=None):
|
||||
raise AssertionError("empty name must not call docker")
|
||||
with mock.patch("cloud.sandbox.shutil.which",
|
||||
return_value="/usr/bin/docker"):
|
||||
ex = DockerExecutor("/fake/host.py", runner=must_not_run,
|
||||
env={"XCM_API_KEY": "test-placeholder"})
|
||||
self.assertFalse(ex.ensure_network(""))
|
||||
self.assertFalse(ex.ensure_network(None))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
+110
-8
@@ -529,6 +529,9 @@ def _task_from_row(row):
|
||||
"coordinator_parts": _coord_parse_parts(_col("coordinator_parts")),
|
||||
"parent_task_id": _col("parent_task_id"),
|
||||
"coordinator_part": _col("coordinator_part"),
|
||||
# 旁聊(Muse 角色对齐 P2):老行缺列/NULL 时按 "main"
|
||||
# 计(与 server 侧 _task_to_dict 同款惯例)。
|
||||
"kind": _col("kind") or "main",
|
||||
# agent 定义 v2:老行缺列时 None = 未用定义,不炸。
|
||||
"agent_name": _col("agent_name"),
|
||||
"agent_version": _col("agent_version"),
|
||||
@@ -3701,6 +3704,7 @@ class Worker:
|
||||
|
||||
def _execute_via_process(self, task, kind="dispatch",
|
||||
deadline_seconds=None, seed_host_dir=None,
|
||||
no_worktree=False,
|
||||
quota_check=None):
|
||||
"""进程模式执行一轮:启动、轮询、取消时杀进程组。
|
||||
|
||||
@@ -3724,10 +3728,12 @@ class Worker:
|
||||
else:
|
||||
# seed 只对 dispatch/goal 首轮有意义:run 续跑不传
|
||||
# (文件已在 worktree 里);popen_task 内部同样只在
|
||||
# dispatch/goal 命令上追加 --seed-dir。
|
||||
# dispatch/goal 命令上追加 --seed-dir。no_worktree
|
||||
# 同款只对 dispatch/goal 命令追加 --no-worktree。
|
||||
proc = self.executor.popen_task(
|
||||
task, kind=kind, seed_dir=seed_host_dir,
|
||||
extra_env=gateway_env, model_provider=gateway_provider)
|
||||
extra_env=gateway_env, model_provider=gateway_provider,
|
||||
no_worktree=no_worktree)
|
||||
self._running_set(task_id, proc=proc)
|
||||
try:
|
||||
self._set_child_pid(task_id, getattr(proc, "pid", None))
|
||||
@@ -4592,6 +4598,50 @@ class Worker:
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _writeback_side_thread(self, task, final, text):
|
||||
"""旁聊终态回写父级(P2 W1):旁聊终局后写一条
|
||||
task_followups 到父任务(sender_task_id=旁聊 id,
|
||||
mode='queued',status='pending')。父 running 时排空
|
||||
循环自动消费(带 _peer_prefix 前缀);父 queued 时
|
||||
下次认领后首轮排空消费;父终态时行只作记录,
|
||||
GET /tasks/{id}/followups 与 chat-history 可见,
|
||||
不复活父任务。正文截 2000 字(与协调员合流报告的
|
||||
单分工上限同款)。回写是尽力而为:失败返回 False,
|
||||
不改终局、不重试、不炸执行。"""
|
||||
parent_id = task.get("parent_task_id")
|
||||
if not parent_id:
|
||||
return False
|
||||
text = (text or "").strip()
|
||||
if final == "completed":
|
||||
head, body = "[旁聊结论]", text or "(旁聊无结论文本)"
|
||||
elif final == "failed":
|
||||
head, body = "[旁聊失败]", text or "(无错误文本)"
|
||||
else:
|
||||
head, body = "[旁聊中断]", text or "(无文本)"
|
||||
full = "%s %s" % (head, body)
|
||||
if len(full) > 2000:
|
||||
full = full[:2000] + "…(已截断)"
|
||||
try:
|
||||
conn = _db_connect(self.db_path)
|
||||
try:
|
||||
_ensure_followups_table(conn)
|
||||
conn.execute(
|
||||
"INSERT INTO task_followups(id, task_id, project_id,"
|
||||
" body, mode, status, created_by, sender_task_id,"
|
||||
" created_at, updated_at)"
|
||||
" VALUES(?, ?, ?, ?, 'queued', 'pending', ?, ?,"
|
||||
" ?, ?)",
|
||||
(uuid.uuid4().hex, parent_id,
|
||||
task["project_id"], full, task.get("created_by"),
|
||||
task["id"], time.time(), time.time()),
|
||||
)
|
||||
conn.commit()
|
||||
return True
|
||||
finally:
|
||||
conn.close()
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _poll_task_snapshot(self, task, poll):
|
||||
"""轮询一个目标任务:每 interval 查一次库状态,直到终态、
|
||||
超时或父任务被取消。等待期间不烧模型费,但占着本 worker
|
||||
@@ -4889,17 +4939,26 @@ class Worker:
|
||||
child_effort = (create["effort"]
|
||||
or task.get("child_effort")
|
||||
or task.get("effort"))
|
||||
# 旁聊继承(P2 G7):旁聊内 marker 派生
|
||||
# 的子任务默认继承父 kind='side'(除非
|
||||
# 块里显式指定 kind),否则子任务会以
|
||||
# 主聊身份建 worktree/分支,旁聊的无
|
||||
# worktree 承诺被绕过;执行器已按
|
||||
# create["executor"] or 父 executor 继承。
|
||||
child_kind = create.get("kind") or (
|
||||
"side" if task.get("kind") == "side"
|
||||
else None)
|
||||
conn.execute(
|
||||
"INSERT INTO tasks(id, project_id, prompt,"
|
||||
" status, idempotency_key, depends_on,"
|
||||
" priority, created_by, created_at,"
|
||||
" updated_at, last_heartbeat, executor,"
|
||||
" model, effort, environment_id,"
|
||||
" parent_task_id, coordinator_part,"
|
||||
" parent_task_id, coordinator_part, kind,"
|
||||
" child_model, child_effort)"
|
||||
" VALUES(?, ?, ?, 'queued', NULL, '[]',"
|
||||
" ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, NULL,"
|
||||
" ?, ?)",
|
||||
" ?, ?, ?)",
|
||||
(child_id, task["project_id"],
|
||||
create["prompt"],
|
||||
task.get("priority", 0) or 0,
|
||||
@@ -4910,6 +4969,7 @@ class Worker:
|
||||
child_effort,
|
||||
task.get("environment_id"),
|
||||
task["id"],
|
||||
child_kind,
|
||||
task.get("child_model"),
|
||||
task.get("child_effort")),
|
||||
)
|
||||
@@ -5624,6 +5684,22 @@ class Worker:
|
||||
task_id = task["id"]
|
||||
result = None
|
||||
error = None
|
||||
# 旁聊默认执行(P2 G4):kind='side' 且执行器是 local
|
||||
# (旁聊默认,未显式要求 docker/cloud)时不建 worktree/
|
||||
# 分支;显式 docker/cloud 走既有语义。显式 local 与
|
||||
# 默认 local 在行上不可区分(都是 local),一律走
|
||||
# 无 worktree 路径。
|
||||
_side_default = task.get("kind") == "side" and \
|
||||
(task.get("executor") or "local") == "local"
|
||||
if _side_default and (task.get("repo") or
|
||||
getattr(self.executor, "repo", None)):
|
||||
# 有 repo 的旁聊:cwd 就是 repo,强制 plan=1
|
||||
# (只读沙箱),不得把 repo 写脏;plan 经
|
||||
# dispatch --sandbox read-only 透传到 codex。
|
||||
# 只改内存里的 task dict,不写库(plan 列是任务
|
||||
# 创建时的属性,旁聊的只读是执行层强制)。
|
||||
task = dict(task)
|
||||
task["plan"] = 1
|
||||
# goal 模式:goal_mode=1(新语义,prompt 即目标)或带
|
||||
# verify_command(旧语义)时走 host.py goal 循环,不走
|
||||
# 普通 dispatch。goal 循环内部已多轮重试,worker 级
|
||||
@@ -5633,7 +5709,10 @@ class Worker:
|
||||
# 附件 seed:dispatch/goal 首轮才用;run 续跑不传(文件已
|
||||
# 在 worktree 里)。cloud 执行器没有 worktree 可 seed,
|
||||
# 创建时本应已拒绝,这里是兜底防线(直接失败,不静默跑)。
|
||||
seed_host_dir = self._task_seed_dir(task)
|
||||
# 旁聊默认执行不 seed(创建时已拒 upload_id,附件行恒为空,
|
||||
# 这里跳过 _task_seed_dir 的一次查库)。
|
||||
seed_host_dir = None if _side_default \
|
||||
else self._task_seed_dir(task)
|
||||
if seed_host_dir and isinstance(self.executor, CloudExecutor):
|
||||
return ({"executor": "cloud", "task_id": task_id,
|
||||
"status": "failed", "stdout": "",
|
||||
@@ -5678,9 +5757,11 @@ class Worker:
|
||||
_quota_watchdog_thread = None
|
||||
# P0 三件其一:goal 任务起断点镜像线程(任何执行器);
|
||||
# 非 goal 任务没有断点语义,不起。终局镜像在 finally 补。
|
||||
# 旁聊默认执行不起镜像(旁聊的恢复走 thread 续跑,不走
|
||||
# 断点镜像;旁聊 goal 同款)。
|
||||
_mirror_stop = None
|
||||
_mirror_thread = None
|
||||
if goal_mode:
|
||||
if goal_mode and not _side_default:
|
||||
try:
|
||||
_mirror_stop = threading.Event()
|
||||
_mirror_thread = threading.Thread(
|
||||
@@ -5765,6 +5846,7 @@ class Worker:
|
||||
task, kind=popen_kind,
|
||||
deadline_seconds=_effective_task_timeout,
|
||||
seed_host_dir=seed_host_dir,
|
||||
no_worktree=_side_default,
|
||||
quota_check=quota_check)
|
||||
combined = (result.get("stdout", "") or "") + \
|
||||
(result.get("stderr", "") or "")
|
||||
@@ -5899,7 +5981,8 @@ class Worker:
|
||||
result = self.executor.goal_task(
|
||||
task, seed_dir=seed_host_dir,
|
||||
extra_env=gateway_env,
|
||||
model_provider=gateway_provider)
|
||||
model_provider=gateway_provider,
|
||||
no_worktree=_side_default)
|
||||
else:
|
||||
result = self.executor.goal_task(task)
|
||||
else:
|
||||
@@ -5907,7 +5990,8 @@ class Worker:
|
||||
result = self.executor.dispatch_task(
|
||||
task, seed_dir=seed_host_dir,
|
||||
extra_env=gateway_env,
|
||||
model_provider=gateway_provider)
|
||||
model_provider=gateway_provider,
|
||||
no_worktree=_side_default)
|
||||
else:
|
||||
result = self.executor.dispatch_task(task)
|
||||
combined = (result.get("stdout", "") or "") + \
|
||||
@@ -8968,6 +9052,12 @@ class Worker:
|
||||
# 只留档);不计数派发(平台/协调员)时为空操作。
|
||||
self._record_ledger_attempt(task_id, "interrupted",
|
||||
_ledger_counted)
|
||||
# 旁聊终态回写(P2 W1):取消也是终态,父级应收到
|
||||
# 中断说明;回写失败不挡取消返回。
|
||||
if task.get("kind") == "side":
|
||||
self._writeback_side_thread(
|
||||
task, "interrupted",
|
||||
(result or {}).get("stdout", "") or "cancelled")
|
||||
return {"dispatched": True, "task_id": task_id,
|
||||
"host_name": self.executor._host_name(task)
|
||||
if hasattr(self.executor, "_host_name") else task_id,
|
||||
@@ -9083,6 +9173,18 @@ class Worker:
|
||||
message="audit loop wire failed: %s" % exc)
|
||||
except Exception:
|
||||
pass
|
||||
# 旁聊终态回写(P2 W1):只在终局真正写回且未被审计
|
||||
# 推回(回 queued 不是终态)时写一条 task_followups
|
||||
# 到父任务;父 running 时排空消费,父 queued 时下次
|
||||
# 认领后首轮排空消费,父终态时只作记录、不复活父。
|
||||
# final 已是审计接线后的终局(含 completed/failed/
|
||||
# interrupted;配额中断的 final 也是 interrupted)。
|
||||
if (task.get("kind") == "side" and final_written
|
||||
and not audit_loop_pushback
|
||||
and final in ("completed", "failed", "interrupted")):
|
||||
self._writeback_side_thread(
|
||||
task, final,
|
||||
stdout if final == "completed" else (error or ""))
|
||||
# 附件暂存清理:任务终局后删 bundle 目录(重试走已存在
|
||||
# 续跑,文件已在 worktree 里,不再需要暂存)。
|
||||
self._cleanup_task_seed(task)
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
| B16 | worker / 监控 / 模型(`GET /workers`、`/workers/heartbeat`、`/metrics`、`/metrics-report`、`/audit-scorecard`、`/routing-shadow`、`/models`、`/relay/models`) | `model/list`、`modelProvider/capabilities/read` 覆盖模型面;其余无 | 部分映射:模型列表有对应;worker 池、指标、评分卡、路由影子 **原生无对应表面** |
|
||||
| B17 | 待办(5 个端点,`/projects/{id}/todos`) | 无 | 原生无对应表面 |
|
||||
| B18 | 模型总览与中转模型列表(`GET /models`、`GET /relay/models`) | `model/list`(C 实测仅一条 `gpt-6.1-sol`) | 部分映射:单模型列表有对应;中转站多模型下拉无原生面(旧控制台用 `/relay/models` 做模型下拉,新 UI 用 `model/list`) |
|
||||
| B19 | 旁聊会话列表(`kind='side'` 任务:`POST /tasks` 带 `kind` + `parent_task_id`、`GET /tasks?kind=side`、`GET /tasks?parent_task_id=&kind=side`) | 无。原生 UI 只有线程列表,无“主聊 + 旁聊”分组、无 kind 筛选、无“开旁聊”入口 | 原生无对应表面(旁聊会话列表仅 `/console-classic`) |
|
||||
|
||||
## §3 旧 console.html 的去留决定
|
||||
|
||||
|
||||
@@ -2588,6 +2588,13 @@ def dispatch(server, state, name, prompt, repo, model, effort, sandbox,
|
||||
branch = None
|
||||
if use_worktree and repo:
|
||||
cwd, branch = make_worktree(repo, name)
|
||||
if not use_worktree and not repo:
|
||||
# 旁聊无 repo:create_thread 不接受 cwd=None
|
||||
# (os.path.abspath(None) 抛错),以 per-task scratch
|
||||
# 目录为 cwd(普通目录,非 git worktree,不建分支;
|
||||
# host 任务名即任务 id 的安全化形式,目录按任务隔离)。
|
||||
cwd = os.path.join(HERE, "work", "side-threads", name)
|
||||
os.makedirs(cwd, exist_ok=True)
|
||||
if seed_dir:
|
||||
# 附件只能进 worktree:直接以 repo 为 cwd 时拷进去等于污染
|
||||
# 仓库,必须 loud fail,绝不静默放行。
|
||||
@@ -3259,6 +3266,10 @@ def main():
|
||||
p.add_argument("--repo", default=None)
|
||||
p.add_argument("--seed-dir", default=None,
|
||||
help="附件暂存目录:派发时拷进 worktree 再跑 turn")
|
||||
p.add_argument("--no-worktree", action="store_true",
|
||||
help="不建 worktree/分支:无 repo 时以 per-task"
|
||||
" scratch 目录为 cwd,有 repo 时以 repo 为"
|
||||
" cwd(旁聊默认,见 dispatch 同款)")
|
||||
p.add_argument("--verify", default=None,
|
||||
help="验收命令;不传时走模型自决:模型在回合末尾"
|
||||
"单独一行输出“目标已达成”即达标")
|
||||
@@ -4208,6 +4219,7 @@ def main():
|
||||
task = dispatch(srv, state, args.name, args.task, args.repo,
|
||||
args.model, args.effort, args.sandbox,
|
||||
args.approval_policy,
|
||||
use_worktree=not args.no_worktree,
|
||||
seed_dir=args.seed_dir)
|
||||
save_state(args.state, state)
|
||||
# 新开循环的 spawn goal 是权威的:上一轮同名循环
|
||||
|
||||
Reference in new issue
Block a user