feat: add code queue submit cli
This commit is contained in:
+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