Author SHA1 Message Date
host 3e0ff898e0 feat: wire native Codex UI as /console entry, keep classic console at /console-classic
ci / test (push) Successful in 45m25s
- Vendor codex-ui-stack host/ as cloud/codex-ui-host/ (original/ not vendored, 650MB)
- Bridge emulates 7 methods missing in codex 0.164.0-alpha.9, exposes 29
  experimental-protocol methods (150 -> 179), Linux binary candidates,
  read-only POST /__codex_git
- GET /console 302s to the UI host (:8095) when reachable, else falls back
  to the classic React console; GET /console-classic always serves it
- Alignment tables: docs/codex-native-ui-alignment.md
- Tests: test_console 7, new test_codex_ui_host 4, handler method pin 247 -> 249
2026-11-06 19:03:35 +00:00
host 41f3985248 console: unify goal mode to no-time-limit only (port of 395dffb onto 37ba687)
ci / test (push) Successful in 40m37s
DispatchSection keeps a single 'goal 模式' checkbox (the former
invincible option): checking it sends goal_mode=1 plus
no_time_limit=1. TaskDetailPanel drops the '无敌版' label.
Ported verbatim from feat-console-goal-unify commit 395dffb
(base 622c51e); console.html rebuilt from cloud/console-react
with npm run build:console (tsc+vite pass) and synced verbatim
from dist/index.html (sha256 daba1941...).
2026-11-06 17:09:34 +00:00
23 changed files with 3054 additions and 64 deletions

No files matched your search

@@ -0,0 +1,370 @@
/**
* Bridge to the real local `codex app-server`.
*
* The desktop app talks to the Codex app server over stdio JSON-RPC; the main
* process owns that child process. In a browser we cannot spawn anything, so
* this module owns the child on the Node side and exposes a plain
* request/response surface over HTTP.
*
* The binary ships inside the ChatGPT desktop app:
* ChatGPT.app/Contents/Resources/codex-cli/CodexCLI.app/Contents/MacOS/codex
*
* On Linux (this repo's server target) the same binary is found via
* $CODEX_BIN (same env name host.py uses), /opt/hatch-image/bin/codex, or
* PATH. $CODEX_APP_SERVER_BIN keeps the highest priority.
*
* Nothing here reaches the network: the app server reads `~/.codex` and runs
* locally. Methods that would require a remote model call are only invoked when
* the UI explicitly asks for them.
*/
import { spawn } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const HERE = path.dirname(fileURLToPath(import.meta.url));
// A6: Linux candidates first after the explicit override, macOS paths kept.
const CANDIDATE_BINARIES = [
process.env.CODEX_APP_SERVER_BIN,
process.env.CODEX_BIN,
"/opt/hatch-image/bin/codex",
"/Applications/ChatGPT.app/Contents/Resources/codex-cli/CodexCLI.app/Contents/MacOS/codex",
"/Applications/ChatGPT.app/Contents/Resources/codex-cli/bin/codex",
].filter(Boolean);
/** Search PATH for an executable `codex` (last-resort candidate, A6). */
function findCodexOnPath() {
const sep = process.platform === "win32" ? ";" : ":";
for (const dir of (process.env.PATH || "").split(sep)) {
if (!dir) continue;
const p = path.join(dir, "codex");
try {
if (fs.existsSync(p) && fs.statSync(p).isFile()) {
fs.accessSync(p, fs.constants.X_OK);
return p;
}
} catch {
/* ignore */
}
}
return null;
}
export function findCodexBinary() {
for (const p of CANDIDATE_BINARIES) {
try {
if (fs.existsSync(p) && fs.statSync(p).isFile()) return p;
} catch {
/* ignore */
}
}
return findCodexOnPath();
}
/**
* Relay configuration. The web build talks to an OpenAI-compatible relay
* instead of a ChatGPT account, so the app server is started with per-process
* `-c` overrides. Nothing is written to ~/.codex/config.toml — the desktop app
* keeps its own settings and the override dies with this process.
*
* Shape: { providerId, providerName, baseUrl, apiKey, envKey, model, wireApi }
*/
export function loadRelayConfig() {
const file = process.env.CODEX_WEB_RELAY_CONFIG || path.join(HERE, "relay-config.json");
if (!fs.existsSync(file)) return null;
try {
const cfg = JSON.parse(fs.readFileSync(file, "utf8"));
return cfg && cfg.baseUrl ? cfg : null;
} catch {
return null;
}
}
/** Translate relay config into `codex app-server` CLI overrides. */
export function relayArgs(cfg) {
if (!cfg) return [];
const id = cfg.providerId || "relay";
const args = [
"-c", `model_provider=${JSON.stringify(id)}`,
"-c", `model=${JSON.stringify(cfg.model)}`,
"-c", `preferred_auth_method="apikey"`,
"-c", `model_providers.${id}.name=${JSON.stringify(cfg.providerName || id)}`,
"-c", `model_providers.${id}.base_url=${JSON.stringify(cfg.baseUrl)}`,
"-c", `model_providers.${id}.wire_api=${JSON.stringify(cfg.wireApi || "responses")}`,
"-c", `model_providers.${id}.env_key=${JSON.stringify(cfg.envKey || "RELAY_API_KEY")}`,
// Everything that would talk to OpenAI's backend (plugin catalog, account
// endpoints, telemetry) is answered by the mock backend served by the web
// host itself: well-formed local JSON, no request ever leaves the machine,
// and callers see success instead of connection errors.
"-c", `chatgpt_base_url=${JSON.stringify(cfg.chatgptBaseUrl || mockBackendUrl())}`,
];
// Optional safety overrides. Left out by default so the app server keeps
// whatever ~/.codex/config.toml says; set them in relay-config.json to pin
// the sandbox for a shared or unattended browser build.
if (cfg.approvalPolicy) args.push("-c", `approval_policy=${JSON.stringify(cfg.approvalPolicy)}`);
if (cfg.sandboxMode) args.push("-c", `sandbox_mode=${JSON.stringify(cfg.sandboxMode)}`);
return args;
}
/** The web host serves the ChatGPT-backend stand-in under /mock. */
function mockBackendUrl() {
const port = process.env.CODEX_WEB_PORT || "8095";
return `http://127.0.0.1:${port}/mock`;
}
const DEFAULT_TIMEOUT_MS = 30_000;
/**
* Methods the app server can leave unanswered when the surrounding desktop
* infrastructure is absent. MCP discovery is the only one today: with no MCP
* servers configured there is nothing to enumerate, and the request simply
* never completes. Answer it locally after a short grace period.
*/
const METHOD_TIMEOUT_MS = { "mcpServerStatus/list": 6_000 };
const METHOD_FALLBACKS = { "mcpServerStatus/list": { data: [] } };
// ------------------------------------------------------------------ A1–A4
// Emulation for methods the vendored UI calls but codex 0.164.0-alpha.9 does
// not implement (verified: the binary answers them with `unknown variant`).
// Only the seven method names listed here are intercepted; every other
// method passes through to the real app server untouched.
//
// A1 thread/stop -> turn/interrupt (the Codex App stop button is
// exactly a turn/interrupt; host.py confirms this).
// A2 thread/rollback -> thread/revert (exists in this binary).
// A3 thread/startAeon -> thread/start. Aeon is an experimental mode with
// no entry point in this binary, so this is a
// downgrade to an ordinary start, not Aeon.
// A4 interactive/* -> the interactive-sessions surface does not exist
// in this binary. *list methods answer with empty
// lists (no sessions are fabricated — the UI shows
// "no live sessions"); upload / sandbox read answer
// with a structured -32601 method-not-found error
// instead of fake data. (Per docs/codex-native-ui-
// alignment.md A4: list 类回空列表.)
const EMULATED_REDIRECTS = {
"thread/stop": "turn/interrupt",
"thread/rollback": "thread/revert",
"thread/startAeon": "thread/start",
};
const INTERACTIVE_EMPTY_LIST_METHODS = new Set([
"interactive/liveSessions/list",
"interactive/sessionSandbox/list",
]);
const INTERACTIVE_UNAVAILABLE_METHODS = new Set([
"interactive/session/upload",
"interactive/sessionSandbox/read",
]);
export class AppServerBridge {
constructor({ log = console, onNotification = null } = {}) {
this.log = log;
this.onNotification = onNotification;
this.child = null;
this.seq = 0;
this.pending = new Map();
this.buffer = "";
this.ready = null;
this.notifications = [];
this.waiter = null;
this.lastError = null;
}
get binary() {
return findCodexBinary();
}
/** Spawn the app server (once) and complete the JSON-RPC handshake. */
async ensure() {
if (this.ready) return this.ready;
this.ready = (async () => {
const bin = this.binary;
if (!bin) throw new Error("codex binary not found");
const relay = loadRelayConfig();
const args = ["app-server", ...relayArgs(relay)];
const env = { ...process.env, RUST_LOG: process.env.RUST_LOG ?? "error" };
if (relay?.apiKey) env[relay.envKey || "RELAY_API_KEY"] = relay.apiKey;
const child = spawn(bin, args, { stdio: ["pipe", "pipe", "pipe"], env });
this.child = child;
if (relay) {
this.log.log?.(`[app-server] relay ${relay.baseUrl} model=${relay.model}`);
}
child.stdout.setEncoding("utf8");
child.stdout.on("data", (chunk) => this.#onStdout(chunk));
child.stderr.setEncoding("utf8");
// Full, untruncated stderr for diagnosis; ANSI colour codes included.
this.stderrFile = fs.createWriteStream("/tmp/codex-appserver-stderr.log", { flags: "a" });
child.stderr.on("data", (chunk) => {
const text = String(chunk);
this.stderrFile?.write(text);
const clean = text.trim();
if (clean && !/unrecognized configuration settings|is ignored/.test(clean)) {
this.lastError = clean.slice(0, 400);
// Backend-shape mismatches surface here (the mock backend answers
// every path; a parse complaint means a shape to improve).
this.log.warn?.(`[app-server] ${clean.slice(0, 200)}`);
}
});
child.on("exit", (code, signal) => {
this.log.warn?.(`[app-server] exited code=${code} signal=${signal}`);
this.child = null;
this.ready = null;
for (const { reject } of this.pending.values()) {
reject(new Error("app-server exited"));
}
this.pending.clear();
});
await this.request("initialize", {
clientInfo: { name: "codex-web", title: "Codex Web", version: "1.0.0" },
// The renderer calls `collaborationMode/list` and friends, which the
// server rejects with -32600 unless the client opts into the
// experimental surface during the handshake.
capabilities: { experimentalApi: true },
});
this.log.log?.("[app-server] ready");
return true;
})();
this.ready.catch((err) => {
this.log.error?.(`[app-server] failed: ${err.message}`);
this.ready = null;
});
return this.ready;
}
#onStdout(chunk) {
this.buffer += chunk;
let nl;
while ((nl = this.buffer.indexOf("\n")) !== -1) {
const line = this.buffer.slice(0, nl).trim();
this.buffer = this.buffer.slice(nl + 1);
if (!line) continue;
let msg;
try {
msg = JSON.parse(line);
} catch {
continue;
}
if (msg.id != null && this.pending.has(msg.id) && !msg.method) {
const { resolve, reject } = this.pending.get(msg.id);
this.pending.delete(msg.id);
msg.error ? reject(Object.assign(new Error(msg.error.message ?? "rpc error"), { rpc: msg.error })) : resolve(msg.result);
continue;
}
if (msg.method) {
// Server-initiated request (has id) or notification (no id). Both go on
// the same stream; the renderer distinguishes them by `id`.
this.notifications.push(msg);
if (this.notifications.length > 2000) this.notifications.shift();
if (this.onNotification) {
try {
this.onNotification(msg);
} catch (err) {
this.log.warn?.(`[app-server] onNotification handler failed: ${err?.message}`);
}
}
this.#wake();
}
}
}
/** Resolve the pending long-poll, if any. */
#wake() {
const waiter = this.waiter;
if (!waiter) return;
this.waiter = null;
clearTimeout(waiter.timer);
waiter.resolve();
}
/**
* Wait until at least one notification is queued (or the timeout fires), then
* hand back everything that accumulated. Long-polling keeps the renderer in
* step with the app server without a websocket.
*/
waitForNotifications(timeoutMs = 15_000) {
if (this.notifications.length) return Promise.resolve(this.drainNotifications());
return new Promise((resolve) => {
const timer = setTimeout(() => {
this.waiter = null;
resolve(this.drainNotifications());
}, timeoutMs);
this.waiter = { resolve: () => resolve(this.drainNotifications()), timer };
});
}
request(method, params) {
// A1–A3: redirect to the real method that implements the same action.
// Only these three names are redirected; anything else falls through.
if (method in EMULATED_REDIRECTS) {
const target = EMULATED_REDIRECTS[method];
this.log.log?.(`[app-server] emulated ${method} -> ${target}`);
return this.ensure().then(() => this.request(target, params));
}
// A4: no live sessions exist in this binary — answer empty, fabricate none.
// The three list-shaped fields cover the shapes the renderer dereferences;
// all three are empty.
if (INTERACTIVE_EMPTY_LIST_METHODS.has(method)) {
return Promise.resolve({ sessions: [], data: [], items: [] });
}
// A4: interactive session upload / sandbox access is unavailable in
// codex 0.164.0-alpha.9. Structured -32601, exactly as the app server
// reports an unknown method — the renderer surfaces it, nothing is faked.
if (INTERACTIVE_UNAVAILABLE_METHODS.has(method)) {
const error = new Error(
`method not found: ${method} (interactive sessions not available in codex 0.164.0-alpha.9)`,
);
error.code = -32601;
error.rpc = { code: -32601, message: error.message };
return Promise.reject(error);
}
if (!this.child) return Promise.reject(new Error("app-server not running"));
const id = ++this.seq;
const payload = JSON.stringify({ jsonrpc: "2.0", id, method, params: params ?? {} });
const timeoutMs = METHOD_TIMEOUT_MS[method] ?? DEFAULT_TIMEOUT_MS;
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
this.pending.delete(id);
// A few methods never answer in a headless build (MCP discovery has no
// servers to enumerate). Resolve with the truthful empty result instead
// of leaving the renderer's query spinning for the full timeout.
if (method in METHOD_FALLBACKS) {
resolve(METHOD_FALLBACKS[method]);
return;
}
reject(new Error(`app-server timeout: ${method}`));
}, timeoutMs);
this.pending.set(id, {
resolve: (v) => {
clearTimeout(timer);
resolve(v);
},
reject: (e) => {
clearTimeout(timer);
reject(e);
},
});
this.child.stdin.write(payload + "\n");
});
}
/** Drain queued server notifications (used by the polling endpoint). */
drainNotifications() {
const out = this.notifications;
this.notifications = [];
return out;
}
dispose() {
this.child?.kill();
this.child = null;
this.ready = null;
}
}
@@ -0,0 +1,181 @@
[
"account/bedrock/checkGovCloudRequirements",
"account/bedrock/discover",
"account/bedrock/setup",
"account/gatewayOAuth/cancel",
"account/gatewayOAuth/login",
"account/gatewayOAuth/read",
"account/login/cancel",
"account/login/start",
"account/logout",
"account/rateLimitResetCredit/consume",
"account/rateLimits/read",
"account/read",
"account/sendAddCreditsNudgeEmail",
"account/usage/read",
"account/workspaceMessages/read",
"app/installed",
"app/list",
"app/read",
"collaborationMode/list",
"command/exec",
"command/exec/resize",
"command/exec/terminate",
"command/exec/write",
"config/batchWrite",
"config/mcpServer/reload",
"config/read",
"config/value/write",
"configRequirements/read",
"environment/add",
"environment/info",
"environment/status",
"experimentalFeature/enablement/set",
"experimentalFeature/list",
"externalAgentConfig/detect",
"externalAgentConfig/import",
"externalAgentConfig/import/readHistories",
"externalAgentConfig/import/recordHistory",
"feedback/upload",
"fs/copy",
"fs/createDirectory",
"fs/getMetadata",
"fs/readDirectory",
"fs/readFile",
"fs/remove",
"fs/unwatch",
"fs/watch",
"fs/writeFile",
"fuzzyFileSearch/sessionStart",
"fuzzyFileSearch/sessionStop",
"fuzzyFileSearch/sessionUpdate",
"hooks/list",
"interactive/liveSessions/list",
"interactive/session/upload",
"interactive/sessionSandbox/list",
"interactive/sessionSandbox/read",
"marketplace/add",
"marketplace/remove",
"marketplace/upgrade",
"mcpServer/event/stream/start",
"mcpServer/event/stream/stop",
"mcpServer/oauth/login",
"mcpServer/resource/read",
"mcpServer/tool/call",
"mcpServerStatus/list",
"memory/reset",
"memory/status",
"mock/experimentalMethod",
"model/list",
"modelProvider/capabilities/read",
"permissionProfile/list",
"plugin/install",
"plugin/installed",
"plugin/list",
"plugin/read",
"plugin/reconcile",
"plugin/search",
"plugin/share/checkout",
"plugin/share/delete",
"plugin/share/list",
"plugin/share/save",
"plugin/share/updateTargets",
"plugin/skill/read",
"plugin/uninstall",
"process/kill",
"process/resizePty",
"process/spawn",
"process/writeStdin",
"project/create",
"project/delete",
"project/import",
"project/list",
"project/move",
"project/read",
"project/update",
"remoteControl/client/list",
"remoteControl/client/revoke",
"remoteControl/disable",
"remoteControl/enable",
"remoteControl/pairing/start",
"remoteControl/pairing/status",
"remoteControl/status/read",
"review/start",
"rollout/compress",
"server/diagnostics",
"skills/config/write",
"skills/extraRoots/set",
"skills/list",
"thread/approveGuardianDeniedAction",
"thread/archive",
"thread/attachment/add",
"thread/attachment/list",
"thread/attachment/remove",
"thread/attachmentOwner/list",
"thread/backgroundTerminals/clean",
"thread/backgroundTerminals/list",
"thread/backgroundTerminals/terminate",
"thread/compact/start",
"thread/decrement_elicitation",
"thread/delete",
"thread/fork",
"thread/goal/clear",
"thread/goal/get",
"thread/goal/set",
"thread/increment_elicitation",
"thread/inject_items",
"thread/items/list",
"thread/items/read",
"thread/list",
"thread/loaded/list",
"thread/memoryMode/set",
"thread/metadata/update",
"thread/name/set",
"thread/prediction/request",
"thread/queue/add",
"thread/queue/delete",
"thread/queue/list",
"thread/queue/reorder",
"thread/queue/start",
"thread/queue/update",
"thread/read",
"thread/readState/update",
"thread/realtime/appendAudio",
"thread/realtime/appendSpeech",
"thread/realtime/appendText",
"thread/realtime/listVoices",
"thread/realtime/start",
"thread/realtime/stop",
"thread/resume",
"thread/revert",
"thread/rollback",
"thread/sample",
"thread/sample/cancel",
"thread/search",
"thread/searchOccurrences",
"thread/section/move",
"thread/settings/update",
"thread/shellCommand",
"thread/start",
"thread/startAeon",
"thread/stop",
"thread/timeline/list",
"thread/turns/list",
"thread/unarchive",
"thread/unsubscribe",
"threadSection/create",
"threadSection/delete",
"threadSection/list",
"threadSection/update",
"turn/interrupt",
"turn/settings/update",
"turn/start",
"turn/steer",
"userVerification/cancel",
"userVerification/delete",
"userVerification/enroll",
"userVerification/status",
"userVerification/verify",
"windowsSandbox/readiness",
"windowsSandbox/setupStart"
]
+109
View File
@@ -0,0 +1,109 @@
#!/usr/bin/env node
/**
* Load the local web host in the headless browser, capture console + exceptions,
* then dump the resulting page state and a screenshot.
*
* node host/cdp-load.mjs --port 9333 --url http://127.0.0.1:8095/ --out /tmp/shot.png
*/
import fs from "node:fs";
const argv = process.argv.slice(2);
const argOf = (name, fallback) => {
const i = argv.indexOf(name);
return i !== -1 && argv[i + 1] ? argv[i + 1] : fallback;
};
const PORT = argOf("--port", "9333");
const URL_ = argOf("--url", "http://127.0.0.1:8095/");
const OUT = argOf("--out", "/tmp/codexweb.png");
const SETTLE = Number(argOf("--settle", "6000"));
const version = await (await fetch(`http://127.0.0.1:${PORT}/json/version`)).json();
const ws = new WebSocket(version.webSocketDebuggerUrl);
let seq = 0;
const pending = new Map();
const events = [];
const send = (method, params = {}, sessionId) =>
new Promise((resolve, reject) => {
const id = ++seq;
pending.set(id, { resolve, reject });
const msg = { id, method, params };
if (sessionId) msg.sessionId = sessionId;
ws.send(JSON.stringify(msg));
});
ws.addEventListener("message", (ev) => {
const msg = JSON.parse(ev.data);
if (msg.method) {
if (msg.method === "Runtime.consoleAPICalled") {
const text = (msg.params.args || [])
.map((a) => a.value ?? a.description ?? a.unserializableValue ?? "")
.join(" ")
.slice(0, 600);
events.push(`[${msg.params.type}] ${text}`);
} else if (msg.method === "Runtime.exceptionThrown") {
const d = msg.params.exceptionDetails;
events.push(`[EXCEPTION] ${d.exception?.description ?? d.text}`.slice(0, 2200));
} else if (msg.method === "Log.entryAdded") {
const e = msg.params.entry;
events.push(`[log:${e.level}] ${e.text}`.slice(0, 500));
}
}
if (!msg.id || !pending.has(msg.id)) return;
const { resolve, reject } = pending.get(msg.id);
pending.delete(msg.id);
msg.error ? reject(new Error(JSON.stringify(msg.error).slice(0, 400))) : resolve(msg.result);
});
await new Promise((r) => ws.addEventListener("open", r, { once: true }));
const { targetInfos } = await send("Target.getTargets");
const pages = targetInfos.filter((t) => t.type === "page");
const main = pages.find((t) => t.url.includes("127.0.0.1")) ?? pages[0];
if (!main) {
console.error("no page target");
process.exit(1);
}
const { sessionId } = await send("Target.attachToTarget", { targetId: main.targetId, flatten: true });
await send("Page.enable", {}, sessionId);
await send("Runtime.enable", {}, sessionId);
await send("Log.enable", {}, sessionId);
await send("Page.navigate", { url: URL_ }, sessionId);
// Runtime.enable replays the previous page's console buffer; drop it so the
// report only covers this load.
events.length = 0;
await new Promise((r) => setTimeout(r, SETTLE));
const state = await send(
"Runtime.evaluate",
{
expression: `(() => {
const root = document.querySelector('#root');
const text = (root?.innerText || '').replace(/\\s+/g, ' ').trim();
return JSON.stringify({
url: location.href,
title: document.title,
rootChildren: root ? root.children.length : -1,
rootBytes: root ? root.innerHTML.length : 0,
textLength: text.length,
textSample: text.slice(0, 500),
bootstrapped: !!globalThis.__codexBootstrapped,
hasShim: typeof globalThis.__codexHost,
}, null, 1);
})()`,
returnByValue: true,
},
sessionId,
);
console.log("STATE:", state.result.value);
console.log(`\nEVENTS (${events.length}):`);
for (const e of events.slice(0, 60)) console.log(" " + e);
const shot = await send("Page.captureScreenshot", { format: "png" }, sessionId);
fs.writeFileSync(OUT, Buffer.from(shot.data, "base64"));
console.log(`\nscreenshot: ${OUT} (${fs.statSync(OUT).size} bytes)`);
ws.close();
+417
View File
@@ -0,0 +1,417 @@
/**
* Local app host for the browser build.
*
* The desktop renderer obtains its host through `connect-app-host`, which in
* Electron hands a MessagePort to the main process; the main process answers
* with a capability-RPC object exposing `services` plus a handful of host
* lifecycle hooks. In a browser there is no main process, so this file supplies
* the same object locally:
*
* - data services are generated from the app-server method table and proxied
* to the real local `codex app-server` (see host/app-server-bridge.mjs);
* - lifecycle members that only exist inside Electron are stubbed.
*
* The generated shape matters: `services.thread.list` must resolve to a call
* that issues `thread/list`, `services.config.read` to `config/read`, and so
* on. Anything the UI probes but we do not provide is reported once on the
* console so gaps are visible instead of silent.
*
* Served as a classic script. The marker below is replaced with the app-server
* method list at response time.
*/
(() => {
const METHODS = /*@METHODS@*/ [];
// Every path prefix of every method becomes a namespace object, so feature
// detection (`if (services.terminal) ...`) sees `undefined` for members that
// genuinely do not exist.
const namespaces = new Set();
for (const method of METHODS) {
const parts = method.split("/");
for (let i = 1; i < parts.length; i += 1) namespaces.add(parts.slice(0, i).join("/"));
}
const reported = new Set();
const reportMissing = (path) => {
if (reported.has(path)) return;
reported.add(path);
const stack = (new Error().stack || "").split("\n").slice(2, 4).join(" | ");
console.info("[codex-local-host] unimplemented service member:", path, "@", stack);
};
// Permissive stub for Electron-main-only service namespaces: every unknown
// member is an inert async function, so unguarded calls do not throw. Only
// use this where the renderer never feature-detects the member itself.
const permissive = (label, members = {}) =>
new Proxy(members, {
get(target, prop) {
if (prop in target) return target[prop];
if (typeof prop === "symbol" || prop === "then" || prop === "toJSON") return undefined;
reportMissing(`${label}.${String(prop)}`);
return () => Promise.resolve();
},
});
async function rpc(method, params) {
const res = await fetch("/__codex_rpc", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ method, params: params ?? {} }),
});
if (!res.ok) throw new Error(`app-server HTTP ${res.status}`);
const body = await res.json();
if (body && body.error) {
const err = new Error(body.error.message || `app-server error: ${method}`);
err.code = body.error.code;
throw err;
}
return body ? body.result : undefined;
}
// ------------------------------------------------------- persisted state
// `r$t()` in the renderer resolves `mi.applicationState` and then calls
// `.read(key)` -> `{ value }` and `.write(key, value)` -> `{ success }`.
// The desktop build backs this with an Electron store on disk; localStorage
// gives the same semantics here, so onboarding flags, the selected project
// and the pinned-thread list survive a reload instead of erroring out.
const STATE_PREFIX = "codex.appstate.";
const stateKey = (key) => STATE_PREFIX + String(key);
const applicationState = {
read: async (key) => {
console.info("[codex-local-host] applicationState.read:", String(key).slice(0, 80));
try {
const raw = localStorage.getItem(stateKey(key));
const out = { value: raw == null ? null : JSON.parse(raw) };
console.info("[codex-local-host] applicationState.read ->", String(key).slice(0, 60), JSON.stringify(out).slice(0, 120));
return out;
} catch (e) {
console.info("[codex-local-host] applicationState.read ERROR:", String(e));
return { value: null };
}
},
write: async (key, value) => {
try {
localStorage.setItem(stateKey(key), JSON.stringify(value));
return { success: true };
} catch {
return { success: false };
}
},
remove: async (key) => {
try {
localStorage.removeItem(stateKey(key));
} catch {
/* ignore */
}
return { success: true };
},
keys: async () => {
const out = [];
for (let i = 0; i < localStorage.length; i += 1) {
const k = localStorage.key(i);
if (k && k.startsWith(STATE_PREFIX)) out.push(k.slice(STATE_PREFIX.length));
}
return out;
},
};
// ------------------------------------------------------------- http fetch
// `$_t` calls `httpFetch.fetch(id, request, onUploadProgress)` where
// `request` is `{ url, method, body, headers, keepalive, cache, ... }` and
// awaits `{ response }` or `{ error, status, ... }` — the handle (both the
// promise and the settled value) carries `Symbol.dispose`.
//
// Backend API paths (`/wham/...`, `/backend-api/...`, …) are answered by the
// local mock backend served by the web host — the same policy the app
// server's chatgpt_base_url follows. Nothing ever leaves the page.
const API_PATH_RE = /^\/(wham|backend-api|ces|v1|api|public)\//;
const httpFetch = {
fetch: (id, request, onUploadProgress) => {
const controller = new AbortController();
const dispose = () => controller.abort();
const opts = typeof request === "string" ? { url: request } : request || {};
let url = String(opts.url ?? "");
const method = String(opts.method || "GET").toUpperCase();
if (API_PATH_RE.test(url)) url = "/mock" + url;
const init = {
method,
headers: opts.headers,
signal: controller.signal,
cache: opts.cache,
keepalive: opts.keepalive,
};
if (opts.body != null && method !== "GET" && method !== "HEAD") {
init.body = typeof opts.body === "string" ? opts.body : JSON.stringify(opts.body);
}
const pending = Promise.resolve()
.then(() => fetch(url, init))
.then(
(response) => ({ response, [Symbol.dispose]: dispose }),
(error) => ({
error: String((error && error.message) || error),
status: 0,
errorCode: null,
responseStatus: 0,
errorKind: "network",
[Symbol.dispose]: dispose,
}),
);
// `$_t` disposes the handle *and* the settled value, so both carry one.
pending[Symbol.dispose] = dispose;
return pending;
},
cancel: async () => {},
};
// ---------------------------------------------------- projectless threads
// A conversation that is not attached to a project still needs a working
// directory. The desktop main process allocates one; here the Node host does
// it (host/projectless.mjs) and this is the renderer-side handle.
//
// `createPaths` returns a cancellable resource rather than a promise: the
// caller awaits `.wait()` and disposes the handle when it aborts.
const hostCall = async (path, body) => {
const res = await fetch(path, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
});
if (!res.ok) throw new Error(`host call ${path} -> HTTP ${res.status}`);
const payload = await res.json();
if (payload && payload.error) throw new Error(payload.error.message || "host call failed");
return payload ? payload.result : undefined;
};
const projectlessThreads = {
createPaths: (options) => {
const controller = new AbortController();
const settled = hostCall("/__codex_projectless", { op: "createPaths", args: options ?? {} });
return {
wait: () => settled,
[Symbol.dispose]: () => controller.abort(),
};
},
readWorkspaceRoot: () => hostCall("/__codex_projectless", { op: "readWorkspaceRoot" }),
};
// ---------------------------------------------------------------- lifecycle
// Members that live in the Electron main process rather than the app server.
// Shapes follow the call sites in the renderer's bootstrap.
const noopDisposable = { [Symbol.dispose]() {} };
const explicit = {
"startup.whenReady": () => Promise.resolve(),
// The renderer reports paint milestones through this and chains `.catch`.
"startup.reach": async () => {},
applicationState,
httpFetch,
projectlessThreads,
// `Q$` falls back to `{ values: {} }` when this is absent, which is the
// right answer for a build with no desktop settings store behind it.
settings: null,
// `remoteConnections` is consumed as `e.catalog.subscribe(fn)` and
// `e.setRemoteControlEnabled(...)`; both must exist and return promises.
remoteConnections: permissive("remoteConnections", {
catalog: { subscribe: () => noopDisposable },
setRemoteControlEnabled: async () => {},
}),
"executionHosts.getDefaultHost": async () => ({ hostId: "local", kind: "local" }),
"executionHosts.getExecutionHostKeys": () => ["local"],
"threadReadState.initialize": async () => {},
"threadReadState.dispose": () => {},
"tracing.exportTraceBatch": async () => {},
"tracing.setSampleRate": async () => ({}),
// Local-only build: no remote telemetry pipeline to feed.
performanceTelemetry: { report: () => {}, flush: async () => {} },
// ---- host-owned app metadata -----------------------------------------
// Normally answered by the Electron main process from the bundle plist.
// Values are derived from the browser environment so the shell can print a
// real platform string instead of "unknown".
"appInfo.get": async () => ({
version: "26.1021.31345",
buildFlavor: "codex",
buildNumber: "26.1021.31345",
systemVersion: navigator.userAgent,
preloadAttribution: null,
chatGptApiBaseUrl: null,
}),
"appInfo.getSystemInfo": async () => ({
platform: "darwin",
osVersion: navigator.userAgent,
arch: "arm64",
release: navigator.userAgent,
}),
// ---- thread configuration ---------------------------------------------
// The desktop main process reads two things off disk before a thread can
// start: developer instructions and the MCP block of the Codex config.
// Neither exists for a browser build. The renderer dereferences
// `.instructions` directly, so the shape matters even when it is empty.
threadConfiguration: {
readDeveloperInstructions: async () => ({ instructions: null }),
readMcpConfig: async () => ({ config: null }),
},
// The desktop shell auto-resolves pending user-input prompts after a
// timeout, and tracks whether a conversation is on screen so it only fires
// for visible chats. A local build never auto-resolves anything, but the
// renderer calls these unconditionally (only the method itself is
// optional-chained), so the namespace must exist.
requestUserInputAutoResolution: {
setConversationPresented: async () => {},
recordConversationActivity: async () => {},
snooze: async () => {},
},
// ---- git integration ----------------------------------------------------
// The desktop main process owns git queries (`RA('git')`). The UI gates the
// composer on the `['git','availability']` query, which is only ever filled
// as a side effect of a `stable-metadata` answer — so the service must
// exist and answer non-null, or Send stays disabled with "Loading…".
//
// A7: instead of the old stub (always `{available:false, …}`), forward to
// the host's POST /__codex_git, which runs read-only git commands inside
// the cwd the renderer passes in params. The renderer calls this as
// request({method, params:{cwd, …}}) (two-arg request(method, params)
// is accepted too). Without a cwd there is nothing to inspect, so fall
// back to the old stub shape for stable-metadata and null otherwise.
// stable-metadata keeps the original six-field shape, now populated.
git: permissive("git", {
request: async (arg1, arg2) => {
let method;
let params = {};
if (typeof arg1 === "string") {
method = arg1;
params = arg2 ?? {};
} else {
({ method, params = {} } = arg1 ?? {});
}
const cwd = typeof params?.cwd === "string" ? params.cwd : null;
if (method === "stable-metadata" && !cwd) {
return { available: false, branch: null, upstream: null, repository: null, state: null, worktree: null };
}
if (!cwd) return null;
try {
const res = await fetch("/__codex_git", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ op: method, cwd }),
});
if (!res.ok) return null;
const body = await res.json();
if (body && body.error) {
// Not a repo / bad cwd: report unavailable instead of throwing,
// so the composer degrades the way the old stub degraded it.
if (method === "stable-metadata") {
return { available: false, branch: null, upstream: null, repository: null, state: null, worktree: null };
}
return null;
}
return body ? body.result ?? null : null;
} catch {
if (method === "stable-metadata") {
return { available: false, branch: null, upstream: null, repository: null, state: null, worktree: null };
}
return null;
}
},
}),
// ---- statsig evaluations ----------------------------------------------
// In the desktop app the main process evaluates Statsig and pushes results
// over this service; the renderer never talks to the network for it. The
// local build pushes one truthful "no evaluations" payload: `payload: null`
// maps to `hasEvaluations: false`, feature defaults apply, and nothing
// ever polls a remote endpoint.
statsigEvaluations: {
subscribe: async (onValue) => {
queueMicrotask(() =>
onValue({
userId: null,
accountId: null,
authMethod: null,
stableId: "codex-web-local",
payload: null,
}),
);
return { [Symbol.dispose]() {} };
},
// The renderer publishes execution evaluations back to the host (which in
// Electron forwards them to the main process). There is nothing to feed
// locally; accept and drop.
publish: async () => {},
},
// Account state comes from the local app server, which reads ~/.codex.
"accountInfo.readAccountInfo": () => rpc("account/read", {}),
};
const explicitNulls = new Set(["histogramCollector", "statsig", "clientCoordination", "terminal", "devboxService"]);
function makeNamespace(path) {
const node = {};
const children = new Set();
for (const candidate of namespaces) {
if (!candidate.startsWith(path ? path + "/" : "")) continue;
const rest = path ? candidate.slice(path.length + 1) : candidate;
if (rest.includes("/")) continue;
children.add(rest);
}
for (const method of METHODS) {
if (!method.startsWith(path ? path + "/" : "")) continue;
const rest = path ? method.slice(path.length + 1) : method;
if (rest.includes("/")) continue;
node[rest] = (params) => rpc(method, params);
}
for (const child of children) {
node[child] = makeNamespace(path ? `${path}/${child}` : child);
}
return node;
}
function buildServices() {
const services = makeNamespace("");
// Overlay the host-only members, then strip the null ones.
for (const key of Object.keys(explicit)) {
const parts = key.split(".");
let cursor = services;
for (let i = 0; i < parts.length - 1; i += 1) {
cursor[parts[i]] = cursor[parts[i]] ?? {};
cursor = cursor[parts[i]];
}
cursor[parts[parts.length - 1]] = explicit[key];
}
for (const key of explicitNulls) services[key] = null;
// Report accesses to members we do not model.
return new Proxy(services, {
get(target, prop, receiver) {
if (typeof prop === "symbol") return Reflect.get(target, prop, receiver);
if (prop === "then" || prop === "toJSON") return undefined;
if (!(prop in target)) {
reportMissing(String(prop));
return undefined;
}
return Reflect.get(target, prop, receiver);
},
});
}
const servicesPromise = Promise.resolve().then(buildServices);
globalThis.__codexLocalHost = () => ({
services: servicesPromise,
onRpcBroken: () => () => {},
});
console.info("[codex-local-host] app host installed", METHODS.length, "methods");
})();
+223
View File
@@ -0,0 +1,223 @@
/**
* Local stand-in for the model wire.
*
* The app server speaks the OpenAI Responses API (`wire_api = "responses"`):
* one POST to `{base_url}/responses` answered with a `text/event-stream` of
* typed events. Producing that stream here keeps every byte of the
* conversation on this machine — no model provider is ever contacted.
*
* Extracted from `web-server.mjs` so the local model lives in one place: the
* host serves it, and any other local caller gets the identical wire shape.
*/
/** Read a JSON request body with a hard size cap. */
export const readJsonBody = (req, limit = 4_000_000) =>
new Promise((resolve, reject) => {
let raw = "";
req.setEncoding("utf8");
req.on("data", (c) => {
raw += c;
if (raw.length > limit) reject(new Error("body too large"));
});
req.on("end", () => {
try {
resolve(raw ? JSON.parse(raw) : {});
} catch (err) {
reject(err);
}
});
req.on("error", reject);
});
/** Collect every text fragment the app server put in the request. */
const collectText = (payload) => {
const texts = [];
const collect = (item) => {
if (!item || typeof item !== "object") return;
if (typeof item.text === "string") texts.push(item.text);
else if (Array.isArray(item.content)) for (const c of item.content) collect(c);
else if (typeof item.content === "string") texts.push(item.content);
};
for (const item of payload?.input ?? []) collect(item);
if (typeof payload?.input === "string") texts.push(payload.input);
return texts.join(" ");
};
/**
* Craft a deterministic local reply from the user's input.
*
* Deterministic arithmetic answers make end-to-end checks (`5+5 -> 10`)
* readable at a glance; anything else echoes back what arrived so a wrong
* request shape is immediately visible in the UI.
*/
export const mockReplyFor = (payload) => {
const joined = collectText(payload);
if (/5\s*\+\s*5/i.test(joined)) return "10";
if (/3\s*\+\s*3/i.test(joined)) return "6";
if (/2\s*\+\s*2/i.test(joined)) return "4";
if (/hello/i.test(joined)) return "hello from the local mock model";
return `Local mock reply. I received: ${joined.slice(0, 200) || "(empty input)"}`;
};
let callCount = 0;
const sseEvent = (res, type, data) => {
res.write(`event: ${type}\ndata: ${JSON.stringify(data)}\n\n`);
};
/**
* Answer `/mockrelay/v1/*` — the model provider surface.
*
* @param {import("node:http").IncomingMessage} req
* @param {import("node:http").ServerResponse} res
* @param {string} urlPath path beginning with `/mockrelay`
* @param {{ onRequest?: (line: string) => void }} [opts]
*/
export const serveMockRelay = (req, res, urlPath, opts = {}) => {
opts.onRequest?.(`${req.method} ${urlPath} (mock model relay)`);
const route = urlPath.replace(/^\/mockrelay/, "") || "/";
const json = (obj, status = 200) => {
res.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
res.end(JSON.stringify(obj));
};
// Model list — keeps provider probes happy.
if (route === "/v1/models" || route === "/models") {
json({ object: "list", data: [{ id: "gpt-6.1-sol", object: "model", owned_by: "local" }] });
return;
}
if (route === "/v1/responses" || route === "/responses") {
if (req.method !== "POST") {
res.writeHead(405).end("method not allowed");
return;
}
readJsonBody(req)
.then((payload) => {
const reply = mockReplyFor(payload);
const model = typeof payload?.model === "string" ? payload.model : "gpt-6.1-sol";
const n = ++callCount;
const respId = `resp_local_${Date.now().toString(36)}_${n}`;
const msgId = `msg_local_${n}`;
const now = Math.floor(Date.now() / 1000);
const responseShell = (status, extra = {}) => ({
id: respId,
object: "response",
created_at: now,
status,
model,
output: [],
usage: null,
...extra,
});
res.writeHead(200, {
"content-type": "text/event-stream; charset=utf-8",
"cache-control": "no-store",
connection: "keep-alive",
});
sseEvent(res, "response.created", { type: "response.created", sequence_number: 0, response: responseShell("in_progress") });
sseEvent(res, "response.in_progress", { type: "response.in_progress", sequence_number: 1, response: responseShell("in_progress") });
const messageItem = { type: "message", id: msgId, role: "assistant", status: "in_progress", content: [] };
sseEvent(res, "response.output_item.added", {
type: "response.output_item.added",
sequence_number: 2,
output_index: 0,
item: messageItem,
});
// Stream the reply in small chunks so the renderer's delta path runs.
const chunks = reply.match(/.{1,8}/gs) ?? [reply];
let seq = 3;
for (const chunk of chunks) {
sseEvent(res, "response.output_text.delta", {
type: "response.output_text.delta",
sequence_number: seq++,
item_id: msgId,
output_index: 0,
content_index: 0,
delta: chunk,
});
}
const completedItem = {
type: "message",
id: msgId,
role: "assistant",
status: "completed",
content: [{ type: "output_text", text: reply, annotations: [] }],
};
sseEvent(res, "response.output_text.done", {
type: "response.output_text.done",
sequence_number: seq++,
item_id: msgId,
output_index: 0,
content_index: 0,
text: reply,
});
sseEvent(res, "response.output_item.done", {
type: "response.output_item.done",
sequence_number: seq++,
output_index: 0,
item: completedItem,
});
const finalResponse = responseShell("completed", {
output: [completedItem],
usage: {
input_tokens: 0,
input_tokens_details: { cached_tokens: 0 },
output_tokens: 1,
output_tokens_details: { reasoning_tokens: 0 },
total_tokens: 1,
},
});
sseEvent(res, "response.completed", { type: "response.completed", sequence_number: seq++, response: finalResponse });
res.end();
})
.catch(() => {
res.writeHead(400, { "content-type": "application/json; charset=utf-8" });
res.end(JSON.stringify({ error: { message: "bad request body" } }));
});
return;
}
json({ error: { message: `mock relay: unhandled path ${route}` } }, 404);
};
/**
* Answer the ChatGPT-backend surface (`/mock/*`) with truthful local shapes.
*
* Every response is either the exact JSON the desktop main process would hand
* over, or an empty-but-valid object. The point is that nothing errors and
* nothing leaves the machine.
*/
export const serveMockBackend = (req, res, urlPath, opts = {}) => {
opts.onRequest?.(`${req.method} ${urlPath}`);
const json = (obj, status = 200) => {
res.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
res.end(JSON.stringify(obj));
};
const p = urlPath.replace(/^\/mock/, "") || "/";
// Cloud MCP endpoint (ChatGPT-hosted app connectors). 404 makes the app
// server mark these unavailable immediately — a semantically empty 200
// would be retried and block thread startup on MCP enumeration.
if (/^\/api\/codex\/ps\/mcp/.test(p)) {
json({ error: { message: "cloud connectors are unavailable in a local build" } }, 404);
return;
}
if (/^\/wham\/tasks\/list/.test(p)) {
json({ items: [], cursor: null });
return;
}
if (/plugin/i.test(p)) {
json({ schema_version: 1, fetched_at: new Date().toISOString(), plugins: [] });
return;
}
json({});
};
+62
View File
@@ -0,0 +1,62 @@
/**
* Projectless thread workspaces.
*
* Codex keeps two kinds of conversation: one attached to a project directory and
* one that starts from nothing. The desktop main process owns the second kind —
* it picks a folder under the user's workspace root, creates it, and hands the
* paths back to the renderer through `projectlessThreads.createPaths`.
*
* The browser cannot touch the filesystem, so the Node host does it here and
* exposes the same three paths over HTTP:
*
* cwd the directory this conversation runs in
* outputDirectory where generated files land (same folder)
* workspaceRoot the shared root that contains every thread
*
* Layout mirrors the desktop app: <root>/<YYYY-MM-DD>/<slug>
*/
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
export const WORKSPACE_ROOT =
// Linux server default: ~/Codex. The desktop default ~/Documents/Codex
// does not exist on a server, so it is not used here.
process.env.CODEX_WEB_WORKSPACE_ROOT || path.join(os.homedir(), "Codex");
export function ensureWorkspaceRoot() {
fs.mkdirSync(WORKSPACE_ROOT, { recursive: true });
return WORKSPACE_ROOT;
}
/** Lowercase, collapse anything awkward, keep CJK, cap the length. */
function slugify(value) {
const text = String(value ?? "").trim();
if (!text) return "";
return text
.toLowerCase()
.replace(/[^\p{Letter}\p{Number}]+/gu, "-")
.replace(/^-+|-+$/g, "")
.slice(0, 40);
}
export function createProjectlessPaths({ directoryName, prompt } = {}) {
const root = ensureWorkspaceRoot();
const day = new Date().toISOString().slice(0, 10);
const base = slugify(directoryName) || slugify(prompt) || "thread";
let dir = path.join(root, day, base);
let suffix = 1;
while (fs.existsSync(dir)) {
suffix += 1;
dir = path.join(root, day, `${base}-${suffix}`);
}
fs.mkdirSync(dir, { recursive: true });
return { cwd: dir, outputDirectory: dir, workspaceRoot: root };
}
export function readWorkspaceRoot() {
return { workspaceRoot: ensureWorkspaceRoot() };
}
@@ -0,0 +1,19 @@
# relay-config.json — mock 现状与接真实 relay
`relay-config.json` 保持 UI 栈自带的 Bumian 配置原样,未换 provider、未写任何密钥值。
## 当前状态:模型调用走本地 mock
- `baseUrl` 是 `http://127.0.0.1:8095/mockrelay/v1`:app-server 的模型请求由本机 `host/mock-model.mjs` 用确定性替身回答,不出本机、不产生真实调用、不花钱。
- `relayBaseUrl`(`https://www.bumianapi.top/v1`)只写在 JSON 里,host 代码没有引用它,当前不生效。
- `apiKey` 为空字符串:真实 relay 需要 key 时,key 值绝不写进本文件。
## 接真实 relay:两步
1. 把 `baseUrl` 的值改为 `relayBaseUrl` 的值(即 `https://www.bumianapi.top/v1`)。
只改这一处;`providerId` / `providerName` / `model` / `wireApi` / `envKey` 不动。
2. 在启动 UI host 的环境里设置 `BUMIAN_API_KEY` 环境变量(key 值只进环境,不进文件、不进仓库):
`BUMIAN_API_KEY=<你的 key> scripts/run-codex-ui.sh`
改完后 app-server 启动时按 `relay-config.json` 的 `-c` 覆盖走真实 relay;`chatgptBaseUrl`
仍指向本机 `/mock`,插件目录、账号端点、遥测等 ChatGPT 后端调用继续由本地替身回答。
@@ -0,0 +1,11 @@
{
"providerId": "bumian",
"providerName": "Bumian Relay",
"baseUrl": "http://127.0.0.1:8095/mockrelay/v1",
"relayBaseUrl": "https://www.bumianapi.top/v1",
"apiKey": "",
"envKey": "BUMIAN_API_KEY",
"model": "gpt-6.1-sol",
"wireApi": "responses",
"chatgptBaseUrl": "http://127.0.0.1:8095/mock"
}
+323
View File
@@ -0,0 +1,323 @@
/* Codex web host shim — browser-only replacement for the Electron preload.
*
* Loaded as a classic script before the module entry. It provides the two
* pieces of the Electron shell the renderer cannot live without:
*
* window.electronBridge — the preload API surface. Only the members the
* renderer actually probes are meaningful; the rest
* resolve to inert promises so .then/.catch chains
* stay valid.
* the app-server bridge — request envelopes sent through
* sendMessageFromView are forwarded to the local
* codex app server and answered on the window
* message bus, which is where the renderer listens.
*/
(() => {
const noop = () => {};
globalThis.__codexOut = [];
globalThis.__codexIn = [];
let rpcCount = 0;
// ------------------------------------------------------------------ inbound
// The renderer validates inbound messages with `event.source === null` (it
// skips the origin check in that case), which is exactly what a synthesized
// MessageEvent gives us.
const dispatchInbound = (payload) => {
globalThis.__codexIn.push(payload);
window.dispatchEvent(new MessageEvent("message", { data: payload }));
};
const forwardRequest = (msg) => {
const { id, method, params } = msg.request;
rpcCount += 1;
if (rpcCount <= 500) console.info("[codex-web] rpc ->", method);
fetch("/__codex_rpc", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ method: method, params: params ?? {} }),
})
.then((res) => res.json())
.then((body) => {
// The renderer's app-server request client reads the reply from
// `message: { id, result | error }`. `response` is kept alongside it
// because the app-host side of the same bus uses that spelling.
const message =
body && body.error
? { id: id, error: { code: body.error.code ?? -32000, message: body.error.message ?? "rpc failed" } }
: { id: id, result: body ? body.result : null };
dispatchInbound({
type: "mcp-response",
hostId: msg.hostId,
message: message,
response: message,
requestMethod: method,
receivedAtMs: Date.now(),
});
})
.catch((err) => {
const message = { id: id, error: { code: -32000, message: String((err && err.message) || err) } };
dispatchInbound({
type: "mcp-response",
hostId: msg.hostId,
message: message,
response: message,
requestMethod: method,
receivedAtMs: Date.now(),
});
});
return Promise.resolve();
};
// ---------------------------------------------------------------- events
// Everything the app server says on its own initiative — streaming deltas,
// turn progress, server-initiated approval prompts — arrives on this
// long-poll. The renderer subscribes to it as `mcp-notification`.
globalThis.__codexEvents = [];
let polling = false;
const pumpEvents = async () => {
if (polling) return;
polling = true;
while (polling) {
try {
const res = await fetch("/__codex_events", { cache: "no-store" });
const body = await res.json();
for (const event of body.events || []) {
globalThis.__codexEvents.push(event);
if (globalThis.__codexEvents.length > 400) globalThis.__codexEvents.shift();
if (event.id != null) {
// Server-initiated request: the renderer owns the answer path, which
// this shim does not model yet. Keep it visible instead of dropping.
globalThis.__codexServerRequests = globalThis.__codexServerRequests || [];
globalThis.__codexServerRequests.push(event);
continue;
}
dispatchInbound({
type: "mcp-notification",
hostId: "local",
method: event.method,
params: event.params ?? {},
});
}
} catch {
await new Promise((r) => setTimeout(r, 1000));
}
}
};
pumpEvents();
// ------------------------------------------------------------- electron API
// Synchronous getters first: callers treat these as plain values.
const SYNC = {
getInitialSidebarBootstrap: () => null,
getConfig: () => null,
getPlatform: () => "darwin",
getLocale: () => "zh-CN",
getVersion: () => "26.1021.31345",
isPackaged: () => true,
getWindowType: () => "electron",
getZoomLevel: () => 0,
isDeviceCheckSupported: () => true,
isIntelMacBuild: () => false,
getSystemThemeVariant: () => "light",
subscribeToSystemThemeVariant: () => noop,
getBuildFlavor: () => "codex",
getAppSessionId: () => "codex-web-local",
getDesktopUserAgent: () => navigator.userAgent,
getSentryInitOptions: () => ({ appVersion: "26.1021.31345", buildFlavor: "codex" }),
getSharedObjectSnapshotValue: () => undefined,
};
const makeBridge = () =>
new Proxy(SYNC, {
get(target, prop) {
if (prop in target) return target[prop];
if (prop === "then") return undefined; // never a thenable
if (typeof prop === "symbol") return undefined;
if (prop === "sendMessageFromView") {
return (msg) => {
globalThis.__codexOut.push(msg);
if (msg && msg.type === "mcp-request" && msg.request && typeof msg.request.method === "string") {
return forwardRequest(msg);
}
return Promise.resolve();
};
}
// Unknown member: an inert async function, so feature probes see
// "present" and promise chains stay valid.
return () => Promise.resolve();
},
has: () => true,
});
if (typeof window.electronBridge === "undefined") window.electronBridge = makeBridge();
if (typeof window.codexWindowType === "undefined") window.codexWindowType = "electron";
if (typeof window.__SENTRY_IPC__ === "undefined") window.__SENTRY_IPC__ = { sendMessage: noop };
window.__codexDesktop = window.__codexDesktop || {};
// ------------------------------------------------------------- offline net
// Nothing leaves the machine. Absolute URLs get a well-formed empty JSON body
// so callers fail softly instead of hitting the CSP. The remaining browser
// transports — XHR, sendBeacon, WebSocket, EventSource — are stubbed the same
// way: local requests pass through, everything else succeeds locally with
// empty data.
const realFetch = window.fetch.bind(window);
const isLocal = (url) =>
!url ||
url.startsWith("/") ||
url.startsWith("data:") ||
url.startsWith("blob:") ||
url.startsWith(location.origin);
window.fetch = (input, init) => {
const url = typeof input === "string" ? input : (input && input.url) || "";
if (isLocal(url)) return realFetch(input, init);
console.info("[codex-web] outbound:", (init?.method || "GET"), url.slice(0, 140));
const json = (obj, status = 200) =>
Promise.resolve(new Response(typeof obj === "string" ? obj : JSON.stringify(obj), {
status,
headers: { "content-type": "application/json" },
}));
// Statsig telemetry hosts. The SDK requires its initialize payload to
// carry `has_updates` (see _typedJsonParse), so hand back a valid empty
// evaluation set — every gate then resolves to "off" locally and the SDK
// completes initialization instead of retrying forever.
if (/ab\.chatgpt\.com|api\.oaistatsig\.com|statsigcdn\.openai\.com/i.test(url)) {
if (/\/initialize/i.test(url)) {
return json({
has_updates: true,
hash_used: "djb2",
time: Date.now(),
feature_gates: {},
dynamic_configs: {},
layer_configs: {},
param_stores: {},
});
}
if (/sdk_exception/i.test(url)) {
return Promise.resolve(new Response(null, { status: 204 }));
}
// rgstr / download_config_specs: accepted-and-empty.
return Promise.resolve(new Response(null, { status: 202 }));
}
// Everything else: generic empty JSON success.
return json({});
};
// XMLHttpRequest: same policy. A cross-origin send completes immediately
// with `{}` and status 200, on the microtask queue so ordering stays sane.
const XHR = window.XMLHttpRequest;
if (XHR && XHR.prototype) {
const openOrig = XHR.prototype.open;
const sendOrig = XHR.prototype.send;
XHR.prototype.open = function (method, url, ...rest) {
this.__codexUrl = String(url ?? "");
return openOrig.call(this, method, url, ...rest);
};
XHR.prototype.send = function (body) {
if (isLocal(this.__codexUrl)) return sendOrig.call(this, body);
const payload = this.responseType === "json" ? {} : "{}";
queueMicrotask(() => {
try {
Object.defineProperty(this, "readyState", { value: 4, configurable: true });
Object.defineProperty(this, "status", { value: 200, configurable: true });
Object.defineProperty(this, "statusText", { value: "OK", configurable: true });
Object.defineProperty(this, "responseText", { value: payload, configurable: true });
Object.defineProperty(this, "response", { value: payload, configurable: true });
Object.defineProperty(this, "responseURL", { value: this.__codexUrl, configurable: true });
this.dispatchEvent(new Event("readystatechange"));
this.dispatchEvent(new ProgressEvent("load"));
this.dispatchEvent(new ProgressEvent("loadend"));
} catch {
/* the request object went away; nothing to satisfy */
}
});
};
}
// sendBeacon is fire-and-forget by contract; "delivered" locally means dropped.
try {
if (navigator.sendBeacon) navigator.sendBeacon = () => true;
} catch {
/* some engines make it a getter; the CSP covers those */
}
// WebSocket: hand back a socket that is already closed. No network dial, no
// browser console noise, and well-behaved clients see a terminal state.
const RealWS = window.WebSocket;
if (RealWS) {
class ClosedSocket extends EventTarget {
constructor(url) {
super();
this.url = String(url ?? "");
this.readyState = 3; // CLOSED
this.bufferedAmount = 0;
this.extensions = "";
this.protocol = "";
this.binaryType = "blob";
queueMicrotask(() => {
this.dispatchEvent(new Event("error"));
this.dispatchEvent(new CloseEvent("close", { wasClean: true, code: 1000 }));
});
}
send() {
throw new DOMException("The socket is closed.", "InvalidStateError");
}
close() {}
}
for (const k of ["CONNECTING", "OPEN", "CLOSING", "CLOSED"]) ClosedSocket[k] = RealWS[k];
window.WebSocket = ClosedSocket;
}
// EventSource: a stream that closes immediately, for the same reason.
if (window.EventSource) {
class ClosedStream extends EventTarget {
constructor(url) {
super();
this.url = String(url ?? "");
this.readyState = 2; // CLOSED
this.withCredentials = false;
queueMicrotask(() => this.dispatchEvent(new Event("error")));
}
close() {}
}
window.EventSource = ClosedStream;
}
console.info("[codex-web] offline host shim installed");
})();
// ------------------------------------------------------------- config preseed
// The scope-query bridge only fetches from mounted React readers, and the
// config/effective + permission-profiles queries have no mounted reader in a
// browser fallback (no project tree, no workspace picker). Prime both from the
// local app server up front so the composer's agent-mode resolution sees real
// data on first render instead of a query that would stay pending forever.
(() => {
const rpc = (method, params) =>
fetch("/__codex_rpc", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ method, params: params ?? null }),
})
.then((r) => r.json())
.then((b) => (b && b.result ? b.result : null))
.catch(() => null);
globalThis.__codexCfg = null;
globalThis.__codexPerms = null;
rpc("config/read", { includeLayers: true, cwd: null }).then((r) => {
if (r && r.config) {
globalThis.__codexCfg = r;
console.info("[codex-web] config preseeded");
}
});
rpc("permissionProfile/list", { cursor: null, limit: 50, cwd: null }).then((r) => {
if (r && Array.isArray(r.data)) {
globalThis.__codexPerms = r.data;
console.info("[codex-web] permission profiles preseeded");
}
});
})();
@@ -0,0 +1,7 @@
{
"source": "/tmp/codex-asar/webview",
"vendoredAt": "2026-10-27T03:31:32.962Z",
"fileCount": 20918,
"indexHtmlSha256": "85e5c5b4ce1cd7e0fc42ccbac7fe6d9c6ac7b2eb4b8a7f77beb39d280fdca9f5",
"verification": "diff -rq clean; identical sha256"
}
@@ -0,0 +1,71 @@
#!/usr/bin/env node
/**
* Vendor the original webview verbatim into ./original.
*
* Source: the extracted ChatGPT.app/Contents/Resources/app.asar, i.e.
* <extracted-asar>/webview
*
* Nothing is rewritten. The copy is byte-for-byte identical, which is what makes
* the UI 100% the original rather than an approximation.
*
* Usage:
* node host/vendor-original.mjs --from /tmp/codex-asar/webview
* node host/vendor-original.mjs --from /path/to/webview --clean
*/
import fs from "node:fs";
import path from "node:path";
import crypto from "node:crypto";
import { fileURLToPath } from "node:url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const projectRoot = path.resolve(__dirname, "..");
const args = process.argv.slice(2);
const getArg = (name, fallback) => {
const i = args.indexOf(name);
return i >= 0 && args[i + 1] ? args[i + 1] : fallback;
};
const from = path.resolve(getArg("--from", "/tmp/codex-asar/webview"));
const to = path.resolve(projectRoot, getArg("--to", "original"));
const clean = args.includes("--clean");
if (!fs.existsSync(path.join(from, "index.html"))) {
console.error(`[vendor] not a webview directory: ${from}`);
process.exit(1);
}
if (clean && fs.existsSync(to)) {
console.log(`[vendor] removing ${to}`);
fs.rmSync(to, { recursive: true, force: true });
}
fs.mkdirSync(to, { recursive: true });
console.log(`[vendor] copying ${from} -> ${to}`);
fs.cpSync(from, to, { recursive: true, force: true });
// Manifest (written outside ./original so the vendored tree stays untouched).
function sha256(file) {
return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
}
function countFiles(dir) {
let n = 0;
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
n += entry.isDirectory() ? countFiles(path.join(dir, entry.name)) : 1;
}
return n;
}
const manifest = {
source: from,
vendoredAt: new Date().toISOString(),
fileCount: countFiles(to),
indexHtmlSha256: sha256(path.join(to, "index.html")),
};
fs.writeFileSync(
path.join(projectRoot, "host", "vendor-manifest.json"),
JSON.stringify(manifest, null, 2),
);
console.log(`[vendor] done — ${manifest.fileCount} files`);
console.log(`[vendor] index.html sha256 ${manifest.indexHtmlSha256}`);
+718
View File
@@ -0,0 +1,718 @@
#!/usr/bin/env node
/**
* Local web host for the vendored Codex webview.
*
* `original/` stays byte-identical — every adaptation happens in the HTTP
* response layer, so the vendored tree remains a clean reference.
*
* The desktop renderer is not a plain web app; three pieces of the Electron
* shell are load-bearing and are replaced here:
*
* 1. CSP. The shipped policy is `default-src 'none'` with a connect-src that
* only whitelists `wss://chatgpt.com`. A browser tab could not reach
* anything, and we do not want it to: the policy is rewritten to `'self'`
* so the page is fully local.
* 2. The app host. `connect-app-host` creates a MessageChannel and posts one
* port to the preload, which hands it to the main process. Here the port is
* never used: `__codexLocalHost` answers instead (host/local-host.js).
* 3. Host services. The bootstrap awaits `appHost.services`, which the main
* process resolves with its service layer. Our local host resolves it with
* a proxy onto the real `codex app-server` (host/app-server-bridge.mjs).
*
* Usage:
* node host/web-server.mjs [--port 8095] [--root original]
*/
import { execFileSync } from "node:child_process";
import fs from "node:fs";
import http from "node:http";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { AppServerBridge } from "./app-server-bridge.mjs";
import { serveMockRelay } from "./mock-model.mjs";
import { createProjectlessPaths, readWorkspaceRoot, WORKSPACE_ROOT } from "./projectless.mjs";
const HERE = path.dirname(fileURLToPath(import.meta.url));
const PROJECT = path.resolve(HERE, "..");
const argv = process.argv.slice(2);
const argOf = (name, fallback) => {
const i = argv.indexOf(name);
return i !== -1 && argv[i + 1] ? argv[i + 1] : fallback;
};
const PORT = Number(argOf("--port", process.env.CODEX_WEB_PORT || "8095"));
// --root wins; otherwise CODEX_UI_ORIGINAL_ROOT (set by scripts/run-codex-ui.sh),
// otherwise the in-repo `original/` directory (never committed — see §4).
const ROOT = path.resolve(PROJECT, argOf("--root", process.env.CODEX_UI_ORIGINAL_ROOT || "original"));
if (!fs.existsSync(path.join(ROOT, "index.html"))) {
console.error(`[web] missing index.html under ${ROOT}`);
process.exit(1);
}
const METHODS = JSON.parse(fs.readFileSync(path.join(HERE, "appserver-methods.json"), "utf8"));
const bridge = new AppServerBridge();
// --------------------------------------------------------------- CSP rewrite
// Fully local: nothing outside this origin is reachable. Blob/data stay allowed
// because the app builds workers and assets in memory.
const WEB_CSP = [
"default-src 'self'",
"img-src 'self' data: blob:",
"script-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval' blob:",
"style-src 'self' 'unsafe-inline'",
"font-src 'self' data:",
"connect-src 'self' data: blob:",
"media-src 'self' data: blob:",
"worker-src 'self' blob:",
"frame-src 'self' blob:",
"child-src 'self' blob:",
].join("; ");
const MIME = {
".html": "text/html; charset=utf-8",
".js": "text/javascript; charset=utf-8",
".mjs": "text/javascript; charset=utf-8",
".css": "text/css; charset=utf-8",
".json": "application/json; charset=utf-8",
".svg": "image/svg+xml",
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".webp": "image/webp",
".gif": "image/gif",
".ico": "image/x-icon",
".woff": "font/woff",
".woff2": "font/woff2",
".ttf": "font/ttf",
".otf": "font/otf",
".wasm": "application/wasm",
".map": "application/json; charset=utf-8",
".txt": "text/plain; charset=utf-8",
".wav": "audio/wav",
".mp3": "audio/mpeg",
};
/** Rewrite rules applied on the fly; `original/` on disk is never touched. */
const JS_PATCHES = [
{
name: "local-app-host",
// `connect-app-host` opens a MessageChannel and posts one port to the
// preload, which forwards it to the Electron main process. There is no main
// process here, so answer with the local host instead.
find: /function (\w+)\((\w+)\)\{let\{port1:(\w+),port2:(\w+)\}=new MessageChannel;return window\.postMessage\(\{type:`connect-app-host`/g,
replace: (_m, fn, arg, p1, p2) =>
`function ${fn}(${arg}){if(globalThis.__codexLocalHost)return globalThis.__codexLocalHost(${arg});` +
`let{port1:${p1},port2:${p2}}=new MessageChannel;return window.postMessage({type:\`connect-app-host\``,
},
{
name: "bootstrap-guard",
// The original bootstrap wires up seven main-process facilities: remote
// connection discovery, an execution host, an RPC-broken hook, thread read
// state, Statsig, client coordination and the terminal. Only the first two
// affect the shell; the rest are Electron-only and would abort the whole
// sequence on the first rejection. Keep the parts that matter, guard them,
// and drop the rest.
find: /async function pL\(\)\{[\s\S]*?\}function mL\(\)/g,
replace: () =>
`async function pL(){` +
`const T=(ms)=>new Promise(r=>setTimeout(r,ms));` +
`const guard=async(f,ms)=>{try{return await Promise.race([f(),T(ms)])}catch(e){console.warn("[codex-web] init step skipped:",e&&e.message)}};` +
`await guard(async()=>{$t(kS(lL))},5000);` +
`await guard(async()=>{dr(await Pt.services)},10000);` +
`await guard(async()=>{await ql(H.remoteConnections)},2000);` +
`await guard(async()=>{di(await H.executionHosts?.getDefaultHost())},2000);` +
`globalThis.__codexBootstrapped=!0` +
`}function mL()`,
},
{
name: "skip-shell-gate",
// The shell waits for auth, app-info, os-info and one more readiness signal
// before rendering anything. In the desktop app the main process supplies
// all four; here auth in particular only settles after the app server has
// been asked for an account, which cannot happen until the shell exists.
// Render the shell immediately and let the data arrive underneath it.
find: /if\((\w+)\.isLoading\|\|(\w+)\|\|(\w+)\|\|(\w+)\)\{/g,
replace: (_m, r, a, s, c) =>
`if(console.info("[codex-web] shell gate",JSON.stringify({rLoad:${r}&&${r}.isLoading,a:${a},s:${s},c:${c}})),!1){`,
},
{
name: "mode-probe",
// `Ck` picks the window mode (`app` / `onboarding` / `login`) from a pile of
// readiness signals and falls back to the loading blossom when it comes back
// empty. The guard is a comma expression inside a single `if`, so the anchor
// is the tail of that expression rather than the `if(` itself. Log the
// decision so the missing input is identifiable.
find: /,!(\w+)\|\|(\w+)===\x60loading\x60\)\{/g,
replace: (_m, mode, status) =>
`,console.info("[codex-web] mode",JSON.stringify({mode:${mode},status:${status}})),!${mode}||${status}===\x60loading\x60){`,
},
{
name: "mode-inputs",
// Bind the mode-input object to the global scope on its way into the
// decision function, so a probe can read exactly what the shell decided on.
find: /,(\w+)=(\w+)\(\{bedrockLoginPending:/g,
replace: (_m, out, fn) => `,${out}=${fn}(globalThis.__modeIn={bedrockLoginPending:`,
},
{
name: "mode-decide",
// The window-mode decision gates on two desktop-only concepts:
//
// gatewayOAuthReadiness — an OpenAI gateway handshake that stays `loading`
// forever here because there is no gateway: this build talks to an
// API-key relay, so there is nothing to wait for.
// onboarding completion — the desktop onboarding flow needs a ChatGPT
// account and a backend, neither of which this build has.
//
// Collapse both: honour an explicit "login required", otherwise go straight
// to the app. The relay authenticates with a key, so there is no login step.
find: /(\w+)===\x60loading\x60\?null:\1===\x60required\x60\?\x60login\x60:[\s\S]{0,200}?:\x60welcome\x60\):\x60app\x60\}/g,
replace: (_m, readiness) =>
`${readiness}===\x60required\x60?\x60login\x60:\x60app\x60:\x60app\x60}`,
},
{
name: "config-effective-preseed",
// The scope-query bridge fetches only from mounted React readers. In a
// browser fallback nothing mounts the config/effective query, so `ofo()`
// would read `isPending: true` forever and keep `isAgentModeLoading` true.
// Short-circuit to the preseeded app-server config (host/shim.js).
find: /let\{data:(\w+),isPending:(\w+)\}=ixn\((\w+)===`~`\?null:\3,(\w+)\),/,
replace: (_m, w, T, p, C) =>
`let{data:${w},isPending:${T}}=globalThis.__codexCfg?{data:{config:globalThis.__codexCfg.config,origins:globalThis.__codexCfg.origins??{},layers:null},isPending:!1}:(ixn(${p}===\`~\`?null:${p},${C}),{data:void 0,isPending:!0}),`,
},
{
name: "permission-profiles-preseed",
// Same mount requirement for the permission-profiles query read inside the
// agent-mode hook (`sfo`); short-circuit to the preseeded profile list.
find: /let (\w+)=ns\(kdo,(\w+),(\w+)\),(\w+)=\w+\|\|\w+\|\|\1\.isPending,(\w+)=\4\|\|\1\.isError,/,
replace: (_m, O, T2, D2, k, A) =>
`let ${O}=globalThis.__codexPerms?{data:globalThis.__codexPerms,isPending:!1,isError:!1}:ns(kdo,${T2},${D2}),${k}=h||g||${O}.isPending,${A}=${k}||${O}.isError,`,
},
{
name: "guardian-off",
// The guardian-approvals feature flag derives from a Statsig dynamic
// config the desktop main process evaluates; with a null evaluations
// payload (this build has no ChatGPT account) the guard reads as
// "unavailable" and `ofo()` treats guardian mode as pending forever.
// This build has no guardian capability at all, so pin it off.
find: /N=(\w+)&&oee\(\w+\?\?void 0\)!==!1&&\(\w+===void 0\|\|\w+\),/,
replace: (_m, e) => `N=!1,`,
},
{
name: "statsig-bootstrap-local",
// The renderer fetches its Statsig bootstrap from the ChatGPT backend
// (`POST /wham/statsig/bootstrap`). Without a ChatGPT token that request
// is rejected before it is ever sent, and the SDK retries forever with
// "SDK initialization has not completed" warnings. Serve the bootstrap
// from the local host instead (see /__codex_statsig_bootstrap).
find: /function (\w+)\((\w+),(\w+)\)\{return (\w+)\.safePost\(`\/wham\/statsig\/bootstrap`,\{requestBody:\2,retry:!1,signal:\3\}\)\}/g,
replace: (_m, fn, reqBody, signal) =>
`async function ${fn}(${reqBody},${signal}){` +
`const r=await fetch("/__codex_statsig_bootstrap",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(${reqBody}??{}),signal:${signal}});` +
`return r.json()}`,
},
{
name: "expose-app-host",
// setAppHost / setAppServices only assign module-scoped variables, which
// makes the wiring invisible from the outside. Mirror them for diagnostics.
find: /function hSt\((\w+)\)\{_St=\1\}/g,
replace: (_m, v) => `function hSt(${v}){_St=${v};globalThis.__appHost=${v}}`,
},
{
name: "expose-app-services",
find: /function gSt\((\w+)\)\{f\$=\1\}/g,
replace: (_m, v) => `function gSt(${v}){f$=${v};globalThis.__appServices=${v}}`,
},
];
function patchJs(source) {
let out = source;
const applied = [];
for (const rule of JS_PATCHES) {
if (!rule.find.test(out)) continue;
rule.find.lastIndex = 0;
out = out.replace(rule.find, rule.replace);
applied.push(rule.name);
}
return { out, applied };
}
function patchHtml(source) {
let out = source;
out = out.replace(
/<meta http-equiv="Content-Security-Policy" content="[\s\S]*?"\s*\/?>/i,
`<meta http-equiv="Content-Security-Policy" content="${WEB_CSP.replace(/"/g, "&quot;")}" />`,
);
// Both shims must run before the module entry.
if (!out.includes("__codex_shim.js")) {
out = out.replace(
/<script type="module"/i,
`<script src="/__codex_local_host.js"></script>\n <script src="/__codex_shim.js"></script>\n <script type="module"`,
);
}
return out;
}
const LOCAL_HOST_JS = fs
.readFileSync(path.join(HERE, "local-host.js"), "utf8")
.replace("/*@METHODS@*/ []", JSON.stringify(METHODS));
const SHIM = fs.readFileSync(path.join(HERE, "shim.js"), "utf8");
// ------------------------------------------------------------- mock backend
// Local stand-in for the ChatGPT backend. The app server runs with
// chatgpt_base_url pointed at /mock, so every call that would reach OpenAI's
// cloud — plugin catalog refresh, account endpoints, telemetry — is answered
// from this process with well-formed local JSON. Nothing leaves the machine
// and no caller ever sees a connection error.
const MOCK_LOG = [];
const nowIso = () => new Date().toISOString();
/** Valid, empty Statsig initialize payload — every gate resolves to "off". */
const statsigBootstrapPayload = (stableId = "codex-web-local") => ({
has_updates: true,
hash_used: "djb2",
time: Date.now(),
feature_gates: {},
dynamic_configs: {},
layer_configs: {},
param_stores: {},
user: {
userID: null,
email: null,
custom: { auth_method: null },
customIDs: { stableID: stableId, WebAnonymousCookieID: stableId, DeviceId: stableId },
},
});
/** A remote-catalog plugin descriptor the renderer can render without data. */
const mockRemotePluginRead = (pluginName) => ({
plugin: {
summary: {
id: pluginName,
name: String(pluginName).replace(/^plugin_[^_]*_/, ""),
remotePluginId: pluginName,
version: null,
installed: false,
enabled: false,
installPolicy: "NOT_AVAILABLE",
installPolicySource: null,
mustShowInstallationInterstitial: false,
authPolicy: "ON_USE",
availability: "NOT_AVAILABLE",
disabledReason: "unavailable_offline",
interface: {
displayName: pluginName,
shortDescription: "Cloud plugin (offline build)",
longDescription: "This plugin belongs to the ChatGPT cloud catalog and is not available in a fully local build.",
developerName: null,
category: null,
capabilities: [],
websiteUrl: null,
privacyPolicyUrl: null,
termsOfServiceUrl: null,
defaultPrompt: [],
brandColor: "#6B7280",
composerIcon: null,
composerIconUrl: null,
logo: null,
logoDark: null,
logoUrl: null,
logoUrlDark: null,
screenshots: [],
screenshotUrls: [],
},
keywords: [],
},
skills: [],
onboardingSkill: null,
},
});
const serveMock = (req, res, urlPath) => {
let search = "";
try {
search = new URL(req.url, "http://localhost").search;
} catch {
/* keep empty */
}
MOCK_LOG.push(`${req.method} ${urlPath}${search}`);
if (MOCK_LOG.length > 200) MOCK_LOG.shift();
const json = (obj, status = 200) => {
res.writeHead(status, { "content-type": MIME[".json"], "cache-control": "no-store" });
res.end(JSON.stringify(obj));
};
const p = urlPath.replace(/^\/mock/, "") || "/";
// Cloud MCP endpoint (ChatGPT-hosted app connectors). Answer 404 so the app
// server marks these connectors unavailable immediately — with a semantically
// empty 200 it would retry and block thread startup on MCP enumeration.
if (/^\/api\/codex\/ps\/mcp/.test(p)) {
res.writeHead(404, { "content-type": MIME[".json"], "cache-control": "no-store" });
res.end(JSON.stringify({ error: { message: "cloud connectors are unavailable in a local build" } }));
return;
}
// ---- known wham (ChatGPT backend) shapes -------------------------------
// Cloud tasks: the renderer dereferences `.items` and paginates on `.cursor`.
if (/^\/wham\/tasks\/list/.test(p)) {
json({ items: [], cursor: null });
return;
}
// Statsig bootstrap: `{ statsigPayload: <string> }`, exactly as the desktop
// main process hands it over.
if (/^\/wham\/statsig\/bootstrap/.test(p)) {
json({ statsigPayload: JSON.stringify(statsigBootstrapPayload()) });
return;
}
// Plugin catalog refresh: the app server persists this under
// ~/.codex/cache/remote_plugin_catalog. The truthful local answer is an
// empty catalog — no cloud plugins exist in this build.
if (/plugin/i.test(p)) {
json({ schema_version: 1, fetched_at: nowIso(), plugins: [] });
return;
}
// Generic empty-but-valid JSON: callers parse it, find nothing to do, and
// move on without errors.
json({});
};
// ------------------------------------------------------- mock model provider
// The model wire lives in ./mock-model.mjs so the local model is defined once
// and every caller gets the identical Responses-API stream.
const logMockRequest = (line) => {
MOCK_LOG.push(line);
if (MOCK_LOG.length > 200) MOCK_LOG.shift();
};
// ------------------------------------------------------------------- server
// Read-only git ops accepted by POST /__codex_git (A7). Anything else is
// rejected; there is intentionally no write op in this set.
const GIT_OPS = new Set(["stable-metadata", "branch", "status"]);
const readJsonBody = (req) =>
new Promise((resolve, reject) => {
let raw = "";
req.setEncoding("utf8");
req.on("data", (c) => {
raw += c;
if (raw.length > 4_000_000) reject(new Error("body too large"));
});
req.on("end", () => {
try {
resolve(raw ? JSON.parse(raw) : {});
} catch (err) {
reject(err);
}
});
req.on("error", reject);
});
const server = http.createServer(async (req, res) => {
let urlPath;
try {
urlPath = decodeURIComponent(new URL(req.url, "http://localhost").pathname);
} catch {
res.writeHead(400).end("bad request");
return;
}
if (urlPath === "/__codex_shim.js") {
res.writeHead(200, { "content-type": MIME[".js"], "cache-control": "no-store" });
res.end(SHIM);
return;
}
if (urlPath === "/__codex_local_host.js") {
res.writeHead(200, { "content-type": MIME[".js"], "cache-control": "no-store" });
res.end(LOCAL_HOST_JS);
return;
}
// Filesystem work the Electron main process would do: allocate a directory for
// a conversation that is not attached to a project.
if (urlPath === "/__codex_projectless") {
if (req.method !== "POST") {
res.writeHead(405).end("method not allowed");
return;
}
try {
const { op, args } = await readJsonBody(req);
const result = op === "readWorkspaceRoot" ? readWorkspaceRoot() : createProjectlessPaths(args ?? {});
res.writeHead(200, { "content-type": MIME[".json"], "cache-control": "no-store" });
res.end(JSON.stringify({ result }));
} catch (err) {
res.writeHead(200, { "content-type": MIME[".json"], "cache-control": "no-store" });
res.end(JSON.stringify({ error: { message: err?.message ?? "workspace error" } }));
}
return;
}
// Read-only git (A7). The renderer gates the composer on
// `git.request("stable-metadata")`, which the desktop main process answers.
// Only three read-only ops are accepted; write operations cannot be
// expressed, and no caller-supplied path ever reaches argv — `cwd` is
// passed to execFile as the working-directory option only, and every git
// invocation runs strictly inside that directory. A missing cwd or a
// non-repository cwd gets a structured error, not a silent stub.
if (urlPath === "/__codex_git") {
if (req.method !== "POST") {
res.writeHead(405).end("method not allowed");
return;
}
const gitError = (message) => {
res.writeHead(200, { "content-type": MIME[".json"], "cache-control": "no-store" });
res.end(JSON.stringify({ error: { message } }));
};
try {
const { op, cwd } = await readJsonBody(req);
if (!GIT_OPS.has(op)) {
gitError(`unsupported git op: ${String(op)}`);
return;
}
if (typeof cwd !== "string" || !cwd) {
gitError("git request needs a cwd");
return;
}
const dir = path.resolve(cwd);
let st;
try {
st = fs.statSync(dir);
} catch {
gitError(`cwd does not exist: ${dir}`);
return;
}
if (!st.isDirectory()) {
gitError(`cwd is not a directory: ${dir}`);
return;
}
const runGit = (args) => execFileSync("git", args, { cwd: dir, encoding: "utf8", timeout: 10_000 }).trim();
let toplevel;
try {
toplevel = runGit(["rev-parse", "--show-toplevel"]);
} catch {
gitError(`not a git repository: ${dir}`);
return;
}
let branch = null;
try {
const raw = runGit(["rev-parse", "--abbrev-ref", "HEAD"]);
branch = raw === "HEAD" ? null : raw; // detached HEAD, or no commits yet
} catch {
branch = null;
}
let upstream = null;
try {
upstream = runGit(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"]);
} catch {
/* no upstream configured */
}
const porcelain = runGit(["status", "--porcelain=v1"]);
const clean = porcelain === "";
// stable-metadata keeps the six fields the old stub returned, now
// populated for real; branch/status return the subset they need.
const result =
op === "branch"
? { branch, upstream }
: op === "status"
? { branch, upstream, clean, files: clean ? [] : porcelain.split("\n") }
: {
available: true,
branch,
upstream,
repository: toplevel,
state: clean ? "clean" : "dirty",
worktree: toplevel,
};
res.writeHead(200, { "content-type": MIME[".json"], "cache-control": "no-store" });
res.end(JSON.stringify({ result }));
} catch (err) {
gitError(err?.message ?? "git error");
}
return;
}
// Diagnostic: what has the app server asked the mock backend for?
if (urlPath === "/__codex_mocklog") {
res.writeHead(200, { "content-type": MIME[".json"], "cache-control": "no-store" });
res.end(JSON.stringify({ requests: MOCK_LOG }));
return;
}
// Statsig bootstrap. The renderer normally POSTs /wham/statsig/bootstrap to
// the ChatGPT backend; the `statsig-bootstrap-local` patch redirects it here.
// The response is a valid, empty initialize payload — every gate resolves to
// "off" and the SDK completes initialization instead of retrying forever.
if (urlPath === "/__codex_statsig_bootstrap") {
MOCK_LOG.push(`POST ${urlPath} (statsig bootstrap)`);
if (MOCK_LOG.length > 200) MOCK_LOG.shift();
if (req.method !== "POST") {
res.writeHead(405).end("method not allowed");
return;
}
let body = {};
try {
body = await readJsonBody(req);
} catch {
/* empty request body is fine */
}
const stableId = typeof body.stable_id === "string" ? body.stable_id : "codex-web-local";
res.writeHead(200, { "content-type": MIME[".json"], "cache-control": "no-store" });
// The renderer expects `{ statsigPayload: <string> }` — the payload as a
// JSON string, exactly as the desktop main process hands it over.
res.end(JSON.stringify({ statsigPayload: JSON.stringify(statsigBootstrapPayload(stableId)) }));
return;
}
// Same-origin Statsig event flush: the renderer posts to /ces/v1/rgstr, which
// lands here. Accept and drop — nothing is measured, nothing leaves.
if (urlPath.startsWith("/ces/")) {
res.writeHead(202, { "content-type": MIME[".json"], "cache-control": "no-store" });
res.end("{}");
return;
}
// Mock model provider (see ./mock-model.mjs). The app server's
// model_provider points here, so `turn/start` completions are answered from
// this process with a well-formed Responses-API SSE stream. Nothing reaches
// the network and the reply renders exactly as a live model's would.
if (urlPath === "/mockrelay" || urlPath.startsWith("/mockrelay/")) {
serveMockRelay(req, res, urlPath, { onRequest: logMockRequest });
return;
}
// Mock ChatGPT backend (see serveMock above).
if (urlPath === "/mock" || urlPath.startsWith("/mock/")) {
serveMock(req, res, urlPath);
return;
}
// Event stream. Everything the app server emits on its own initiative —
// streaming deltas, turn progress, server-initiated approval requests — comes
// back through here. Long-polled so the renderer stays in step without a
// websocket.
if (urlPath === "/__codex_events") {
try {
await bridge.ensure();
const messages = await bridge.waitForNotifications(15_000);
res.writeHead(200, { "content-type": MIME[".json"], "cache-control": "no-store" });
res.end(JSON.stringify({ events: messages }));
} catch (err) {
res.writeHead(200, { "content-type": MIME[".json"], "cache-control": "no-store" });
res.end(JSON.stringify({ events: [], error: String(err?.message ?? err) }));
}
return;
}
// Local JSON-RPC passthrough to the real codex app server.
if (urlPath === "/__codex_rpc") {
if (req.method !== "POST") {
res.writeHead(405).end("method not allowed");
return;
}
let method, params;
try {
({ method, params } = await readJsonBody(req));
if (typeof method !== "string") {
res.writeHead(400, { "content-type": MIME[".json"] }).end('{"error":{"message":"missing method"}}');
return;
}
// The desktop main process strips `originator` filtering before it
// forwards `thread/list` to the app server — a local app-server rejects
// the parameter with -32602. Mirror the sanitisation here.
if (method === "thread/list" && params && typeof params === "object") {
const strip = (obj) => {
if (!obj || typeof obj !== "object") return;
for (const key of Object.keys(obj)) {
if (key === "originator" || key === "originators") delete obj[key];
else if (obj[key] && typeof obj[key] === "object") strip(obj[key]);
}
};
strip(params);
}
await bridge.ensure();
const result = await bridge.request(method, params);
res.writeHead(200, { "content-type": MIME[".json"], "cache-control": "no-store" });
res.end(JSON.stringify({ result: result ?? null }));
} catch (err) {
// A cloud-catalog plugin cannot be fetched in a local build; answer with
// a well-formed offline descriptor instead of an error the renderer
// would surface as a toast.
if (method === "plugin/read" && params && typeof params.pluginName === "string") {
res.writeHead(200, { "content-type": MIME[".json"], "cache-control": "no-store" });
res.end(JSON.stringify({ result: mockRemotePluginRead(params.pluginName) }));
return;
}
const message = err?.rpc?.message ?? err?.message ?? "rpc failed";
res.writeHead(200, { "content-type": MIME[".json"], "cache-control": "no-store" });
res.end(JSON.stringify({ error: { code: err?.rpc?.code ?? -32000, message } }));
}
return;
}
const rel = urlPath === "/" ? "index.html" : urlPath.replace(/^\/+/, "");
const abs = path.join(ROOT, rel);
if (!abs.startsWith(ROOT)) {
res.writeHead(403).end("forbidden");
return;
}
let file = abs;
if (!fs.existsSync(file) || fs.statSync(file).isDirectory()) {
file = path.join(ROOT, "index.html"); // SPA fallback
if (!fs.existsSync(file)) {
res.writeHead(404).end("not found");
return;
}
}
const ext = path.extname(file).toLowerCase();
const type = MIME[ext] || "application/octet-stream";
try {
let body = fs.readFileSync(file);
let patched = null;
if (ext === ".html") {
body = Buffer.from(patchHtml(body.toString("utf8")), "utf8");
} else if (ext === ".js") {
const { out, applied } = patchJs(body.toString("utf8"));
if (applied.length) {
body = Buffer.from(out, "utf8");
patched = applied.join(",");
}
}
res.writeHead(200, {
"content-type": type,
"cache-control": "no-store",
...(patched ? { "x-codex-patched": patched } : {}),
});
res.end(body);
} catch (err) {
console.error("[web] read error", file, err.message);
res.writeHead(500).end("read error");
}
});
server.listen(PORT, "127.0.0.1", () => {
console.log(`[web] Codex webview served from ${ROOT}`);
console.log(`[web] http://127.0.0.1:${PORT}/`);
console.log(`[web] app server: ${bridge.binary ?? "not found"}`);
console.log(`[web] workspace root: ${WORKSPACE_ROOT}`);
console.log(`[web] mock backend: http://127.0.0.1:${PORT}/mock`);
});
// A long-running local host must survive its own bugs: log loudly, keep
// serving. Without this one bad request would take the whole app down.
process.on("uncaughtException", (err) => {
console.error("[web] uncaught:", err?.stack ?? err);
});
process.on("unhandledRejection", (err) => {
console.error("[web] unhandled rejection:", err?.stack ?? err);
});
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
# Start the standalone original-UI web host in development mode.
#
# ./start.sh UI :8095
# UI_PORT=9000 ./start.sh custom port
# node verify.mjs --deep run the self-check in another shell
#
# The app-server binary ships inside ChatGPT.app; set CODEX_APP_SERVER_BIN
# if it lives elsewhere. There is no orchestrator/API layer in this stack.
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
UI_PORT="${UI_PORT:-8095}"
export CODEX_WEB_PORT="$UI_PORT"
echo "[stack] UI http://127.0.0.1:${UI_PORT}/"
echo "[stack] test node ${ROOT}/verify.mjs --deep"
exec node "$ROOT/host/web-server.mjs" --port "$UI_PORT"
+160
View File
@@ -0,0 +1,160 @@
#!/usr/bin/env node
/**
* Self-check for the standalone original-UI web host. Run while ./start.sh is up.
* node verify.mjs served assets + HTTP surface
* node verify.mjs --deep also drive a real app-server RPC through the UI host
*
* The orchestrator/API layer is intentionally not part of this stack; nothing
* here probes port 8899. Everything the UI needs is served by host/web-server.mjs
* on UI_PORT, including the local JSON-RPC passthrough and the mock backends.
*/
import http from "node:http";
const uiPort = Number(process.env.UI_PORT || 8095);
const deep = process.argv.includes("--deep");
function request(path, { method = "GET", headers = {}, body = null, timeout = 8000 } = {}) {
return new Promise((resolve) => {
const req = http.request({ host: "127.0.0.1", port: uiPort, path, method, headers }, (res) => {
const chunks = [];
res.on("data", (c) => chunks.push(c));
res.on("end", () => resolve({
status: res.statusCode,
body: Buffer.concat(chunks).toString("utf8"),
}));
});
req.setTimeout(timeout, () => req.destroy(new Error("timeout")));
req.on("error", (e) => resolve({ status: 0, body: String(e.message) }));
if (body) req.write(body);
req.end();
});
}
function json(r) {
try { return JSON.parse(r.body); } catch { return null; }
}
const results = [];
async function check(name, fn) {
let out;
try {
out = await fn();
} catch (e) {
out = { ok: false, detail: String(e.message) };
}
results.push({ name, ...out });
console.log(`${out.ok ? "PASS" : "FAIL"} ${name} ${out.detail ?? ""}`);
}
const post = (path, payload, timeout = 8000) => request(path, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(payload),
timeout,
});
await check("UI GET / 注入两个宿主脚本", async () => {
const r = await request("/");
const ok = r.status === 200
&& r.body.includes("/__codex_local_host.js")
&& r.body.includes("/__codex_shim.js");
return { ok, detail: `HTTP ${r.status}, ${r.body.length}B` };
});
await check("UI GET /__codex_local_host.js", async () => {
const r = await request("/__codex_local_host.js");
return {
ok: r.status === 200 && r.body.includes("__codexLocalHost"),
detail: `HTTP ${r.status}, ${r.body.length}B`,
};
});
await check("UI GET /__codex_shim.js", async () => {
const r = await request("/__codex_shim.js");
return { ok: r.status === 200 && r.body.length > 1000, detail: `HTTP ${r.status}, ${r.body.length}B` };
});
await check("UI 响应层补丁命中 connect-app-host", async () => {
const r = await request("/assets/connect-app-host-90a50ca9e4b4.js");
return {
ok: r.status === 200 && r.body.includes("__codexLocalHost"),
detail: `HTTP ${r.status}, ${r.body.length}B`,
};
});
await check("UI 原版入口 chunk 可加载", async () => {
const r = await request("/assets/index-fcd3fcef6840.js");
return { ok: r.status === 200 && r.body.length > 100, detail: `HTTP ${r.status}, ${r.body.length}B` };
});
await check("UI GET /ces/v1/rgstr 遥测丢弃", async () => {
const r = await request("/ces/v1/rgstr");
return { ok: r.status === 202, detail: `HTTP ${r.status}` };
});
await check("UI GET /__codex_rpc 拒绝非 POST", async () => {
const r = await request("/__codex_rpc");
return { ok: r.status === 405, detail: `HTTP ${r.status}` };
});
await check("UI POST /__codex_projectless 工作区根", async () => {
const r = await post("/__codex_projectless", { op: "readWorkspaceRoot" });
const j = json(r);
return {
ok: r.status === 200 && typeof j?.result?.workspaceRoot === "string",
detail: `HTTP ${r.status}, ${j?.result?.workspaceRoot ?? r.body.slice(0, 80)}`,
};
});
await check("UI POST /__codex_statsig_bootstrap 空配置", async () => {
const r = await post("/__codex_statsig_bootstrap", { stable_id: "verify" });
const j = json(r);
const gates = json({ body: j?.statsigPayload ?? "" })?.feature_gates;
return {
ok: r.status === 200 && gates && Object.keys(gates).length === 0,
detail: `HTTP ${r.status}, ${r.body.length}B`,
};
});
await check("UI GET /mock/wham/tasks/list 云端替身", async () => {
const r = await request("/mock/wham/tasks/list");
const j = json(r);
return {
ok: r.status === 200 && Array.isArray(j?.items) && j.items.length === 0,
detail: `HTTP ${r.status}, ${r.body.trim()}`,
};
});
await check("UI GET /__codex_mocklog 诊断日志", async () => {
const r = await request("/__codex_mocklog");
const j = json(r);
return {
ok: r.status === 200 && Array.isArray(j?.requests),
detail: `HTTP ${r.status}, ${j?.requests?.length ?? 0} 条`,
};
});
if (deep) {
await check("UI POST /__codex_rpc -> 真实 app-server thread/list", async () => {
const r = await post("/__codex_rpc", { method: "thread/list", params: { limit: 1 } }, 45000);
const j = json(r);
const ok = r.status === 200 && j && !j.error && j.result != null;
return { ok: !!ok, detail: `HTTP ${r.status}, ${r.body.slice(0, 140)}` };
});
await check("UI POST /__codex_rpc -> config/read 含 relay 覆盖", async () => {
const r = await post("/__codex_rpc", { method: "config/read", params: {} }, 45000);
const j = json(r);
const model = j?.result?.config?.model;
return {
ok: r.status === 200 && typeof model === "string" && model.length > 0,
detail: `HTTP ${r.status}, model=${model ?? "n/a"}`,
};
});
}
const failed = results.filter((r) => !r.ok);
console.log(`\n${results.length - failed.length}/${results.length} passed`);
console.log("orchestrator 已移除;本栈只有 :8095 一个服务,无 :8899 API。");
if (!deep) console.log("shallow run: pass --deep to exercise the real app-server RPC");
process.exit(failed.length ? 1 : 0);
@@ -9,7 +9,6 @@ export default function DispatchSection() {
const [prompt, setPrompt] = useState(""); const [prompt, setPrompt] = useState("");
const [coordParts, setCoordParts] = useState(""); const [coordParts, setCoordParts] = useState("");
const [goalMode, setGoalMode] = useState(false); const [goalMode, setGoalMode] = useState(false);
const [goalInvincible, setGoalInvincible] = useState(false);
const [planMode, setPlanMode] = useState(false); const [planMode, setPlanMode] = useState(false);
const [executor, setExecutor] = useState("docker"); const [executor, setExecutor] = useState("docker");
const [model, setModel] = useState(""); const [model, setModel] = useState("");
@@ -45,19 +44,18 @@ export default function DispatchSection() {
} }
if (planMode) { if (planMode) {
if (approval === "full") { setMsg("计划模式不能与完全访问同用"); return; } if (approval === "full") { setMsg("计划模式不能与完全访问同用"); return; }
if (goalMode || goalInvincible) { setMsg("计划模式不能与 goal 模式同用"); return; } if (goalMode) { setMsg("计划模式不能与 goal 模式同用"); return; }
body.plan = 1; body.plan = 1;
} }
// 无敌版 = goal 模式 + 任务不限时:勾了它就同时发 goal_mode 与 // 唯一 goal 模式 = 原无敌版:勾选即同时发 goal_mode 与
// no_time_limit(服务端已支持该字段);与普通 goal 勾选不冲突, // no_time_limit(任务不限时);普通 goal 入口已取消,不再
// 无敌版优先,单独勾普通 goal 时行为不变。 // 单独发不带 no_time_limit 的 goal_mode。
if (goalInvincible) { body.goal_mode = 1; body.no_time_limit = 1; } if (goalMode) { body.goal_mode = 1; body.no_time_limit = 1; }
else if (goalMode) body.goal_mode = 1;
const coordRaw = coordParts.trim(); const coordRaw = coordParts.trim();
if (coordRaw) { if (coordRaw) {
try { body.coordinator_parts = JSON.parse(coordRaw); } catch { setMsg("协调员子任务 JSON 解析失败"); return; } try { body.coordinator_parts = JSON.parse(coordRaw); } catch { setMsg("协调员子任务 JSON 解析失败"); return; }
if (!Array.isArray(body.coordinator_parts)) { setMsg("协调员子任务必须是数组"); return; } if (!Array.isArray(body.coordinator_parts)) { setMsg("协调员子任务必须是数组"); return; }
if (goalMode || goalInvincible || planMode) { setMsg("协调员任务不能与 goal/计划模式同用:协调员任务本身不跑模型回合,请取消 goal/计划勾选,或清空协调员 JSON"); 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 = gatherAttachments(filesRef.current?.files || null, dirRef.current?.files || null, imgRef.current?.files || null);
try { try {
@@ -81,7 +79,6 @@ export default function DispatchSection() {
<option value="docker">docker(默认,多用户下隔离最强)</option><option value="local">local</option><option value="cloud">cloud</option> <option value="docker">docker(默认,多用户下隔离最强)</option><option value="local">local</option><option value="cloud">cloud</option>
</select> </select>
<label><input type="checkbox" checked={goalMode} onChange={(e) => setGoalMode(e.target.checked)} /> goal 模式</label> <label><input type="checkbox" checked={goalMode} onChange={(e) => setGoalMode(e.target.checked)} /> goal 模式</label>
<label><input type="checkbox" checked={goalInvincible} onChange={(e) => setGoalInvincible(e.target.checked)} /> goal 模式无敌版</label>
<label><input type="checkbox" checked={planMode} onChange={(e) => setPlanMode(e.target.checked)} /> 计划模式(只读)</label> <label><input type="checkbox" checked={planMode} onChange={(e) => setPlanMode(e.target.checked)} /> 计划模式(只读)</label>
</Bar> </Bar>
<textarea className="prompt stacked" placeholder="任务 prompt(必填)" value={prompt} onChange={(e) => setPrompt(e.target.value)} /> <textarea className="prompt stacked" placeholder="任务 prompt(必填)" value={prompt} onChange={(e) => setPrompt(e.target.value)} />
@@ -113,8 +110,7 @@ export default function DispatchSection() {
</select> </select>
</Bar> </Bar>
<div className="muted">子任务模型/强度是派生默认:本任务用 marker task-create 或协调员分工派生的子任务,未显式写模型/强度时用它;块/分工里显式写了则覆盖它。也可以不在这里填、直接在 prompt 里写“子任务用 gpt-6-luna max”,建任务时服务端会解析成同一对字段(任务详情里可核对)。</div> <div className="muted">子任务模型/强度是派生默认:本任务用 marker task-create 或协调员分工派生的子任务,未显式写模型/强度时用它;块/分工里显式写了则覆盖它。也可以不在这里填、直接在 prompt 里写“子任务用 gpt-6-luna max”,建任务时服务端会解析成同一对字段(任务详情里可核对)。</div>
{goalMode ? <div className="muted">goal 模式:输入框内容即目标,不需要专用 prompt 与验收命令;不限轮次,模型确认达成后在回复末尾输出“目标已达成”即结束,未达成会一直运行,可用取消停止。</div> : null} {goalMode ? <div className="muted">goal 模式:输入框内容即目标,不需要专用 prompt 与验收命令;不限轮次,模型确认达成后在回复末尾输出“目标已达成”即结束,未达成会一直运行,可用取消停止;任务不限时(去掉 worker/docker 总超时;单轮 600 秒护栏还在)。服务端默认的自动续跑与断点镜像同时生效。</div> : null}
{goalInvincible ? <div className="muted">goal 模式无敌版:goal 模式 + 任务不限时(去掉 worker/docker 总超时;单轮 600 秒护栏还在,不限的是任务总时长与总轮次)。服务端默认的自动续跑与断点镜像同时生效。</div> : null}
<Bar> <Bar>
<span className="muted">附件(放进任务工作区,图片与普通文件不区分):</span> <span className="muted">附件(放进任务工作区,图片与普通文件不区分):</span>
<input ref={filesRef} type="file" multiple title="文件" /> <input ref={filesRef} type="file" multiple title="文件" />
@@ -60,7 +60,7 @@ export default function TaskDetailPanel() {
const fc = firstChar(t); const fc = firstChar(t);
const cost = ctx.costSummary; const cost = ctx.costSummary;
const modeText = t.plan ? "计划模式(只读)" const modeText = t.plan ? "计划模式(只读)"
: t.goal_mode ? (t.no_time_limit ? "goal 模式无敌版(不限时)" : "goal 模式(不限轮次,模型宣告完成)") : t.goal_mode ? (t.no_time_limit ? "goal 模式(不限轮次,模型宣告完成;任务不限时)" : "goal 模式(不限轮次,模型宣告完成)")
: t.verify_command ? "goal 模式(旧语义,验收命令)" : null; : t.verify_command ? "goal 模式(旧语义,验收命令)" : null;
return ( return (
<div className="detail-stack" id="nav-detail"> <div className="detail-stack" id="nav-detail">
+28 -28
View File
@@ -4,7 +4,7 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Codex 命令台</title> <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 M of C.addedNodes)M.tagName==="LINK"&&M.rel==="modulepreload"&&f(M)}).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 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={};/**
* @license React * @license React
* react-jsx-runtime.production.js * react-jsx-runtime.production.js
* *
@@ -12,7 +12,7 @@
* *
* This source code is licensed under the MIT license found in the * This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree. * 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 M=null;if(C!==void 0&&(M=""+C),y.key!==void 0&&(M=""+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:M,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:{}},yt={};/** */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={};/**
* @license React * @license React
* react.production.js * react.production.js
* *
@@ -20,7 +20,7 @@
* *
* This source code is licensed under the MIT license found in the * This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/var wh;function qy(){if(wh)return yt;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"),M=Symbol.for("react.context"),S=Symbol.for("react.forward_ref"),_=Symbol.for("react.suspense"),O=Symbol.for("react.memo"),Y=Symbol.for("react.lazy"),x=Symbol.for("react.activity"),H=Symbol.for("react.view_transition"),L=Symbol.iterator;function tt(g){return g===null||typeof g!="object"?null:(g=L&&g[L]||g["@@iterator"],typeof g=="function"?g:null)}var dt={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},J=Object.assign,gt={};function _t(g,U,ct){this.props=g,this.context=U,this.refs=gt,this.updater=ct||dt}_t.prototype.isReactComponent={},_t.prototype.setState=function(g,U){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,U,"setState")},_t.prototype.forceUpdate=function(g){this.updater.enqueueForceUpdate(this,g,"forceUpdate")};function Dt(){}Dt.prototype=_t.prototype;function Q(g,U,ct){this.props=g,this.context=U,this.refs=gt,this.updater=ct||dt}var ft=Q.prototype=new Dt;ft.constructor=Q,J(ft,_t.prototype),ft.isPureReactComponent=!0;var D=Array.isArray;function X(){}var et={H:null,A:null,T:null,S:null},xt=Object.prototype.hasOwnProperty;function Mt(g,U,ct){var it=ct.ref;return{$$typeof:i,type:g,key:U,ref:it!==void 0?it:null,props:ct}}function mt(g,U){return Mt(g.type,U,g.props)}function Nt(g){return typeof g=="object"&&g!==null&&g.$$typeof===i}function B(g){var U={"=":"=0",":":"=2"};return"$"+g.replace(/[=:]/g,function(ct){return U[ct]})}var nt=/\/+/g;function rt(g,U){return typeof g=="object"&&g!==null&&g.key!=null?B(""+g.key):U.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(U){g.status==="pending"&&(g.status="fulfilled",g.value=U)},function(U){g.status==="pending"&&(g.status="rejected",g.reason=U)})),g.status){case"fulfilled":return g.value;case"rejected":throw g.reason}}throw g}function A(g,U,ct,it,Ot){var Et=typeof g;(Et==="undefined"||Et==="boolean")&&(g=null);var kt=!1;if(g===null)kt=!0;else switch(Et){case"bigint":case"string":case"number":kt=!0;break;case"object":switch(g.$$typeof){case i:case r:kt=!0;break;case Y:return kt=g._init,A(kt(g._payload),U,ct,it,Ot)}}if(kt)return Ot=Ot(g),kt=it===""?"."+rt(g,0):it,D(Ot)?(ct="",kt!=null&&(ct=kt.replace(nt,"$&/")+"/"),A(Ot,U,ct,"",function(se){return se})):Ot!=null&&(Nt(Ot)&&(Ot=mt(Ot,ct+(Ot.key==null||g&&g.key===Ot.key?"":(""+Ot.key).replace(nt,"$&/")+"/")+kt)),U.push(Ot)),1;kt=0;var P=it===""?".":it+":";if(D(g))for(var ht=0;ht<g.length;ht++)it=g[ht],Et=P+rt(it,ht),kt+=A(it,U,ct,Et,Ot);else if(ht=tt(g),typeof ht=="function")for(g=ht.call(g),ht=0;!(it=g.next()).done;)it=it.value,Et=P+rt(it,ht++),kt+=A(it,U,ct,Et,Ot);else if(Et==="object"){if(typeof g.then=="function")return A(G(g),U,ct,it,Ot);throw U=String(g),Error("Objects are not valid as a React child (found: "+(U==="[object Object]"?"object with keys {"+Object.keys(g).join(", ")+"}":U)+"). If you meant to render a collection of children, use an array instead.")}return kt}function ut(g,U,ct){if(g==null)return g;var it=[],Ot=0;return A(g,it,"","",function(Et){return U.call(ct,Et,Ot++)}),it}function bt(g){if(g._status===-1){var U=g._result,ct=U();ct.then(function(it){(g._status===0||g._status===-1)&&(g._status=1,g._result=it,ct.status===void 0&&(ct.status="fulfilled",ct.value=it))},function(it){(g._status===0||g._status===-1)&&(g._status=2,g._result=it,ct.status===void 0&&(ct.status="rejected",ct.reason=it))}),g._status===-1&&(g._status=0,g._result=ct)}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 U=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(U))return}else if(typeof process=="object"&&typeof process.emit=="function"){process.emit("uncaughtException",g);return}console.error(g)};function Se(g){var U=et.T,ct={};ct.types=U!==null?U.types:null,et.T=ct;try{var it=g(),Ot=et.S;Ot!==null&&Ot(ct,it),typeof it=="object"&&it!==null&&typeof it.then=="function"&&it.then(X,pt)}catch(Et){pt(Et)}finally{U!==null&&ct.types!==null&&(U.types=ct.types),et.T=U}}function we(g){var U=et.T;if(U!==null){var ct=U.types;ct===null?U.types=[g]:ct.indexOf(g)===-1&&ct.push(g)}else Se(we.bind(null,g))}var Ne={map:ut,forEach:function(g,U,ct){ut(g,function(){U.apply(this,arguments)},ct)},count:function(g){var U=0;return ut(g,function(){U++}),U}Line truncated */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
* @license React * @license React
* scheduler.production.js * scheduler.production.js
* *
@@ -28,7 +28,7 @@
* *
* This source code is licensed under the MIT license found in the * This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/var Hh;function Gy(){return Hh||(Hh=1,(function(i){function r(G,A){var ut=G.length;G.push(A);t:for(;0<ut;){var bt=ut-1>>>1,pt=G[bt];if(0<y(pt,A))G[bt]=A,G[ut]=pt,ut=bt;else break t}}function h(G){return G.length===0?null:G[0]}function f(G){if(G.length===0)return null;var A=G[0],ut=G.pop();if(ut!==A){G[0]=ut;t:for(var bt=0,pt=G.length,Se=pt>>>1;bt<Se;){var we=2*(bt+1)-1,Ne=G[we],g=we+1,U=G[g];if(0>y(Ne,ut))g<pt&&0>y(U,Ne)?(G[bt]=U,G[g]=ut,bt=g):(G[bt]=Ne,G[we]=ut,bt=we);else if(g<pt&&0>y(U,ut))G[bt]=U,G[g]=ut,bt=g;else break t}}return A}function y(G,A){var ut=G.sortIndex-A.sortIndex;return ut!==0?ut:G.id-A.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 M=Date,S=M.now();i.unstable_now=function(){return M.now()-S}}var _=[],O=[],Y=1,x=null,H=3,L=!1,tt=!1,dt=!1,J=!1,gt=typeof setTimeout=="function"?setTimeout:null,_t=typeof clearTimeout=="function"?clearTimeout:null,Dt=typeof setImmediate<"u"?setImmediate:null;function Q(G){for(var A=h(O);A!==null;){if(A.callback===null)f(O);else if(A.startTime<=G)f(O),A.sortIndex=A.expirationTime,r(_,A);else break;A=h(O)}}function ft(G){if(dt=!1,Q(G),!tt)if(h(_)!==null)tt=!0,D||(D=!0,Nt());else{var A=h(O);A!==null&&rt(ft,A.startTime-G)}}var D=!1,X=-1,et=5,xt=-1;function Mt(){return J?!0:!(i.unstable_now()-xt<et)}function mt(){if(J=!1,D){var G=i.unstable_now();xt=G;var A=!0;try{t:{tt=!1,dt&&(dt=!1,_t(X),X=-1),L=!0;var ut=H;try{e:{for(Q(G),x=h(_);x!==null&&!(x.expirationTime>G&&Mt());){var bt=x.callback;if(typeof bt=="function"){x.callback=null,H=x.priorityLevel;var pt=bt(x.expirationTime<=G);if(G=i.unstable_now(),typeof pt=="function"){x.callback=pt,Q(G),A=!0;break e}x===h(_)&&f(_),Q(G)}else f(_);x=h(_)}if(x!==null)A=!0;else{var Se=h(O);Se!==null&&rt(ft,Se.startTime-G),A=!1}}break t}finally{x=null,H=ut,L=!1}A=void 0}}finally{A?Nt():D=!1}}}var Nt;if(typeof Dt=="function")Nt=function(){Dt(mt)};else if(typeof MessageChannel<"u"){var B=new MessageChannel,nt=B.port2;B.port1.onmessage=mt,Nt=function(){nt.postMessage(null)}}else Nt=function(){gt(mt,0)};function rt(G,A){X=gt(function(){G(i.unstable_now())},A)}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"):et=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 A=3;break;default:A=H}var ut=H;H=A;try{return G()}finally{H=ut}},i.unstable_requestPaint=function(){J=!0},i.unstable_runWithPriority=function(G,A){switch(G){case 1:case 2:case 3:case 4:case 5:break;default:G=3}var ut=H;H=G;try{return A()}finally{H=ut}},i.unstable_scheduleCallback=function(G,A,ut){var bt=i.unstable_now();switch(typeof ut=="object"&&ut!==null?(ut=ut.delay,ut=typeof ut=="number"&&0<ut?bt+ut:bt):ut=bt,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=ut+pt,G={id:Y++,callback:A,priorityLevel:G,startTime:ut,expirationTime:pt,sortIndex:-1},ut>bt?(G.sortIndex=ut,r(O,G),h(_)===null&&G===h(O)&&(dt?(_t(X),X=-1):dt=!0,rt(ft,ut-bt))):(G.sortIndex=pt,r(_,G),tt||L||(tt=!0,D||(D=!0,Nt()))),G},i.unstable_shouldYield=Mt,i.unstable_wrapCallback=function(G){var A=H;return function(){var ut=H;H=A;try{return G.apply(this,arguments)}finally{H=ut}}}})(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,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={};/**
* @license React * @license React
* react-dom.production.js * react-dom.production.js
* *
@@ -36,7 +36,7 @@
* *
* This source code is licensed under the MIT license found in the * This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree. * 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 H=2;H<arguments.length;H++)x+="&args[]="+encodeURIComponent(arguments[H])}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"),M=Symbol.for("react.optimistic_key");function S(Y,x,H){var L=3<arguments.length&&arguments[3]!==void 0?arguments[3]:null;return{$$typeof:y,key:L==null?null:L===M?M:""+L,children:Y,containerInfo:x,implementation:H}}var _=i.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;function O(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 H=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,H)},Ue.flushSync=function(Y){var x=_.T,H=f.p;try{if(_.T=null,f.p=2,Y)return Y()}finally{_.T=x,f.p=H,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 H=x.as,L=O(H,x.crossOrigin),tt=typeof x.integrity=="string"?x.integrity:void 0,dt=typeof x.fetchPriority=="string"?x.fetchPriority:void 0;H==="style"?f.d.S(Y,typeof x.precedence=="string"?x.precedence:void 0,{crossOrigin:L,integrity:tt,fetchPriority:dt}):H==="script"&&f.d.X(Y,{crossOrigin:L,integrity:tt,fetchPriority:dt,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 H=O(x.as,x.crossOrigin);f.d.M(Y,{crossOrigin:H,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 H=x.as,L=O(H,x.crossOrigin);f.d.L(Y,H,{crossOrigin:L,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 H=O(x.as,x.crossOrigin);f.d.m(Y,{as:typeof x.as=="string"&&x.as!=="script"?x.as:void 0,crossOrigin:H,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,H){return _.H.useFormState(Y,x,H)},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 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}/**
* @license React * @license React
* react-dom-client.production.js * react-dom-client.production.js
* *
@@ -44,37 +44,37 @@
* *
* This source code is licensed under the MIT license found in the * This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree. * 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 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 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 O(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 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 L(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 tt(t){var e=[null,null],l=H(t);return l===null||dt(e,t,l.child,{foundSelf:!1}),e}function dt(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)&&dt(t,e,l.child,a))return!0;l=l.sibling}return!1}function J(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 gt=null,_t=null;function Dt(t,e,l){return t===l?!0:t===e?(gt=t,!0):!1}function Q(t,e,l){return t===l?(_t=t,!1):t===e?(_t!==null&&(gt=t),!0):!1}function ft(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 D(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,et=Symbol.for("react.element"),xt=Symbol.for("react.transitional.element"),Mt=Symbol.for("react.portal"),mt=Symbol.for("react.fragment"),Nt=Symbol.for("react.strict_mode"),B=Symbol.for("react.profiler"),nt=Symbol.for("react.consumer"),rt=Symbol.for("react.context"),G=Symbol.for("react.forward_ref"),A=Symbol.for("react.suspense"),ut=Symbol.for("react.suspense_list"),bt=Symbol.for("react.memo"),pt=Symbol.for("react.lazy"),Se=Symbol.for("react.activity"),we=Symbol.for("react.legacy_hidden"),Ne=Symbol.for("react.memo_cache_sentinel"),g=Symbol.for("react.view_transition"),U=Symbol.for("react.recoverable"),ct=Symbol.iterator;function it(t){return t===null||typeof t!="object"?null:(t=ct&&t[ct]||t["@@iterator"],typeof t=="function"?t:null)}var Ot=Symbol.for("react.client.reference");function Et(t){if(t==null)return null;if(typeof t=="function")return t.$$typeof===Ot?null:t.displayName||t.name||null;if(typeof t=="string")return t;switch(t){case mt:return"Fragment";case B:return"Profiler";case Nt:return"StrictMode";case A:return"Suspense";case ut:return"SuspenseList";case Se:return"Activity";case g:return"ViewTransition"}if(typeof t=="object")switch(t.$$typeof){case Mt:return"Portal";case rt:return t.displayName||"Context";case nt: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 bt:return e=t.displayName||null,e!==null?e:Et(t.type)||"Memo";case pt:e=t._payload,t=t._init;try{return Et(t(e))}catch{}}return null}var kt=Array.isArray,P=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,se={pending:!1,data:null,method:null,actiLine truncated */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
at`)?" (<anonymous>)":-1<l.stack.indexOf("@")?"@unknown:0:0":""}return` at`)?" (<anonymous>)":-1<l.stack.indexOf("@")?"@unknown:0:0":""}return`
`+He+t+re}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 R=function(){throw Error()};if(Object.defineProperty(R.prototype,"props",{set:function(){throw Error()}}),typeof Reflect=="object"&&Reflect.construct){try{Reflect.construct(R,[])}catch(V){var p=V}Reflect.construct(t,[],R)}else{try{R.call()}catch(V){p=V}R=!1;try{var E=Object.getOwnPropertyDescriptor(t.prototype,"props");Object.defineProperty(t.prototype,"props",{configurable:!0,set:function(){throw Error()}}),R=!0,new t}finally{R&&(E!==void 0?Object.defineProperty(t.prototype,"props",E):delete t.prototype.props)}}}else{try{throw Error()}catch(V){p=V}(R=t())&&typeof R.catch=="function"&&R.catch(function(){})}}catch(V){if(V&&p&&typeof V.stack=="string")return[V.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(` `+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(`
`),j=o.split(` `),b=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 N=` `);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:"")?oe(l):""}function ya(t,e){switch(t.tag){case 26:case 27:case 5:return oe(t.type);case 16:return oe("Lazy");case 13:return t.child!==e&&e!==null?oe("Suspense Fallback"):oe("Suspense");case 19:return oe("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 oe("Activity");case 30:return oe("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` `+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`
Error generating stack: `+a.message+` 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,k=262144,I=4194304;function at(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=at(a):(s&=o,s!==0?n=at(s):l||(l=o&~t,l!==0&&(n=at(l))))):(o=a&~u,o!==0?n=at(o):s!==0?n=at(s):l||(l=a&~t,l!==0&&(n=at(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 w(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 Z(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 K(){var t=I;return I<<=1,(I&62914560)===0&&(I=4194304),t}function Yt(t){for(var e=[],l=0;31>l;l++)e.push(t);return e}function St(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,j=t.hiddenUpdates;for(l=s&~l;0<l;){var N=31-Ae(l),R=1<<N;o[N]=0,m[N]=-1;var p=j[N];if(p!==null)for(j[N]=null,N=0;N<p.length;N++){var E=p[N];E!==null&&(E.lane&=-536870913)}l&=~R}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),ye="__reactFiber$"+ne,Je="__reactProps$"+ne,gn="__reactContainer$"+ne,xo="__reactEvents$"+ne,Ph="__reactListeners$"+ne,tv="__reactHandles$"+ne,bo="__reactResources$"+ne,pu="__reactMarker$"+ne,fi="__reactLoad$"+ne;function di(t){delete t[ye],delete t[Je],delete t[Ph],delete t[tv]}function Za(t){var e;if(e=t[ye])return e;for(var l=t.parentNode;l;){if(e=l[gn]||l[ye]){if(l=e.alternate,e.child!==null||l!==null&&l.child!==null)for(t=ih(t);t!==null;){if(l=t[ye])return l;t=ih(t)}return e}t=l,l=t.parentNode}return null}function pn(t){if(t=t[ye]||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[bo];return e||(e=t[bo]={hoistableStyles:new Map,hoistableScripts:new Map}),e}function je(t){t[pu]=!0}function jo(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 `+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===""||jn(t,a);else if(typeof a=="number"||typeof a=="bigint")e!=="body"&&jn(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&&Rt("scroll",t);return;case"onScrollEnd":a!=null&&Rt("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":Rt("beforetoggle",t),Rt("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")jn(t,a);else if(typeof a=="number"||typeof a=="bigint")jn(t,""+a);else return;break;case"onScroll":a!=null&&Rt("scroll",t);return;case"onScrollEnd":a!=null&&Rt("scroLine 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+` `;Yt&&(qe+=Yt+`
`),St&&(qe+=(Array.isArray(St)?St.join(` `),xt&&(qe+=(Array.isArray(xt)?xt.join(`
`):String(St))+` `):String(xt))+`
`),qe+=JSON.stringify(K,null,2).slice(0,4e3),_l(qe),se("已返回登录信息,见下方")}catch(K){se("发起失败:"+K.message)}},[]),I=d.useCallback(async()=>{const w=tt.trim();if(!w){L("请填要查配额的用户 id");return}try{const Z=await W(F.adminQuotaUser(w));L("用户 "+w.slice(0,8)+":本月用量 "+iu(Z.used_usd_micros)+" / 配额 "+iu(Z.quota_usd_micros)+(Z.remaining_usd_micros!==null&&Z.remaining_usd_micros!==void 0?"(剩余 "+iu(Z.remaining_usd_micros)+")":"")+" | 月份 "+(Z.month||""))}catch(Z){L(Dc(Z))}},[tt]),at=d.useCallback(async(w,Z)=>{const K=w.trim();if(!K){L("请填要设置配额的用户 id");return}let Yt;if(!Z.trim())Yt=null;else{const St=Number(Z.trim());if(!Number.isFinite(St)||St<0){L("配额金额无效:请输入 >= 0 的美元数,留空表示不限额");return}Yt=Math.round(St*1e6)}try{const St=await W(F.adminQuotas,"POST",{user_id:K,quota_usd_micros:Yt});L("配额已设置:用户 "+K.slice(0,8)+":本月用量 "+iu(St.used_usd_micros)+" / 配额 "+iu(St.quota_usd_micros)+(St.remaining_usd_micros!==null&&St.remaining_usd_micros!==void 0?"(剩余 "+iu(St.remaining_usd_micros)+")":"")+" | 月份 "+(St.month||""))}catch(St){L(Dc(St))}},[]),qt=d.useCallback(async()=>{if(window.confirm("确定手动触发 sweep 吗?超阈值的陈旧 running 任务会被标为 interrupted。"))try{const Z=(await W(F.adminSweep,"POST",{})).swept||[];L("sweep 完成,清理 "+Z.length+" 个任务"+(Z.length?":"+Z.join("、"):""))}catch(w){L(Dc(w))}},[]),hl=d.useCallback(async()=>{try{const w=await W(F.adminBackup,"POST",{});L("备份完成:backend="+w.backend+" | 路径 "+w.path+" | 大小 "+w.size_bytes+" 字节 | 任务 "+w.task_count+" 行")}catch(w){L(Dc(w))}},[]);return{keySaved:i,keyState:h,quotaText:y,healthText:M,adminGate:_,adminBody:Y,adminResult:H,adminUserId:tt,setAdminUserId:dt,projects:J,projectId:_t,setProjectId:Dt,projectMsg:Q,githubRepos:D,workers:et,workersSummary:Mt,workersError:Nt,members:nt,memberMsg:G,memberError:ut,skills:pt,apps:we,mcpServers:g,skillMsg:ct,skillError:Ot,mcpMsg:kt,mcpOauthMsg:ht,mcpOauthBox:La,envs:Ve,envCount:Qt,envMsg:Wl,envRepoLine:va,envRepoBar:Ul,envRepoMsg:El,envSelect:re,setEnvSelect:oe,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:k,adminQuota:I,adminSetQuota:at,adminSweep:qt,adminBackup:hl}}const mo=20;function ag(){const[i,r]=d.useState([]),[h,f]=d.useState(""),[y,C]=d.useState(""),[M,S]=d.useState(""),[_,O]=d.useState(""),[Y,x]=d.useState(0),[H,L]=d.useState(0),[tt,dt]=d.useState({}),J=d.useRef(0),gt=d.useRef({status:"",executor:"",q:""});gt.current={status:y,executor:M,q:_};const _t=d.useRef(""),Dt=d.useCallback(async(nt,rt,G)=>{const A=nt!==void 0?nt:rt;if(!A){r([]),L(0),dt({});return}_t.current=A;const ut=G!==void 0?G:J.current,bt=gt.current;try{const pt=await W(F.tasksPage(A,mo,ut*mo,bt.status,bt.executor,bt.q));r(pt.tasks||[]),L(pt.total??(pt.tasks||[]).length),dt(pt.status_counts||{}),f("")}catch(pt){f("加载失败:"+pt.message)}},[]),Q=d.useCallback(nt=>{const rt=Math.max(0,nt);J.current=rt,x(rt)},[]),ft=d.useCallback(nt=>{Q(nt),Dt(_t.current||void 0,void 0,nt)},[Dt,Q]);d.useEffect(()=>{if(!_t.current)return;const nt=setTimeout(()=>{Dt(_t.current,void 0,0)},300);return()=>clearTimeout(nt)},[y,M,_]);const D=d.useCallback(nt=>{C(nt),Q(0)},[Q]),X=d.useCallback(nt=>{S(nt),Q(0)},[Q]),et=d.useCallback(nt=>{O(nt),Q(0)},[Q]),xt=d.useCallback(async nt=>{Q(0),await Dt(nt,void 0,0)},[Dt,Q]),Mt=Math.max(1,Math.ceil(H/mo)),mt=H===0?"共 0 个任务":"共 "+H+" 个任务,第 "+(Y+1)+" / "+Mt+" 页",Nt=i,B=d.useCallback(async(nt,rt)=>{const G={...nt};rt.length&&(G.upload_id=await tg(rt));const A=await W(F.tasksCreate,"POST",G);return"任务已建:"+pe(A.id)},[]);return{tasks:i,taskError:h,statusFilter:y,setStatusFilter:D,executorFilter:M,setExecutorFilter:X,searchText:_,setSearchText:et,filteredTasks:Nt,taskCountText:mt,refreshTasks:Dt,refreshTasksFirstPage:xt,page:Y,totalPages:Mt,taskTotal:H,taskStatusCounts:tt,goToPage:ft,dispatch:B}}function ng(i,r){const[h,f]=d.useState(null),[y,C]=d.useState(""),[M,S]=d.useState(""),[_,O]=d.useState(""),[Y,x]=d.useState(""),[H,L]=d.useState(void 0),[tt,dt]=d.useState(!1),[J,gt]=d.useState(""),[_t,Dt]=d.useState([]),[Q,ft]=d.useState([]),[D,X]=d.useState(""),[et,xt]=d.useState([]),[Mt,mt]=d.useState(""),[Nt,B]=d.useState(""),[nt,rt]=d.useState([]),[G,A]=d.useState(""),[ut,bt]=d.useState(""),[pt,Se]=d.useState(""),[we,Ne]=d.useState(""),[g,U]=d.useState([]),[ct,it]=d.useState([]),[Ot,Et]=d.useState(""),[kt,P]=d.useState([]),[ht,se]=d.useState(""),[La,_l]=d.useState(""),[Ve,ae]=d.useState(""),[Qt,ve]=d.useState(""),[Wl,xe]=d.useState([]),[va,Ze]=d.useState(""),Line truncated `),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
`);yn=Bl.pop()||"";for(const yu of Bl){const gu=yu.split(` `);yn=Bl.pop()||"";for(const yu of Bl){const gu=yu.split(`
`).filter(Ke=>Ke.indexOf("data: ")===0).map(Ke=>Ke.slice(6)).join(` `).filter(Ke=>Ke.indexOf("data: ")===0).map(Ke=>Ke.slice(6)).join(`
`);if(gu)try{const Ke=JSON.parse(gu);if(Ke.type==="snapshot")at=Ke.entries||[],Object.keys(qt).forEach(ne=>delete qt[ne]),at.forEach((ne,ye)=>{ne.id&&(qt[ne.id]=ye)}),hl=Ke.note||"",rt([...at]),A(hl),$l(q);else if(Ke.type==="entry"){const ne=Ke.entry;ne.id&&ne.id in qt?at[qt[ne.id]]=ne:(ne.id&&(qt[ne.id]=at.length),at.push(ne)),rt([...at]),$l(q)}else Ke.type==="done"&&(w=!0,Z=Ke.status,bt(Z))}catch{}}if(w)break}}catch(K){if(K&&K.name==="AbortError")return;A("链路流中断:"+K.message);return}w&&r()},[oe,$l,r]),be=d.useCallback(async q=>{oe();try{const k=await W(F.task(q));f(k);let I="";k.result_text&&(I+=`=== result === `);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 ===
`+k.result_text.slice(0,8e3)+` `+w.result_text.slice(0,8e3)+`
`),k.error_text&&(I+=`=== error === `),w.error_text&&(P+=`=== error ===
`+k.error_text.slice(0,8e3)+` `+w.error_text.slice(0,8e3)+`
`),I||(I="(暂无 result/error 输出)"),C(I),x(""),L(void 0),kl(q),dt(!1),gt(""),se(""),ve(""),vn(k.status);const at=i(k.project_id),qt=at==="maintainer"||at==="owner",hl=at==="contributor"||qt,w=su.includes(k.status),Z=[];["failed","interrupted","merge-failed"].includes(k.status)&&qt||Z.push("重试:不可用——仅 failed/interrupted/merge-failed 可重试"+(qt?"":",且仅 maintainer 可重试")),!w&&hl||Z.push("取消:不可用——"+(w?"终态任务不可取消":"仅项目成员(contributor 及以上)可取消")),k.status==="completed"&&qt||Z.push("合并:不可用——仅 completed 可合并"+(qt?"":",且仅 maintainer 可合并")),k.status==="merged"&&qt||Z.push("提 PR:不可用——仅 merged 可提 PR"+(qt?"":",且仅 maintainer 可提 PR")),O(Z.join(";")),S(""),Va(q,k.status),fl(q),ya(k),tl(q),te(q),dl(q),$t(q),Hl(q),Be(q),Nl(q)}catch(k){f(null),C("加载失败:"+k.message)}},[oe,Va,fl,ya,tl,te,dl,$t,Hl,Be,Nl,kl,i]),Il=d.useCallback(async()=>{const q=re.current;if(q){try{const k=await W(F.task(q.id));f(k);let I="";k.result_text&&(I+=`=== result === `),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 ===
`+k.result_text.slice(0,8e3)+` `+w.result_text.slice(0,8e3)+`
`),k.error_text&&(I+=`=== error === `),w.error_text&&(P+=`=== error ===
`+k.error_text.slice(0,8e3)+` `+w.error_text.slice(0,8e3)+`
`),C(I||"(暂无 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 k=re.current;if(!k)return;const I={cancel:"确定取消该任务吗?",merge:"确定把 completed 任务标为 merged 吗?",pr:"确定为该任务提 PR 吗?(会在仓库真实建 PR)"};if(!(I[q]&&!window.confirm(I[q])))try{const at=await W(F.taskAction(k.id,q),"POST",{});S(q+" 已提交:"+JSON.stringify(at).slice(0,300)),r(),await be(k.id)}catch(at){S(q+" 失败:"+at.message)}},[be,r]),fu=d.useCallback(async()=>{const q=re.current;if(q)try{const k=await W(F.taskCost(q.id));x(JSON.stringify(k,null,2).slice(0,4e3))}catch(k){x("查成本失败:"+k.message)}},[]),du=d.useCallback(async q=>{const k=re.current;if(k){if(!q.trim()){gt("分支消息必填");return}if(!su.includes(k.status)){gt("仅终态任务可分支");return}try{const I=await W(F.taskFork(k.id),"POST",{message:q.trim()});gt("分支任务已建:"+I.id.slice(0,8)),r()}catch(I){gt("分支失败:"+I.message)}}},[r]),Pl=d.useCallback(async q=>{const k=re.current;if(k)try{await W(F.taskReviews(k.id),"POST",q),mt("评审已登记(requested)"),await Be(k.id)}catch(I){mt("登记评审需求失败:"+I.message)}},[Be]),Ce=d.useCallback(q=>{const k=re.current;if(!k)return;const I=window.prompt("输入该条评审的 findings(将写回记录并标为 completed):");I!==null&&W(F.taskReviewFindings(k.id,q),"POST",{findings:I}).then(()=>void Be(k.id)).catch(at=>mt("写 findings 失败:"+at.message))},[Be]),ml=d.useCallback((q,k)=>{const I=re.current;if(!I)return;const at=k==="approved"?"approve":"deny";W(F.taskApprovalDecision(I.id,q,at),"POST",{}).then(()=>{Et("已记录"+(k==="approved"?"允许":"拒绝")+"(当前版本只留回执,不拦截实时行为)"),Nl(I.id)}).catch(qt=>Et("决定失败:"+qt.message))},[Nl]),Ae=d.useCallback(async(q,k,I)=>{const at=re.current;if(at){if(!k.trim()){se("目标任务 id 不能为空");return}if(!I.trim()){se("消息正文不能为空");return}try{const qt=await W(F.taskMessagesTo(k.trim()),"POST",{from_task_id:at.id,body:I.trim(),mode:q});se("已发送("+qt.delivery+")"),await dl(at.id)}catch(qt){se("发送失败:"+qt.message)}}},[dl]),mu=d.useCallback((q,k)=>{const I=re.current;if(!I)return;if(!["completed","failed","interrupted"].includes(I.status)){ve("仅 completed/failed/interrupted 任务可操作线程(merged/merge-failed 不可改,服务端同样 409)");return}const at={};if(q==="rename"){if(!k.trim()){ve("新线程名不能为空");return}at.name=k.trim()}W(F.taskThreadAction(I.id,q),"POST",at).then(qt=>{ve(q+" 已提交:"+JSON.stringify(qt).slice(0,300)),$t(I.id),Hl(I.id)}).catch(qt=>ve(q+" 失败:"+qt.message))},[$t,Hl]),hu=d.useCallback(async q=>{const k=re.current;if(!k)return;const I=q.trim();if(!I){Ze("消息不能为空");return}try{su.includes(k.status)&&k.status!=="merged"?(await W(F.taskContinue(k.id),"POST",{message:I}),Ze("已重新排队,同一会话继续"),r(),await be(k.id)):["queued","running"].includes(k.status)?(await W(F.taskFollowups(k.id),"POST",{body:I,mode:"direct"}),Ze("已直接发送"),await te(k.id),await tl(k.id)):Ze("merged/merge-failed 任务不可继续,也不可发后续消息")}catch(at){Ze("发送失败:"+at.message)}},[be,r,te,tl]),vu=d.useCallback(async()=>{const q=re.current;if(!q){Ne("先选一个任务");return}try{const k=await W(F.taskTrace(q.id)),I=new Blob([JSON.stringify({task_id:q.id,status:k.status,note:k.note||"",count:(k.entries||[]).length,entries:k.entries||[]},null,2)],{type:"application/json"}),at=document.createElement("a");at.href=URL.createObjectURL(I),at.download="trace-"+q.id.slice(0,8)+".json",document.body.appendChild(at),at.click(),at.remove(),setTimeout(()=>URL.revokeObjectURL(at.href),1e3),Ne("已下载完整链路 "+(k.entries||[]).length+" 条")}catch(k){Ne("下载失败:"+k.message)}},[]);return d.useEffect(()=>()=>oe(),[oe]),{currentTask:h,detailBody:y,actionMsg:M,actionReasons:_,costBox:Y,costSummary:H,forkVisible:tt,setForkVisible:dt,forkMsg:J,coordChildren:_t,agents:Q,agentsNote:D,reviews:et,reviewMsg:Mt,reviewError:Nt,traceEntries:nt,traceNote:G,traceDone:ut,traceKind:pt,setTraceKind:Se,traceMsg:we,followups:g,approvals:ct,approvalMsg:Ot,peerMessages:kt,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,M]=d.useState(null),[S,_]=d.useState(""),[O,Y]=d.useState(null),[x,H]=d.useState(""),L=d.useCallback(async Q=>{const ft=Q!==void 0?Q:i();if(!ft){h([]);return}try{const D=await W(F.projectAutomations(ft));h(D.automations||[])}catLine truncated `),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(tt=>({id:tt.id,role:tt.role,text:tt.text,label:tt.label,kind:tt.kind}));const H=new Set(_.map(tt=>tt.id)),L=i.traceEntries.filter(tt=>tt.id&&!H.has(tt.id)&&!String(tt.id).startsWith("fu-")).map((tt,dt)=>Lh(tt,dt));_=[..._,...L]}else{const H=r.prompt?[{id:"task-prompt",role:"user",text:r.prompt}]:[],L=i.traceEntries.filter(dt=>!String(dt.id||"").startsWith("fu-")).map((dt,J)=>Lh(dt,J)),tt=i.followups.map((dt,J)=>({id:"followup-"+J,role:"user",text:String(dt.body||""),label:"后续消息"}));_=[...H,...L,...tt]}const O=i.traceKind,Y=O?_.filter(H=>H.role==="user"||!H.kind||H.kind===O):_,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,M=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:[M,"。",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 (链路摘要已截断至 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
`);for(;nt>=0;){const rt=mt.slice(0,nt);mt=mt.slice(nt+2);for(const G of rt.split(` `);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 A=JSON.parse(G.slice(6));A.waiting?S(A.task_status==="interrupted"?"任务已中断,等待续跑…":A.task_status==="paused"?"任务已暂停…":"等待会话…"):A.image&&(f("data:image/jpeg;base64,"+A.image),S("直播中"));const ut={};A.holder&&(ut.holder=A.holder),A.url&&(ut.url=A.url),A.title!==void 0&&(ut.title=A.title),Object.keys(ut).length>0&&C(bt=>({...bt||{},...ut}))}catch{}nt=mt.indexOf(` `))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(`
`)}}S("流已断开")}catch(et){if(et.name==="AbortError")return;O(et instanceof Error?et.message:String(et)),S("未开始")}finally{x(!1)}},[r]),J=d.useCallback(()=>{var D;(D=H.current)==null||D.abort(),x(!1),S("已停止")},[]),gt=d.useCallback(async D=>{if(r)try{const X=await W(F.taskBrowserControl(r.id),"POST",{holder:D});C(X),O("")}catch(X){O(X instanceof Error?X.message:String(X))}},[r]),_t=d.useCallback(async D=>{if(!r||(y==null?void 0:y.holder)!=="user"||!L.current)return;const X=L.current.getBoundingClientRect(),et=Math.round((D.clientX-X.left)/X.width*1280),xt=Math.round((D.clientY-X.top)/X.height*800);try{await W(F.taskBrowserInput(r.id),"POST",{type:"mouse_click",x:et,y:xt})}catch(Mt){O(Mt instanceof Error?Mt.message:String(Mt))}},[r,y]),Dt=d.useCallback(async D=>{if(!(!r||(y==null?void 0:y.holder)!=="user")){D.preventDefault();try{D.key.length===1?await W(F.taskBrowserInput(r.id),"POST",{type:"type",text:D.key}):await W(F.taskBrowserInput(r.id),"POST",{type:"key",key:D.key})}catch(X){O(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)||"—",ft=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:["会话 ",ft?"活跃":"未激活(轮次结束即消失)"]}),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:J,children:"停止观看"}):c.jsx("button",{type:"button",onClick:()=>void dt(),children:"开始观看"}),c.jsx("button",{type:"button",onClick:()=>void tt(),children:"刷新状态"}),Q!=="user"?c.jsx("button",{type:"button",onClick:()=>void gt("user"),disabled:!ft,children:"接管"}):c.jsx("button",{type:"button",onClick:()=>void gt("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:L,src:h,alt:"浏览器实时画面",tabIndex:0,onClick:D=>void _t(D),onKeyDown:D=>void Dt(D),style:{width:"100%",maxWidth:1280,cursor:Q==="user"?"crosshair":"default",border:"1px solid var(--border, #ccc)"}}):c.jsx("div",{className:"muted",children:ft?"点“开始观看”接入画面流(接管后点画面即点击)":"浏览器会话未激活:任务本轮未开浏览器,或轮次已结束。"})})]})}function jg(){const i=Lt(),[r,h]=d.useState("uncommittedChanges"),[f,y]=d.useState(""),[C,M]=d.useState(""),[S,_]=d.useState("");return c.jsxs(c.Fragment,{children:[c.jsx("h3",{children:"评审队列"}),c.jsxs("select",{"aria-label":"评审目标",value:r,onChange:O=>h(O.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:O=>y(O.target.value)}),c.jsx("input",{className:"stacked",placeholder:"指定提交用:提交 sha",style:{width:"100%"},value:C,onChange:O=>M(O.target.value)}),c.jsx("textarea",{placeholder:"自定义评审指令用:评审指令正文",value:S,onChange:O=>_(O.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(O=>{let Y=O.target_type||"";return O.branch&&(Y+=":"+O.branch),O.sha&&(Y+=":"+String(O.sha).slice(0,12)),O.instructions&&(Y+=":"+String(O.instructions).slice(0,80)),c.jsxs("li",{children:[c.jsxs("span",{className:"row-main",children:[c.jsx("b",{children:"评审"})," ",pe(O.id)," | ",Y," | ",c.jsx("span",{className:"st-"+(O.status||""),children:O.sLine truncated `)}}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
<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 <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> </head>
<body> <body>
+56 -12
View File
@@ -54,6 +54,7 @@ from collections import deque
from decimal import Decimal from decimal import Decimal
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import parse_qs, urlparse from urllib.parse import parse_qs, urlparse
import urllib.request
from cloud.agent_def import default_agents_dir as _agent_default_dir from cloud.agent_def import default_agents_dir as _agent_default_dir
from cloud.agent_def import list_known_skills as _agent_known_skills from cloud.agent_def import list_known_skills as _agent_known_skills
@@ -1042,6 +1043,28 @@ def _db_backend_name():
return (os.environ.get("CLOUD_DB_BACKEND") or "sqlite").strip().lower() return (os.environ.get("CLOUD_DB_BACKEND") or "sqlite").strip().lower()
def _codex_ui_url():
"""原生 UI host 地址:环境变量 CODEX_UI_URL,未设则默认本机 :8095。"""
return os.environ.get("CODEX_UI_URL") or "http://127.0.0.1:8095/"
def _codex_ui_reachable():
"""探测 UI host 是否可达:GET <CODEX_UI_URL 的 origin>/,2 秒超时。
任一异常(连接失败、超时、DNS 失败等)或非 200 一律视为不可达,
调用方回落旧控制台;探测失败绝不影响 /console 本身的响应。"""
try:
parsed = urlparse(_codex_ui_url())
if not parsed.netloc:
return False
probe = "{}://{}/".format(parsed.scheme or "http", parsed.netloc)
request = urllib.request.Request(probe, method="GET")
with urllib.request.urlopen(request, timeout=2) as resp:
return resp.status == 200
except Exception:
return False
def _deployed_commit(): def _deployed_commit():
"""当前上线代码的 commit:deploy.py 部署时在包根写的 DEPLOYED_COMMIT。 """当前上线代码的 commit:deploy.py 部署时在包根写的 DEPLOYED_COMMIT。
@@ -2329,6 +2352,29 @@ class CloudControlPlaneHandler(OAuthMixin, IdentityMemoryMixin, ProjectMixin, Au
self._responded = True self._responded = True
self.wfile.write(body) self.wfile.write(body)
def _send_redirect(self, location):
self.send_response(302)
self.send_header("Location", location)
self.send_header("Content-Length", "0")
self.end_headers()
# 发头即算已响应(与 _send_json/_send_html 同款约定)。
self._responded = True
def _serve_console_classic(self):
# 旧 React 控制台(cloud/console.html):只出静态壳,不走鉴权
# 与限流(与 /health 同款 pre-auth 待遇)。壳里没有任何数据;
# 数据一律凭用户在浏览器里填的 X-API-Key 调接口,key 只存在
# 用户浏览器 localStorage。页面文件与 server.py 同目录,改动
# 随部署走。/console(UI host 不可达时回落)与 /console-classic
# 共用本方法,不要复制两份读文件代码。
try:
here = os.path.dirname(os.path.abspath(__file__))
with open(os.path.join(here, "console.html"),
"r", encoding="utf-8") as f:
self._send_html(200, f.read())
except OSError:
self._send_json(404, {"error": "console not found"})
def _read_body(self): def _read_body(self):
try: try:
length = int(self.headers.get("Content-Length") or 0) length = int(self.headers.get("Content-Length") or 0)
@@ -2616,18 +2662,16 @@ class CloudControlPlaneHandler(OAuthMixin, IdentityMemoryMixin, ProjectMixin, Au
"commit": _deployed_commit(), "commit": _deployed_commit(),
}) })
return return
if path == "/console" and method == "GET": if path in ("/console", "/console-classic") and method == "GET":
# 命令台 v1:只出静态壳,不走鉴权与限流(与 /health 同款 # /console:原生 UI 入口(见 docs/codex-native-ui-alignment.md
# pre-auth 待遇)。壳里没有任何数据;数据一律凭用户在 # §3):UI host 可达时 302 到 CODEX_UI_URL(默认
# 浏览器里填的 X-API-Key 调接口,key 只存在用户浏览器 # http://127.0.0.1:8095/),不可达时回落旧控制台;
# localStorage。页面文件与 server.py 同目录,改动随部署走。 # /console-classic 永远 serve 旧控制台。两者都不走鉴权与
try: # 限流(与 /health 同款 pre-auth 待遇)。
here = os.path.dirname(os.path.abspath(__file__)) if path == "/console" and _codex_ui_reachable():
with open(os.path.join(here, "console.html"), self._send_redirect(_codex_ui_url())
"r", encoding="utf-8") as f: else:
self._send_html(200, f.read()) self._serve_console_classic()
except OSError:
self._send_json(404, {"error": "console not found"})
return return
if path.startswith("/oauth/"): if path.startswith("/oauth/"):
# OAuth 登录/回调是 pre-auth 端点(用户还没本站 API key, # OAuth 登录/回调是 pre-auth 端点(用户还没本站 API key,
+80
View File
@@ -0,0 +1,80 @@
"""Codex 原生 UI host 入仓清单单测(见 docs/codex-native-ui-alignment.md §4 第 4 项)。
只做静态断言,不起服务、不调网络、不读密钥:
- appserver-methods.json 是合法 JSON、179 个方法、无重复;
- app-server-bridge.mjs 里 7 个仿真方法都在(读文件文本断言方法名出现);
- 仓内无 cloud/codex-ui-host/original/ 目录(650MB 原版栈不入仓);
- scripts/run-codex-ui.sh 存在且可执行。
路径一律以仓库根(本文件向上两级)为基准,不写死绝对路径。
"""
import json
import os
import unittest
REPO_ROOT = os.path.dirname(os.path.dirname(
os.path.dirname(os.path.abspath(__file__))))
# §4 适配 A5:150 个 UI 清单方法 + 29 个实验协议方法 = 179。
EXPECTED_METHOD_COUNT = 179
# §4 适配 A1–A4:桥接层仿真的 7 个缺失方法。
SIMULATED_METHODS = [
"thread/stop",
"thread/rollback",
"thread/startAeon",
"interactive/liveSessions/list",
"interactive/session/upload",
"interactive/sessionSandbox/list",
"interactive/sessionSandbox/read",
]
class TestAppserverMethodsJson(unittest.TestCase):
def test_valid_json_179_methods_no_duplicates(self):
path = os.path.join(
REPO_ROOT, "cloud", "codex-ui-host", "host",
"appserver-methods.json")
with open(path, encoding="utf-8") as f:
methods = json.load(f)
self.assertIsInstance(methods, list)
self.assertEqual(len(methods), EXPECTED_METHOD_COUNT)
self.assertTrue(all(isinstance(m, str) for m in methods))
self.assertEqual(len(set(methods)), len(methods),
"appserver-methods.json must not list a method twice")
class TestAppServerBridgeSimulations(unittest.TestCase):
def test_seven_simulated_methods_present(self):
path = os.path.join(
REPO_ROOT, "cloud", "codex-ui-host", "host",
"app-server-bridge.mjs")
with open(path, encoding="utf-8") as f:
text = f.read()
for name in SIMULATED_METHODS:
self.assertIn(name, text,
"%s must be simulated in app-server-bridge.mjs"
% name)
class TestOriginalNotVendored(unittest.TestCase):
def test_no_original_directory_in_repo(self):
self.assertFalse(
os.path.exists(os.path.join(
REPO_ROOT, "cloud", "codex-ui-host", "original")),
"cloud/codex-ui-host/original/ must not be vendored "
"(650MB original stack stays out of the repo)")
class TestRunScript(unittest.TestCase):
def test_run_script_exists_and_is_executable(self):
path = os.path.join(REPO_ROOT, "scripts", "run-codex-ui.sh")
self.assertTrue(os.path.isfile(path),
"scripts/run-codex-ui.sh must exist")
self.assertTrue(os.access(path, os.X_OK),
"scripts/run-codex-ui.sh must be executable")
if __name__ == "__main__":
unittest.main()
+63 -9
View File
@@ -1,8 +1,14 @@
"""命令台 /console 单测:只验页面壳与路由,不跑浏览器 JS。 """命令台 /console 单测:只验页面壳与路由,不跑浏览器 JS。
页面本身不走鉴权(只是个壳),数据一律凭 X-API-Key 调接口, /console 是原生 UI 入口(见 docs/codex-native-ui-alignment.md §3):
所以 /console 无 key 也应 200 出 HTML;真正的鉴权由各 API UI host 可达时 302 到 CODEX_UI_URL,不可达时回落旧控制台;
端点负责(已有单测覆盖)。 /console-classic 永远 serve 旧控制台。两者都不走鉴权(只是个
壳),数据一律凭 X-API-Key 调接口,所以无 key 也应 200 出 HTML;
真正的鉴权由各 API 端点负责(已有单测覆盖)。
默认测试环境没有 UI host 在跑,所以 /console 的断言是回落行为;
302 行为由 test_console_redirects_to_ui_host_when_reachable 用
测试内起的假 UI host 单独覆盖。
""" """
import http.client import http.client
import os import os
@@ -10,7 +16,9 @@ import sys
import tempfile import tempfile
import threading import threading
import unittest import unittest
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path from pathlib import Path
from unittest import mock
try: try:
from cloud.auth import AuthStore from cloud.auth import AuthStore
@@ -32,6 +40,13 @@ class ConsoleTestCase(unittest.TestCase):
self.thread = threading.Thread( self.thread = threading.Thread(
target=self.server.serve_forever, daemon=True) target=self.server.serve_forever, daemon=True)
self.thread.start() self.thread.start()
# 回落断言必须与环境无关:显式把 UI host 指到一个不可能有
# 监听的端口(127.0.0.1:1 立即 connection refused),而不是
# 赌测试机 :8095 上恰好没服务;302 测试在内层再覆盖它。
self._env_patch = mock.patch.dict(
os.environ, {"CODEX_UI_URL": "http://127.0.0.1:1/"})
self._env_patch.start()
self.addCleanup(self._env_patch.stop)
def tearDown(self): def tearDown(self):
self.server.shutdown() self.server.shutdown()
@@ -56,10 +71,47 @@ class ConsoleTestCase(unittest.TestCase):
finally: finally:
conn.close() conn.close()
def test_console_serves_html_without_key(self): def test_console_falls_back_to_classic_when_ui_host_unreachable(self):
# UI host 不可达时 /console 回落旧控制台:无 key 200 出 HTML。
status, headers, body = self._get("/console") status, headers, body = self._get("/console")
self.assertEqual(status, 200) self.assertEqual(status, 200)
self.assertIn("text/html", headers.get("Content-Type", "")) self.assertIn("text/html", headers.get("Content-Type", ""))
self.assertIn("Codex 命令台", body.decode("utf-8"))
def test_console_redirects_to_ui_host_when_reachable(self):
# 假 UI host:探测只看 GET / 是否 200,内容不重要。
class _FakeUIHandler(BaseHTTPRequestHandler):
def do_GET(self):
body = b"ok"
self.send_response(200)
self.send_header("Content-Type", "text/plain")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, *args):
pass
fake = ThreadingHTTPServer(("127.0.0.1", 0), _FakeUIHandler)
thread = threading.Thread(
target=fake.serve_forever, daemon=True)
thread.start()
try:
ui_url = "http://127.0.0.1:{}/".format(
fake.server_address[1])
with mock.patch.dict(os.environ, {"CODEX_UI_URL": ui_url}):
status, headers, _body = self._get("/console")
self.assertEqual(status, 302)
self.assertEqual(headers.get("Location"), ui_url)
finally:
fake.shutdown()
fake.server_close()
thread.join(timeout=5)
def test_console_classic_serves_html_without_key(self):
status, headers, body = self._get("/console-classic")
self.assertEqual(status, 200)
self.assertIn("text/html", headers.get("Content-Type", ""))
text = body.decode("utf-8") text = body.decode("utf-8")
self.assertIn("Codex 命令台", text) self.assertIn("Codex 命令台", text)
# 壳必须引用它实际调的接口,防 JS 与服务端路由漂移。 # 壳必须引用它实际调的接口,防 JS 与服务端路由漂移。
@@ -95,9 +147,10 @@ class ConsoleTestCase(unittest.TestCase):
def test_console_does_not_leak_data_without_key(self): def test_console_does_not_leak_data_without_key(self):
# 壳里只有静态文本,没有任何任务/项目数据(数据须凭 key 调 API)。 # 壳里只有静态文本,没有任何任务/项目数据(数据须凭 key 调 API)。
_status, _headers, body = self._get("/console") for path in ("/console", "/console-classic"):
text = body.decode("utf-8") _status, _headers, body = self._get(path)
self.assertNotIn(self.owner_id, text) text = body.decode("utf-8")
self.assertNotIn(self.owner_id, text)
def test_console_init_loads_relay_models(self): def test_console_init_loads_relay_models(self):
# 回访用户 key 已在 localStorage 里:初始化序列必须含 # 回访用户 key 已在 localStorage 里:初始化序列必须含
@@ -106,8 +159,9 @@ class ConsoleTestCase(unittest.TestCase):
# React 版(2026-10-29 起):旧单页的 setKeyState/loadHealth # React 版(2026-10-29 起):旧单页的 setKeyState/loadHealth
# 序列已不存在,初始化在 useConsoleStore 的 useEffect 里, # 序列已不存在,初始化在 useConsoleStore 的 useEffect 里,
# 有 key 时调 loadRelayModels;断言意图不变,只换成 # 有 key 时调 loadRelayModels;断言意图不变,只换成
# React 构建产物里实际存在的标记。 # React 构建产物里实际存在的标记。断言对象是旧控制台壳,
_status, _headers, body = self._get("/console") # 所以走 /console-classic(/console 只在回落时才出这个壳)。
_status, _headers, body = self._get("/console-classic")
text = body.decode("utf-8") text = body.decode("utf-8")
self.assertIn("loadRelayModels", text) self.assertIn("loadRelayModels", text)
self.assertIn("/relay/models", text) self.assertIn("/relay/models", text)
@@ -148,7 +148,10 @@ class TestMethodAccounting(unittest.TestCase):
# 增量全是 main 侧方法(_routing_shadow、_resolve_create_ # 增量全是 main 侧方法(_routing_shadow、_resolve_create_
# agent_definition、_validate_create_audit_loop),源线方法 # agent_definition、_validate_create_audit_loop),源线方法
# 零丢失、mixin 侧零增减;此为源线 34b9a34 同款 re-pin。 # 零丢失、mixin 侧零增减;此为源线 34b9a34 同款 re-pin。
def test_total_247_no_duplicates(self): # 补漏注:codex 原生 UI 接线新增 2 个 server.py 自有方法
# (_send_redirect、_serve_console_classic),总数 247 -> 249、
# server.py 自有 153 -> 155;mixin 侧零增减,源线方法零丢失。
def test_total_249_no_duplicates(self):
seen = {} seen = {}
dups = [] dups = []
classes = [CloudControlPlaneHandler] classes = [CloudControlPlaneHandler]
@@ -163,9 +166,9 @@ class TestMethodAccounting(unittest.TestCase):
dups.append((seen[name], cls.__name__, name)) dups.append((seen[name], cls.__name__, name))
seen[name] = cls.__name__ seen[name] = cls.__name__
self.assertEqual(dups, []) self.assertEqual(dups, [])
self.assertEqual(total, 247) self.assertEqual(total, 249)
self.assertEqual(len(_own_methods(CloudControlPlaneHandler)), self.assertEqual(len(_own_methods(CloudControlPlaneHandler)),
247 - 94) 249 - 94)
if __name__ == "__main__": if __name__ == "__main__":
+93
View File
@@ -0,0 +1,93 @@
# Codex 原生 UI ↔ codex-app-server 双向对齐表
- 基线:Gitea main `37ba6876bf326ebd6b1e01ae7d2c5684dafe439b`(2026-11-06,线上 code.tose.top 生产基线)
- 工作区:`~/workspace/codex-app-server-codex-ui`,分支 `feat/codex-native-ui`
- UI 栈:`~/workspace/user/downloads/codex-ui-stack/`(zip sha256 `668bcd82…3125e`);`original/` 2026-10-27 自 `/tmp/codex-asar/webview` 原样 vendored(20918 文件,`index.html` sha `85e5c5b4…`),**必须保持字节一致**,改动只发生在 `host/` 响应层与 app-server 侧
- 清点依据:A(UI host 清点)、C(codex 0.164.0-alpha.9 全量 150 方法实测探活)、D(37ba687 基线重核:控制面 **179 个端点**,控制台为 `cloud/console-react/` 源码构建的 353KB React 包)。B 路按旧分支 5ba4d35 清点的数字(60 端点、820 行 console、resident 为 0)**在本基线全部作废**,未采用。
- codex 二进制:`codex-cli 0.164.0-alpha.9`(`/opt/hatch-image/bin/codex`)
- 约定:凡原生 UI 没有对应表面可承载某功能时,本表如实写“原生无对应表面”,不自造页面/组件。
## 集成架构(结论先行)
1. **线程共享是集成点**:`host.py` 一个 app-server 进程托管多 thread,线程状态持久化在 `CODEX_HOME`(默认 `~/.codex`)。UI host 的 `app-server-bridge.mjs` 自己 spawn 一个 app-server,但因共享同一 CODEX_HOME,`thread/list` 能看到 host.py 任务的 thread(C 实测:`thread/list` 返回了既有 thread)。因此 UI 不需要直连 host.py 的 app-server 进程,也不需要新增 socket/ws 附着逻辑。
2. **入口替换方式**:`GET /console` 改为按需 302 到 UI host(默认 `http://127.0.0.1:8095/`,可配 `CODEX_UI_URL`);UI host 不可达时回落到旧控制台。旧 React 控制台**保留**,新路由 `GET /console-classic` 继续 serve `cloud/console.html`(理由见 §3)。
3. **original/ 不进仓**:解压后 650MB / 20918 文件,不可提交。入仓的只有 `host/` 适配层(约 104KB)与启动脚本;`original/` 由 `CODEX_UI_ORIGINAL_ROOT` 指向已下载的栈目录,部署时以 zip sha256 校验(见 §4)。
4. **模型走 relay,不走 mock**:栈自带的 `relay-config.json` 里 `baseUrl` 指向本地 mock(`http://127.0.0.1:8095/mockrelay/v1`),真实 Bumian 地址只写在 JSON 里、host 代码无引用,`apiKey` 为空。当前配置下模型调用走确定性 mock 替身。接真实 relay 须把 `baseUrl` 改为 `relayBaseUrl` 并经环境变量给 key(只写 envKey 名,key 值绝不入文件)。
---
## A 表——UI 要、codex-app-server / 官方 app-server 缺的
| # | UI 需要的方法/功能 | 缺口现状(实测) | 补齐方案与落点 |
|---|---|---|---|
| A1 | `thread/stop` | 官方 0.164.0-alpha.9 不存在(`unknown variant`,C 实测;实验 schema 枚举 172 方法里也没有它) | 桥接层仿真:`app-server-bridge.mjs` 拦截 `thread/stop`,转调 `turn/interrupt`(host.py:939 已确认:对齐 Codex App 停止按钮就是发 `turn/interrupt`)。落点:入仓的 `cloud/codex-ui-host/host/app-server-bridge.mjs` |
| A2 | `thread/rollback` | 同上,不存在 | 桥接层仿真:转调 `thread/revert`(实测存在)。同上落点 |
| A3 | `thread/startAeon` | 同上,不存在 | 桥接层仿真:转调 `thread/start`(Aeon 为实验模式,本二进制无此入口;降级为普通 start,并在方法注释里写明)。同上落点 |
| A4 | `interactive/liveSessions/list`、`interactive/session/upload`、`interactive/sessionSandbox/list`、`interactive/sessionSandbox/read`(4 个) | 同上,均不存在 | 桥接层仿真:`list` 类回空列表、`upload`/`read` 回结构化 `-32601 method not found (interactive sessions not available in codex 0.164.0-alpha.9)`。不伪造会话数据。UI 的 live-session 面若调用,即表现为“无 live 会话”,不静默成功。落点同上 |
| A5 | 实验协议 29 个方法(UI 清单漏列,二进制实测 schema 里有) | UI 清单只有 150 个;`local-host.js` 按清单动态生成 services,漏列的 29 个 UI 调不到。29 个:`account/bedrock/checkGovCloudRequirements`、`account/sendAddCreditsNudgeEmail`、`account/workspaceMessages/read`、`command/exec/resize|terminate|write`、`environment/add`、`environment/status`、`externalAgentConfig/import/recordHistory`、`plugin/share/checkout`、`server/diagnostics`、`thread/backgroundTerminals/list|terminate`、`thread/decrement_elicitation|increment_elicitation`、`thread/items/read`、`thread/prediction/request`、`thread/readState/update`、`thread/realtime/appendAudio|appendSpeech|appendText|listVoices|start|stop`、`userVerification/cancel|delete|enroll|status|verify` | 把 29 个追加进 `appserver-methods.json`(150 → 179 个,与控制面端点数巧合相同,无关联)。只加清单、不改 `original/`。其中 `realtime/*` 与 `userVerification/*` 在本机未逐个探活,标注“仅 schema 有名,未探活”。落点:入仓的 `cloud/codex-ui-host/host/appserver-methods.json` |
| A6 | 桥接在 Linux 上找到 codex 二进制 | `app-server-bridge.mjs` 的默认候选只有 `$CODEX_APP_SERVER_BIN` 与两个 macOS ChatGPT 内路径;在 Linux 上不设环境变量即报 `codex binary not found`(C 实测) | 入仓的 bridge 增加候选:`$CODEX_BIN`(host.py 同款环境变量名)→ `/opt/hatch-image/bin/codex` → PATH 里的 `codex`。落点同 A1 |
| A7 | UI 要 `git.request("stable-metadata")`(Send 门控靠它非空) | `local-host.js` 里 git 是 stub,固定回 `{available:false, …}`,其余回 null | 桥接层补只读 git 能力:`local-host.js` 的 `git.request` 改为调 host 新增端点 `POST /__codex_git`(只接受 `stable-metadata` / `branch` / `status` 三种只读 op,在给定 cwd 内跑 `git` 只读命令,拒绝写操作与 cwd 外路径)。落点:入仓的 `cloud/codex-ui-host/host/local-host.js` + `web-server.mjs` |
| A8 | UI 要终端面 | `local-host.js:274` 把 `terminal` 服务显式置 null;进程类能力只剩 app-server 的 `process/*` / `thread/shellCommand` | 不补终端面:原生 UI 的终端服务在本栈是 stub,app-server 的 `process/spawn|writeStdin|resizePty|kill` 实测存在(缺参但方法存在),UI 若经 `process/*` 调用即走真实进程。实现时核实 UI 是否调用 `terminal` 服务:若调用,记为“原生终端面在本栈不可用”,不造终端组件。落点:验证阶段核实,不写代码 |
| A9 | UI 要账号 / 用量 / 远控 | `account/rateLimits/read`、`account/usage/read`、`plugin/share/list`、`remoteControl/enable|disable|pairing/start` 需 ChatGPT 账号登录(C 实测);当前 provider(relay/apikey)不用 gateway OAuth,`account/gatewayOAuth/login` 不可用 | **不可补**:这是 provider/账号限制,不是代码缺口。UI 的 `mode-decide` 改写已强制跳过登录直进 app,本栈账号面实际不可达。实现不改动,验收时以实测报错为准 |
| A10 | UI host 无鉴权 vs 控制面要求 `X-API-Key` | UI host 全部端点同源、无鉴权,只绑 `127.0.0.1:8095`;控制面除 `/health`、`/console`、`/oauth/*`、`/webhooks/*` 外一律要求 key | 不给 UI host 加自造登录面:保持 loopback-only 绑定;`/console` 的 302 只发生在控制面已鉴权(或本机回环)的请求之后——实现方案:`cloud/server.py` 的 `/console` 在 302 前先做一次 UI host 健康探测(`GET /` 2 秒超时),探测失败则回落旧控制台;UI host 本身不暴露到外网。密钥不参与 UI host 鉴权 |
| A11 | UI 要 `config/read` 含 relay 覆盖、`model/list` | C 实测均 ok;`config/read` 返回 `model=gpt-6.1-sol`、`model_provider=openkun`(与本机 `~/.codex/config.toml` 一致);`model/list` 仅一条 | 无缺口:桥接的 `-c` 覆盖(`relayArgs`)保持原样;`verify.mjs --deep` 第 13 项即校验此点。实现不改动 |
## B 表——app-server 有、UI 缺的(控制面 179 个端点 → 原生 UI 表面)
控制面分组(D 实测,179 = 任务 66 + 项目/环境/上传 17 + 租户配额/管理 11 + OAuth/身份/key 14 + GitHub App/webhook 7 + resident 7 + automations 10 + 记忆/KB/成本/推送 23 + worker/监控/模型 10 + 其它 full-auto 9 + todos 5):
| # | 控制面功能(端点) | 原生 UI 对应表面 | 结论 |
|---|---|---|---|
| B1 | 任务创建 / 列表 / 详情(`POST /tasks`、`GET /tasks`、`GET /tasks/{id}`) | 线程列表(`thread/list`)与线程阅读(`thread/read`、`thread/items/list`)是**线程**,不是**任务**;任务含 prompt/执行器/模型/goal/分支/worktree 等宿主字段,原生 UI 无任务队列面 | 原生无对应表面(任务列表、建任务表单)。任务的线程内容可经 `thread/read`、`thread/items/list` 看到,属部分映射,不等同 |
| B2 | 任务操作:取消/暂停/重试/合并/发布/提 PR/分叉/继续(`POST /tasks/{id}/{cancel,pause,retry,merge,release,pr,fork,continue}`) | 取消→`turn/interrupt`(+ A1 的 `thread/stop` 仿真);重试/继续→`thread/resume`、`thread/start`;分叉→`thread/fork` | 部分映射:cancel/retry/fork/continue 有方法级对应;pause/release/merge/pr **原生无对应表面**(合并与 PR 是宿主 Git 流程,原生 UI 无此概念) |
| B3 | 任务 trace(`GET /tasks/{id}/trace`、`/trace/stream` SSE)、时间线、成本、结果全文、agents、文件/目录树(`/files`、`/tree`) | `thread/timeline/list`(trace 的部分映射)、`thread/items/list`(内容)、`fs/readDirectory`、`fs/readFile`(文件浏览的部分映射) | 部分映射:trace/时间线/文件有近似方法;成本(`GET /tasks/{id}/cost`)、agents 分工、SSE trace 流 **原生无对应表面** |
| B4 | 任务线程管理(`/threads`、`/thread`、`/thread/{rename,archive,unarchive,compact,message}`、`/chat-history`、`/messages`、followups、reviews、approvals、handoff、refs、worktree、audit-loop、goal、sleep/wait) | `thread/name/set`、`thread/archive|unarchive`、`thread/compact/start`、`thread/items/list`、`review/start`、`thread/approveGuardianDeniedAction` | 部分映射:改名/归档/压缩/评审启动/审批有方法级对应;followups、handoff、refs、worktree 快照、audit-loop 裁决/修复、goal 续跑、sleep/wait、任务间 messages **原生无对应表面** |
| B5 | 浏览器远控(`/tasks/{id}/browser/{state,stream,input,control}`) | 无。原生 UI 的浏览器面是它自己的渲染面,不接管外部浏览器 | 原生无对应表面 |
| B6 | 项目与成员(`POST/GET /projects`、`PATCH /projects/{id}`、`/projects/{id}/members`) | `project/list|read|create|import|update|delete|move` 是 codex 本地工程目录,不是云端多租户项目;成员/角色模型(viewer/contributor/maintainer/owner)原生 UI 没有 | 原生无对应表面(云端项目制与成员管理) |
| B7 | 环境(`POST/GET /environments`、`/environments/{id}/archive`) | `environment/info` 是单机执行环境信息,不是“项目仓库命名基准检出” | 原生无对应表面 |
| B8 | 租户配额 / 管理(`GET /users/me/quota`、`/admin/quotas`、`/admin/sweep`、`/admin/backup`、`/admin/audit-ledger/*`、`/admin/push/dispatch`、`/admin/cost-by-creator`、`/admin/ops/approval-timeout`、`/users/me/cost`) | `account/usage/read`、`account/rateLimits/read` 需 ChatGPT 登录且不可用(A9);其余无 | 原生无对应表面 |
| B9 | OAuth / 身份 / API key(`/oauth/{provider}/*`、`/users/me/keys`、`/users/me/profile`、`/users/me/gateway/rotate`、`/github-app/*`) | `account/login/start`、`account/logout` 存在,但本栈 `mode-decide` 改写跳过登录直进 app,账号面实际不可达;key 管理无 | 原生无对应表面(OAuth 与本站 key 管理) |
| B10 | GitHub webhook / Slack webhook(`POST /webhooks/github|slack`)、GitHub 仓库列表(`GET /github/repos`) | 无 | 原生无对应表面 |
| B11 | resident 常驻监听(7 个端点,`POST/GET/PATCH /projects/{id}/resident-watches`、pause/resume/end) | 无。控制台 `console-react/src/` 全文 0 处 `resident`,连旧控制台都没有这个面 | 原生无对应表面 |
| B12 | automations 与分诊(10 个端点,`/projects/{id}/automations`、bind/heartbeat/cancel、`/triage`) | 无。`automations.py` 的自动化是云端定时建任务;原生 UI 无自动化面(host.py 的本地 automations 是另一套) | 原生无对应表面(bind/heartbeat 连旧控制台都没调) |
| B13 | full-auto(9 个端点,`/full-auto/{status,config-plan,validate-action,merge-gate,ci-fix,release-plan,rollback-plan,execute,audit}`) | 无 | 原生无对应表面 |
| B14 | 项目记忆 / KB / 成本 / 推送(23 个端点,`/projects/{id}/{memory,kb,cost,push/subscriptions}`、`/users/me/memories`、`/users/me/identity-context`、`/users/me/auto-memory*`) | `memory/status|reset` 是 codex 本地记忆状态,不是项目记忆库;`skills/list` 与项目技能清单(`GET /projects/{id}/skills`)部分对应 | 部分映射:项目技能→`skills/list`;其余(项目记忆/KB/成本/推送订阅/自动记忆)**原生无对应表面** |
| B15 | 技能 / 应用 / MCP(`GET /projects/{id}/{skills,apps,mcp-servers}`、`POST …/mcp-servers/reload|oauth-login`) | `skills/list`、`plugin/list|installed`、`app/list|read`、`mcpServerStatus/list`、`mcpServer/tool/call`、`config/mcpServer/reload` | 覆盖:技能/插件/应用/MCP 有方法级对应(`plugin/read` 失败时 host 有离线回落) |
| 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`) |
## §3 旧 console.html 的去留决定
**决定:保留旧控制台,改路由为 `GET /console-classic`,`GET /console` 改为新 UI 入口(302 到 UI host,不可达时回落旧控制台)。**
理由:
1. 旧控制台是 React 构建产物(`cloud/console-react/`,46 个源码文件),承载 B 表里大量“原生无对应表面”的管理面(配额/管理、resident、automations、full-auto、审计、成员、待办等)。悄悄删除等于删除功能,违反“不删除较新工作”。
2. 原生 UI 是单用户线程界面,无法承载多租户控制面;“替换”指的是默认入口,不是不留后路。
3. `test_console.py`(5 个测试)只验壳与路由,不跑浏览器 JS;路由改动须同步更新这 5 个测试(`/console` 无 key 200 出 HTML 的断言要改为 302/回落行为,`/console-classic` 承接原断言),并新增 UI host 相关测试。
## §4 入仓清单(实现范围)
1. `cloud/codex-ui-host/`:自 `~/workspace/user/downloads/codex-ui-stack/` 拷入 `host/` 全部 12 个文件 + `start.sh` + `verify.mjs`(原样,改动只在下述适配处;`original/` 不入仓)。
- 适配 A1–A4:`app-server-bridge.mjs` 增加 7 个缺失方法的仿真分支。
- 适配 A5:`appserver-methods.json` 追加 29 个方法(150 → 179)。
- 适配 A6:`app-server-bridge.mjs` 增加 `$CODEX_BIN`、`/opt/hatch-image/bin/codex`、PATH 候选。
- 适配 A7:`local-host.js` 的 `git.request` 走新增 `POST /__codex_git`(只读三 op);`web-server.mjs` 增加该端点。
- 适配:`relay-config.json` 保持栈自带 Bumian 配置原样(`apiKey` 为空),另加 `relay-config.README.md` 说明 mock 现状与接真实 relay 的两步(`baseUrl` 改为 `relayBaseUrl` + 设 `BUMIAN_API_KEY` 环境变量);不换 provider、不写 key。
- 适配:`projectless.mjs` 的默认工作区根在 Linux 上改为 `$CODEX_WEB_WORKSPACE_ROOT` 或 `~/Codex`(原 `~/Documents/Codex` 在服务器上不存在)。
2. `cloud/server.py`:`GET /console` 改为 302 到 `CODEX_UI_URL`(默认 `http://127.0.0.1:8095/`),2 秒探测失败回落旧控制台;新增 `GET /console-classic` serve `cloud/console.html`(原 `/console` 行为整体平移)。
3. `scripts/run-codex-ui.sh`(新建):启动 UI host 的唯一入口,设置 `CODEX_APP_SERVER_BIN`、`CODEX_UI_ORIGINAL_ROOT`(默认已下载的栈目录),`exec node cloud/codex-ui-host/host/web-server.mjs --port ${UI_PORT:-8095}`。
4. 测试:`cloud/tests/test_console.py` 按 §3 更新;新增 `cloud/tests/test_codex_ui_host.py`(Python 侧:方法表计数 179、7 个仿真方法在 bridge 里存在、original/ 未被入仓——即仓内无 `cloud/codex-ui-host/original/`、启动脚本存在且可执行)。
5. 验证:`python3 run_tests.py cloud` 相关子集 + `node verify.mjs`(shallow)+ `node verify.mjs --deep`(真实 app-server 可用时)+ 真实联调冒烟(起 :8095,`thread/list` 与等价调用,日志为证)。
## §4.1 实现注记(实现后补记,与规范同等效力)
- A5:`appserver-methods.json` 是纯 JSON,加文件头注释会破坏合法性,故注释未写进 JSON 本身;“`realtime/*` 与 `userVerification/*` 仅 schema 有名、未逐个探活”记在这里(本节),与 A5 表格同效。
- A4 口径:以本表 A4 为准——`list` 类(`interactive/liveSessions/list` 与 `interactive/sessionSandbox/list`)回空列表;`upload` 与 `sessionSandbox/read` 回结构化 `-32601`。实现初版曾把 `sessionSandbox/list` 划入 `-32601` 组,协调员已按本表纠正。
- `web-server.mjs` 的 `--root` 默认值增加了 `CODEX_UI_ORIGINAL_ROOT` 回落(1 行):不加则启动脚本设该变量无效;接受该改动,启动脚本无需改传 `--root`。
## §5 剩余缺口(实现后仍存在,如实记录)
- B 表里标记“原生无对应表面”的功能(B1 任务队列、B6 项目制、B7 环境、B8 配额/管理、B9 OAuth/key、B10 webhook、B11 resident、B12 automations、B13 full-auto、B14 项目记忆/KB/成本/推送大部、B17 待办、B5 浏览器远控)**没有**原生界面承载,仍走 `/console-classic`(旧 React 控制台)。这是按用户要求不自造界面的直接结果,不是遗漏。
- A9(ChatGPT 登录类方法)不可补;A8 终端面待验证阶段核实。
- `original/` 不进仓:部署时须另行提供与 zip sha256 一致的 `original/` 目录,否则 UI host 无法启动;这一步属于部署(须用户点头),不在本轮本地完成范围内。
+29
View File
@@ -0,0 +1,29 @@
#!/usr/bin/env bash
# Start the Codex native UI host — the only entry point for the UI host.
#
# scripts/run-codex-ui.sh UI on :8095
# UI_PORT=9000 scripts/run-codex-ui.sh custom port
#
# Env (all optional, no secrets in this file):
# CODEX_APP_SERVER_BIN codex binary for the app-server bridge
# (default /opt/hatch-image/bin/codex)
# CODEX_UI_ORIGINAL_ROOT vendored original/ webview directory
# (default ~/workspace/user/downloads/codex-ui-stack/original;
# original/ is never committed — see docs/codex-native-ui-alignment.md §4)
# UI_PORT port to listen on (default 8095)
# BUMIAN_API_KEY real-relay key, only when relay-config.json points
# at the real relay (see host/relay-config.README.md)
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
UI_PORT="${UI_PORT:-8095}"
export CODEX_APP_SERVER_BIN="${CODEX_APP_SERVER_BIN:-/opt/hatch-image/bin/codex}"
export CODEX_UI_ORIGINAL_ROOT="${CODEX_UI_ORIGINAL_ROOT:-$HOME/workspace/user/downloads/codex-ui-stack/original}"
export CODEX_WEB_PORT="$UI_PORT"
echo "[codex-ui] UI http://127.0.0.1:${UI_PORT}/"
echo "[codex-ui] codex ${CODEX_APP_SERVER_BIN}"
echo "[codex-ui] root ${CODEX_UI_ORIGINAL_ROOT}"
exec node "$REPO_ROOT/cloud/codex-ui-host/host/web-server.mjs" --port "${UI_PORT}"