feat: add code queue submit cli

This commit is contained in:
Codex
2026-05-17 05:49:27 +00:00
parent 68a14cdd4e
commit 2b2a327301
5 changed files with 250 additions and 9 deletions
+4 -2
View File
@@ -23,7 +23,7 @@ UniDesk 是一个以主 server 为统一入口的分布式工作平台;本文
## CLI
- `bun scripts/cli.ts help`:输出所有可用命令的 JSON 索引,详细规范见 `docs/reference/cli.md`
- `bun scripts/cli.ts --main-server-ip <ip> <command>`:默认通过公网 frontend 登录态远程执行调试、用户服务(底层命令名 `microservice`)、`codex task <taskId>` 与节点自测命令,不要求主 server SSH key,详细规范见 `docs/reference/cli.md`
- `bun scripts/cli.ts --main-server-ip <ip> <command>`:默认通过公网 frontend 登录态远程执行调试、用户服务(底层命令名 `microservice`)、Code Queue 查询与节点自测命令,不要求主 server SSH key,详细规范见 `docs/reference/cli.md`
- `bun scripts/cli.ts config show`:校验并展示根目录 `config.json`,配置来源规则见 `docs/reference/config.md`
- `bun scripts/cli.ts check`:运行配置、TypeScript、文件存在性和 Docker Compose 配置检查,测试入口见 `TEST.md`
- `bun scripts/cli.ts server start`:以异步 job 启动 database、backend-core、frontend、provider-gateway 和主 server 用户服务,部署规则见 `docs/reference/deployment.md`
@@ -32,11 +32,13 @@ UniDesk 是一个以主 server 为统一入口的分布式工作平台;本文
- `bun scripts/cli.ts server rebuild <backend-core|frontend|provider-gateway|todo-note|project-manager|baidu-netdisk|oa-event-flow>`:以 build-first、Compose lock、no-deps force-recreate 和 post-up validation 的异步 job 重建主 server Compose 内单个服务;Code Queue 部署在 D601,规则见 `docs/reference/deployment.md`
- `bun scripts/cli.ts provider attach <providerId> [--master-server URL] [--up] [--force]`:在新增计算节点上生成两项配置的 provider-gateway 挂载包;默认只需要主 server URL(默认 `http://74.48.78.17/`)和唯一 Provider ID,生成的 Compose 固定 Docker socket、`pid: "host"``restart: always`、只读 `/workspace`、SSH 维护私钥挂载和 loopback egress proxy 端口,规则见 `docs/reference/provider-gateway.md`
- `bun scripts/cli.ts ssh <providerId> [ssh-like args...]`:通过 provider-gateway 的 Host SSH / WSL SSH 维护桥打开近似原生 ssh 的交互会话或远端命令,并在远端 PATH 注入 `apply_patch``glob``skill-discover``apply-patch``py``skills`、结构化 `find``glob``argv` 子命令用于避免远端补丁、Python stdin、skill 发现与常用只读命令的嵌套转义问题,使用规则见 `docs/reference/cli.md``docs/reference/provider-gateway.md`
- `bun scripts/cli.ts microservice list/status/health/proxy`:管理和验证挂载在主 server、计算节点 Docker 或 k3s 控制面上的用户服务,OA Event Flow/Todo Note/Baidu Netdisk on main-server、k3s Control/Code Queue/MDTODO/FindJob/Pipeline/MET Nonlinear on D601 的规则见 `docs/reference/microservices.md`
- `bun scripts/cli.ts microservice list/status/health/proxy`:管理和验证挂载在主 server、计算节点 Docker 或 k3s 控制面上的用户服务,`proxy` 支持受控 JSON bodyOA Event Flow/Todo Note/Baidu Netdisk on main-server、k3s Control/Code Queue/MDTODO/FindJob/Pipeline/MET Nonlinear on D601 的规则见 `docs/reference/microservices.md`
- `bun scripts/cli.ts deploy check/plan/apply [--file deploy.json] [--service <id>]`:按根目录 `deploy.json` 的服务 repo 和 commit 期望状态校验或更新用户服务,目标侧自行 fetch、构建、部署和 live commit 验证;规则见 `docs/reference/deploy.md`
- `bun scripts/cli.ts codex deploy <commitId>`:Code Queue 兼容部署入口,会生成临时 desired manifest 并调用 `deploy apply --service code-queue` 的同一条 target-side build 与 live commit 验证路径;规则见 `docs/reference/codex-deploy.md`
- `bun scripts/cli.ts codex submit [prompt] [--prompt-file path|--prompt-stdin] [--queue <id>]`:通过 backend-core 私有代理提交 Code Queue 任务,`--dry-run` 可只检查请求体不入队,规则见 `docs/reference/cli.md`
- `bun scripts/cli.ts codex task <taskId>`:按 Code Queue 任务 ID 查询初始 prompt、最后 assistant message、工具调用摘要、attempt/judge/error 和耗时,便于新任务引用历史 session。
- `bun scripts/cli.ts codex judge <taskId> --attempt <n> [--dry-run]`:按指定 task/attempt 用与队列 worker 相同的上下文构建和 MiniMax judge 调用路径单步复现完成判定;`--dry-run` 只输出 prompt/payload 诊断。
- `bun scripts/cli.ts codex interrupt|cancel <taskId>`:通过 Code Queue 私有代理中断运行任务或取消 queued/retry_wait 任务,规则见 `docs/reference/cli.md`
- `bun scripts/cli.ts server stop`:以异步 job 停止固定 Compose 项目中的全部 UniDesk 服务,停止后用 `server status` 复核。
- `bun scripts/cli.ts job list` / `bun scripts/cli.ts job status latest`:查询 `.state/jobs/` 中的异步任务状态,job 机制见 `docs/reference/cli.md`
- `bun scripts/cli.ts debug health` / `bun scripts/cli.ts debug dispatch` / `bun scripts/cli.ts debug task`:通过 Docker 内网 core、真实 HTTP、WebSocket、系统指标、Docker 状态和 Host SSH 维护桥流程调试健康检查、任务下发与任务结果,调试规则见 `docs/reference/cli.md`
+4 -2
View File
@@ -18,13 +18,15 @@ UniDesk 的统一 CLI 入口是根目录 `scripts/cli.ts`,运行方式固定
- `ssh <providerId> apply-patch [tool args...] < patch.diff` 直接调用远端注入的 `apply_patch` 工具,并把本地 stdin 中的标准 `*** Begin Patch` / `*** End Patch` patch 流透传给目标节点。
- `ssh <providerId> py [script-args...] < script.py` 把本地 stdin 落到远端临时 `.py` 文件后再以 `python3 -u` 执行并自动清理,避免再手写 `'python3 -'`、heredoc 或多层引号;`script-args` 会按 argv 安全透传给远端脚本。
- `ssh <providerId> skills [--scope all|wsl|windows] [--limit N]` 发现目标节点上的 WSL/Linux skill 根目录;当 provider 是 WSL 时同一次调用还会扫描 Windows 用户目录下的 `.agents/skills``.codex/skills`
- `microservice list/status/health/proxy` 通过 backend-core 内网 API 管理挂载在计算节点 Docker 中的用户服务(底层命令名仍为 microservice);`health``proxy` 会走真实 backend-core -> provider-gateway -> 节点本机后端链路,`proxy` 对超大 body 默认输出有界预览,规则见 `docs/reference/microservices.md`
- `microservice list/status/health/proxy` 通过 backend-core 内网 API 管理挂载在计算节点 Docker 或 k3s 控制面中的用户服务(底层命令名仍为 microservice);`health``proxy` 会走真实 backend-core -> provider-gateway 或 k3sctl-adapter -> 节点服务链路,`proxy` 支持受控 JSON 请求体并对超大响应 body 默认输出有界预览,规则见 `docs/reference/microservices.md`
- `deploy check/plan/apply` 从根目录 `deploy.json` 读取服务 repo 与 commit 期望状态,join `config.json` 和现有 manifest 后使用 target-side build 单一路径校验或更新直管服务与 k3s 代管服务;规则见 `docs/reference/deploy.md`
- `codex deploy <commitId>` 是 Code Queue 兼容部署入口,会生成临时 desired manifest 并调用 `deploy apply --service code-queue` 的同一条 target-side build、k3s import、rollout 和 live commit 验证路径;详细规则见 `docs/reference/codex-deploy.md`
- `codex submit [prompt] [--prompt-file path|--prompt-stdin] [--queue queueId] [--provider-id id] [--cwd path] [--model model] [--reasoning-effort effort] [--execution-mode mode] [--max-attempts N] [--reference-task-id id] [--dry-run]` 通过 backend-core 私有代理向 Code Queue 提交任务;prompt 必须且只能来自位置参数、文件或 stdin 之一,`--dry-run` 只返回结构化请求与 prompt 预览,不实际入队。
- `codex task <taskId>` 通过 Code Queue 私有代理按任务 ID 查询结构化执行摘要;默认只返回有界 prompt/response 预览、执行 Provider、工作目录、最后 assistant message、最近工具调用摘要、attempt、judge、错误、耗时和 trace 翻页提示,适合在新队列任务中引用历史 session 且避免噪声爆炸。
- `codex task <taskId> --trace --tail|--from-start|--after-seq N|--before-seq N --limit N` 按页拉取 Code Queue 的逻辑 trace;响应会返回 `nextAfterSeq``previousBeforeSeq``hasMore``hasBefore` 和下一页/上一页命令,默认 `--trace` 取最新一页,需要完整 prompt/最后 response 时加 `--full`
- `codex output <taskId> --tail|--from-start|--after-seq N|--before-seq N --limit N [--full-text]` 按原始 output seq 分页读取底层记录;当 trace 行提示 `commandOmittedLines``bodyOmittedLines``rawSeqs` 时,用该命令按 seq 补取完整信息,默认仍有单条文本预览上限,显式 `--full-text` 才返回该页全文。
- `codex judge <taskId> --attempt N [--dry-run] [--include-prompt]` 通过 Code Queue 私有代理按指定 attempt 单步复现 judge;后端会从 PostgreSQL task JSON 与 output 归档重建该 attempt 在真实队列 worker 中的 `QueueTask`/`CodexRunResult`,再调用同一套 judge prompt builder 和 MiniMax 请求路径。默认会真实调用 MiniMax,`--dry-run` 只返回 prompt/payload 大小、attempt 窗口和重建来源诊断,`--include-prompt` 仅用于本地深度排查。
- `codex interrupt|cancel <taskId>` 通过 Code Queue 私有代理请求中断;running/judging 任务会请求当前 agent run 停止,queued/retry_wait 任务会直接转为 canceled,返回有界 task 摘要和后续查询命令。
- Code Queue 多队列 lane 由 `codex` 命令命名空间管理:`queues` 列表、`queue create <queueId>` 创建、`queue merge <sourceQueueId> --into <targetQueueId>` 合并、`move <taskId> --queue <queueId>` 迁移;同一个 queue 内部串行执行,不同 queue 之间并行执行。合并会移动任务归属并自动删除源 queue 记录,只保留合并后的目标 queue;合并后的目标 queue 按任务原 `queueEnteredAt`/`createdAt` 时间顺序串行。迁移 queued/retry_wait 任务后会立即调度目标 queue。
- `job list``job status` 查询 `.state/jobs/` 文件系统状态,是异步命令的可观测入口。
- `debug health``debug dispatch``debug task` 走真实内部 core、WebSocket、数据库、provider、系统指标、Docker 状态和 Host SSH 维护桥流程,只用于开发调试,不写入 `TEST.md` 的正式验收步骤。
@@ -42,7 +44,7 @@ UniDesk 的统一 CLI 入口是根目录 `scripts/cli.ts`,运行方式固定
每条命令的最外层 JSON 包含 `ok``command``data``error`。失败时 CLI 设置非零退出码,但仍然输出 JSON 错误对象;错误对象应包含 `name``message` 和可用的 `stack`
`microservice proxy` 是面向人工验证的私有后端读取入口。正式写入型用户服务操作由 frontend 同源代理或 E2E 直接调用 backend-core 完成,并由 config 中的 `allowedMethods` 限制;CLI `proxy` 默认仍作为 GET/HEAD 读取验证入口,必要时可显式加 `--method POST|PUT|PATCH|DELETE` 调用无需自定义请求体的受控调试/自测端点,例如 `bun scripts/cli.ts microservice proxy baidu-netdisk /api/self-test --method POST --raw`。为了避免 Pipeline snapshot 这类超大业务 JSON 造成 CLI 输出爆炸,响应 body 超过默认阈值时会返回 `bodyOmitted=true``bodyPreview``bodyBytes``rawHint`;需要完整 body 时显式添加 `--raw`,或用 `--max-body-bytes <N>` 调整预览阈值。正式 frontend 展示仍应优先使用业务控件和 `__unideskArrayLimit` 这类展示级裁剪参数,而不是默认倾倒完整 JSON。
`microservice proxy` 是面向人工验证和受控调试的私有后端入口。默认 method 为 GET;使用 `--body-json JSON``--body-file path``--body-stdin` 默认 method 切换为 POST,也可显式加 `--method POST|PUT|PATCH|DELETE`,但 GET/HEAD 不允许携带请求体。所有请求仍受 config 中的 `allowedMethods``allowedPathPrefixes` 限制。为了避免 Pipeline snapshot 这类超大业务 JSON 造成 CLI 输出爆炸,响应 body 超过默认阈值时会返回 `bodyOmitted=true``bodyPreview``bodyBytes``rawHint`;需要完整 body 时显式添加 `--raw`,或用 `--max-body-bytes <N>` 调整预览阈值。正式 frontend 展示仍应优先使用业务控件和 `__unideskArrayLimit` 这类展示级裁剪参数,而不是默认倾倒完整 JSON。
`network perf` 用于生成组网性能前后对比数据。标准 Code Queue overview 读路径基准命令是 `bun scripts/cli.ts network perf --service code-queue --path /api/tasks/overview?limit=30 --count 30 --concurrency 1 --label before`,远程主 server 可用 `bun scripts/cli.ts --main-server-ip 74.48.78.17 network perf ...`。输出包含成功/失败数、状态码分布、`x-unidesk-cache``x-unidesk-proxy-mode``x-unidesk-upstream-proxy-mode` 分布和 min/p50/p90/p95/maxprovider-gateway 长连接数据面验收应看到 `proxyModeCounts.provider-ws-http-tunnel`adapter native Service 数据面验收应看到 upstream proxy mode 为 `kubernetes-native-service`,若出现 `kubernetes-api-service-proxy` 必须结合 `/api/control-plane.nativeServiceProxy.failedServices` 解释 fallback 原因。
+3 -1
View File
@@ -43,14 +43,16 @@ function help(): unknown {
{ command: "microservice list", description: "List UniDesk-managed user services and their provider/runtime mapping." },
{ command: "microservice status <id>", description: "Show one user service config, repository reference, backend mapping, and runtime status." },
{ command: "microservice health <id>", description: "Probe one user service through backend-core -> provider-gateway HTTP proxy." },
{ command: "microservice proxy <id> <path> [--method GET|POST|PUT|PATCH|DELETE] [--raw] [--max-body-bytes N]", description: "Access a private user-service backend path through the same frontend-only proxy used by WebUI; large bodies are summarized unless --raw is set." },
{ command: "microservice proxy <id> <path> [--method GET|POST|PUT|PATCH|DELETE] [--body-json JSON|--body-file path|--body-stdin] [--raw] [--max-body-bytes N]", description: "Access a private user-service backend path through the same frontend-only proxy used by WebUI; JSON request bodies are supported for controlled write/debug endpoints." },
{ command: "deploy check|plan|apply [--file deploy.json] [--service id] [--dry-run] [--force]", description: "Reconcile services from a repo+commit manifest using target-side build and live commit verification." },
{ command: "schedule list|get|runs|run|delete", description: "Manage backend-core scheduled tasks and run history; schedule run <id> supports --wait-ms N." },
{ command: "schedule upsert-pgdata-backup [--time HH:MM] [--remote-base /SERVER_DATA/UNIDESK_PG_DATA]", description: "Create or update the daily PGDATA physical backup task that uploads monthly rotated archives to Baidu Netdisk." },
{ command: "codex deploy <commitId> [--provider-id D601] [--timeout-ms N]", description: "Compatibility wrapper for deploy apply --service code-queue with a temporary repo+commit manifest." },
{ command: "codex submit [prompt] [--prompt-file path|--prompt-stdin] [--queue queueId] [--provider-id id] [--cwd path] [--model model] [--execution-mode mode] [--max-attempts N] [--reference-task-id id] [--dry-run]", description: "Submit a Code Queue task through backend-core -> code-queue proxy; --dry-run shows the structured request without enqueueing." },
{ command: "codex task <taskId> [--trace --tail|--from-start|--after-seq N|--before-seq N --limit N] [--full]", description: "Fetch a compact Code Queue task summary; trace rows are opt-in and paged with next/previous commands to avoid output explosion." },
{ command: "codex output <taskId> [--tail|--from-start|--after-seq N|--before-seq N --limit N] [--full-text]", description: "Fetch paged raw Code Queue output records by seq when a trace row has omitted command/output text." },
{ command: "codex judge <taskId> --attempt N [--dry-run] [--include-prompt]", description: "Replay one stored Code Queue attempt through the same judge context builder and MiniMax judge call path used by the live queue worker." },
{ command: "codex interrupt|cancel <taskId>", description: "Request interrupt for a running Code Queue task, or cancel a queued/retry_wait task, through the same private proxy." },
{ command: "codex (queues | queue create <queueId> | queue merge <sourceQueueId> --into <targetQueueId> | move <taskId> --queue <queueId>)", description: "List/create/merge Code Queue lanes and move a queued task; merge preserves task queue time order and deletes the source queue record." },
{ command: "job list", description: "List async jobs from .state/jobs." },
{ command: "job status <jobId|latest> [--tail-bytes N]", description: "Show job state with bounded stdout/stderr tails." },
+205 -1
View File
@@ -1,3 +1,4 @@
import { readFileSync } from "node:fs";
import { type UniDeskConfig } from "./config";
import { coreInternalFetch } from "./microservices";
@@ -33,6 +34,19 @@ interface CodexJudgeOptions {
includePrompt: boolean;
}
interface CodexSubmitOptions {
prompt: string;
queueId: string | undefined;
providerId: string | undefined;
cwd: string | undefined;
model: string | undefined;
reasoningEffort: string | undefined;
executionMode: string | undefined;
maxAttempts: number | undefined;
referenceTaskIds: string[];
dryRun: boolean;
}
type CodexRequestInit = { method?: string; body?: unknown };
type CodexResponseFetcher = (path: string, init?: CodexRequestInit) => unknown;
type AsyncCodexResponseFetcher = (path: string, init?: CodexRequestInit) => Promise<unknown>;
@@ -66,6 +80,16 @@ function stringList(value: unknown): string[] {
return asArray(value).map((item) => String(item ?? "")).filter((item) => item.length > 0);
}
function textPreview(value: string, maxChars: number): Record<string, unknown> {
const truncated = value.length > maxChars;
return {
text: truncated ? value.slice(0, maxChars) : value,
chars: value.length,
truncated,
omittedChars: truncated ? value.length - maxChars : 0,
};
}
function fmtDuration(ms: unknown): string {
const value = Number(ms);
if (!Number.isFinite(value) || value < 0) return "--";
@@ -624,6 +648,19 @@ function optionValue(args: string[], names: string[]): string | undefined {
return undefined;
}
function optionValues(args: string[], names: string[]): string[] {
const values: string[] = [];
for (let index = 0; index < args.length; index += 1) {
const name = args[index] ?? "";
if (!names.includes(name)) continue;
const raw = args[index + 1];
if (raw === undefined || raw.trim().length === 0) throw new Error(`${name} requires a non-empty value`);
values.push(raw.trim());
index += 1;
}
return values;
}
function positionalArgs(args: string[]): string[] {
const positions: string[] = [];
for (let index = 0; index < args.length; index += 1) {
@@ -637,6 +674,19 @@ function positionalArgs(args: string[]): string[] {
return positions;
}
function positionalArgsWithValueOptions(args: string[], valueOptions: Set<string>): string[] {
const positions: string[] = [];
for (let index = 0; index < args.length; index += 1) {
const value = args[index] ?? "";
if (value.startsWith("--")) {
if (valueOptions.has(value)) index += 1;
continue;
}
positions.push(value);
}
return positions;
}
function requireMergeSourceQueueId(args: string[], command: string): string {
const raw = optionValue(args, ["--source", "--from", "--queue"]) ?? positionalArgs(args)[0];
if (raw === undefined || raw.trim().length === 0) throw new Error(`${command} requires source queue id, for example: codex queue merge old --into default`);
@@ -665,8 +715,158 @@ function codexMoveTask(taskId: string, queueId: string): unknown {
return unwrapCodexResponse(coreInternalFetch(`/api/microservices/code-queue/proxy/api/tasks/${encodeURIComponent(taskId)}/move`, { method: "POST", body: { queueId } }));
}
function promptFromSubmitArgs(args: string[]): string {
const promptFile = optionValue(args, ["--prompt-file", "--file"]);
const promptStdin = hasFlag(args, "--prompt-stdin") || hasFlag(args, "--stdin");
const promptArgs = positionalArgsWithValueOptions(args, new Set([
"--prompt-file",
"--file",
"--queue",
"--queue-id",
"--provider",
"--provider-id",
"--cwd",
"--workdir",
"--model",
"--reasoning-effort",
"--execution-mode",
"--mode",
"--max-attempts",
"--reference-task-id",
"--reference",
"--ref",
]));
const sources = [promptFile !== undefined, promptStdin, promptArgs.length > 0].filter(Boolean).length;
if (sources !== 1) throw new Error("codex submit requires exactly one prompt source: positional prompt, --prompt-file, or --prompt-stdin");
const text = promptFile !== undefined
? (promptFile === "-" ? readFileSync(0, "utf8") : readFileSync(promptFile, "utf8"))
: promptStdin
? readFileSync(0, "utf8")
: promptArgs.join(" ");
if (text.trim().length === 0) throw new Error("codex submit prompt must not be empty");
return text;
}
function referenceTaskIdsFromOptions(args: string[]): string[] {
const values = optionValues(args, ["--reference-task-id", "--reference", "--ref"]);
const ids: string[] = [];
for (const value of values.flatMap((item) => item.split(/[,\s]+/u))) {
const id = value.trim();
if (id.length > 0 && !ids.includes(id)) ids.push(id);
}
return ids;
}
function parseSubmitOptions(args: string[]): CodexSubmitOptions {
const maxAttempts = args.some((arg) => arg === "--max-attempts")
? positiveIntegerOption(args, ["--max-attempts"], 99, 99)
: undefined;
return {
prompt: promptFromSubmitArgs(args),
queueId: optionValue(args, ["--queue", "--queue-id"]),
providerId: optionValue(args, ["--provider-id", "--provider"]),
cwd: optionValue(args, ["--cwd", "--workdir"]),
model: optionValue(args, ["--model"]),
reasoningEffort: optionValue(args, ["--reasoning-effort"]),
executionMode: optionValue(args, ["--execution-mode", "--mode"]),
maxAttempts,
referenceTaskIds: referenceTaskIdsFromOptions(args),
dryRun: hasFlag(args, "--dry-run"),
};
}
function submitPayload(options: CodexSubmitOptions): Record<string, unknown> {
return {
prompt: options.prompt,
...(options.queueId === undefined ? {} : { queueId: options.queueId }),
...(options.providerId === undefined ? {} : { providerId: options.providerId }),
...(options.cwd === undefined ? {} : { cwd: options.cwd }),
...(options.model === undefined ? {} : { model: options.model }),
...(options.reasoningEffort === undefined ? {} : { reasoningEffort: options.reasoningEffort }),
...(options.executionMode === undefined ? {} : { executionMode: options.executionMode }),
...(options.maxAttempts === undefined ? {} : { maxAttempts: options.maxAttempts }),
...(options.referenceTaskIds.length === 0 ? {} : { referenceTaskIds: options.referenceTaskIds }),
};
}
function compactTaskMutationResponse(task: unknown): Record<string, unknown> {
const record = asRecord(task) ?? {};
const taskId = asString(record.id);
return {
id: taskId || null,
queueId: record.queueId ?? null,
status: record.status ?? null,
queuedReason: record.queuedReason ?? null,
providerId: record.providerId ?? null,
model: record.model ?? null,
reasoningEffort: record.reasoningEffort ?? null,
cwd: record.cwd ?? null,
executionMode: record.executionMode ?? null,
maxAttempts: record.maxAttempts ?? null,
currentAttempt: record.currentAttempt ?? null,
cancelRequested: record.cancelRequested ?? null,
createdAt: record.createdAt ?? null,
startedAt: record.startedAt ?? null,
updatedAt: record.updatedAt ?? null,
finishedAt: record.finishedAt ?? null,
prompt: textPreview(asString(record.displayPrompt ?? record.basePrompt ?? record.prompt), 1200),
commands: taskId.length === 0 ? null : {
show: `bun scripts/cli.ts codex task ${taskId}`,
trace: `bun scripts/cli.ts codex task ${taskId} --trace --tail --limit ${defaultTraceLimit}`,
output: `bun scripts/cli.ts codex output ${taskId} --tail --limit ${defaultOutputLimit}`,
interrupt: `bun scripts/cli.ts codex interrupt ${taskId}`,
move: `bun scripts/cli.ts codex move ${taskId} --queue <queueId>`,
},
};
}
function compactQueueMutationSummary(value: unknown): Record<string, unknown> | null {
const record = asRecord(value);
if (record === null) return null;
return {
activeQueueIds: record.activeQueueIds ?? null,
activeTaskIds: record.activeTaskIds ?? null,
queuedTaskIds: record.queuedTaskIds ?? null,
counts: record.counts ?? null,
byQueue: Array.isArray(record.byQueue) ? record.byQueue : undefined,
};
}
function codexSubmitTask(args: string[]): unknown {
const options = parseSubmitOptions(args);
const payload = submitPayload(options);
if (options.dryRun) {
return {
ok: true,
dryRun: true,
request: {
...payload,
prompt: textPreview(options.prompt, 3000),
},
};
}
const response = unwrapCodexResponse(coreInternalFetch("/api/microservices/code-queue/proxy/api/tasks", { method: "POST", body: payload }));
return {
upstream: response.upstream,
tasks: asArray(response.body.tasks).map(compactTaskMutationResponse),
queue: compactQueueMutationSummary(response.body.queue),
};
}
function codexInterruptTask(taskId: string): unknown {
const response = unwrapCodexResponse(coreInternalFetch(`/api/microservices/k3sctl-adapter/proxy/api/services/code-queue-scheduler/proxy/api/tasks/${encodeURIComponent(taskId)}/interrupt`, { method: "POST" }));
return {
upstream: response.upstream,
task: compactTaskMutationResponse(response.body.task),
queue: compactQueueMutationSummary(response.body.queue),
};
}
export async function runCodeQueueCommand(_config: UniDeskConfig, args: string[]): Promise<unknown> {
const [action = "task", taskIdArg] = args;
if (action === "submit" || action === "enqueue") {
return codexSubmitTask(args.slice(1));
}
if (action === "task" || action === "summary" || action === "show") {
const taskId = requireTaskId(taskIdArg, `codex ${action}`);
return codexTaskQuery(taskId, args.slice(2));
@@ -693,5 +893,9 @@ export async function runCodeQueueCommand(_config: UniDeskConfig, args: string[]
const taskId = requireTaskId(taskIdArg, "codex move");
return codexMoveTask(taskId, requireQueueId(args.slice(2), "codex move"));
}
throw new Error("codex command must be one of: task, summary, show, output, judge, queues, queue list, queue create, queue merge, move");
if (action === "interrupt" || action === "cancel") {
const taskId = requireTaskId(taskIdArg, `codex ${action}`);
return codexInterruptTask(taskId);
}
throw new Error("codex command must be one of: submit, enqueue, task, summary, show, output, judge, queues, queue list, queue create, queue merge, move, interrupt, cancel");
}
+34 -3
View File
@@ -1,3 +1,4 @@
import { readFileSync } from "node:fs";
import { runCommand } from "./command";
import { type UniDeskConfig, repoRoot } from "./config";
import { jsonByteLength, previewJson } from "./preview";
@@ -58,9 +59,38 @@ function stringOption(args: string[], name: string): string | undefined {
return raw;
}
function methodOption(args: string[]): string {
const method = (stringOption(args, "--method") ?? "GET").toUpperCase();
function hasFlag(args: string[], name: string): boolean {
return args.includes(name);
}
function parseJsonOption(raw: string, name: string): unknown {
try {
return JSON.parse(raw) as unknown;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new Error(`${name} must be valid JSON: ${message}`);
}
}
function requestBodyOption(args: string[]): unknown | undefined {
const bodyJson = stringOption(args, "--body-json");
const bodyFile = stringOption(args, "--body-file");
const bodyStdin = hasFlag(args, "--body-stdin");
const sources = [bodyJson !== undefined, bodyFile !== undefined, bodyStdin].filter(Boolean).length;
if (sources > 1) throw new Error("microservice proxy accepts only one request body source: --body-json, --body-file, or --body-stdin");
if (bodyJson !== undefined) return parseJsonOption(bodyJson, "--body-json");
if (bodyFile !== undefined) {
const text = bodyFile === "-" ? readFileSync(0, "utf8") : readFileSync(bodyFile, "utf8");
return parseJsonOption(text, "--body-file");
}
if (bodyStdin) return parseJsonOption(readFileSync(0, "utf8"), "--body-stdin");
return undefined;
}
function methodOption(args: string[], hasBody = false): string {
const method = (stringOption(args, "--method") ?? (hasBody ? "POST" : "GET")).toUpperCase();
if (!["GET", "HEAD", "POST", "DELETE", "PUT", "PATCH"].includes(method)) throw new Error(`unsupported --method ${method}`);
if (hasBody && (method === "GET" || method === "HEAD")) throw new Error(`microservice proxy cannot send a request body with ${method}`);
return method;
}
@@ -98,7 +128,8 @@ export async function runMicroserviceCommand(_config: UniDeskConfig, args: strin
if (action === "proxy") {
const id = requireId(idArg, "microservice proxy");
const path = requireProxyPath(pathArg);
return summarizeMicroserviceProxyResponse(coreInternalFetch(`/api/microservices/${encodeId(id)}/proxy${path}`, { method: methodOption(args) }), args);
const body = requestBodyOption(args);
return summarizeMicroserviceProxyResponse(coreInternalFetch(`/api/microservices/${encodeId(id)}/proxy${path}`, { method: methodOption(args, body !== undefined), body }), args);
}
throw new Error("microservice command must be one of: list, status, health, proxy");
}