feat: add code queue submit cli
This commit is contained in:
@@ -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 body,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 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`。
|
||||
|
||||
@@ -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/max;provider-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
@@ -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
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user