From bdc56627f4014053f99cc844eea77138ec3b116e Mon Sep 17 00:00:00 2001 From: Robert He Date: Fri, 18 Sep 2026 11:28:59 +0800 Subject: [PATCH] feat: add support for codex wire protocol and sandbox configurations --- .env.example | 4 ++++ .vscode/settings.json | 1 + Dockerfile | 5 +++-- apps/server/src/config.ts | 15 +++++++++++++++ apps/server/src/db.ts | 9 ++++++++- apps/server/src/routes/admin.ts | 7 +++++++ apps/server/src/services/codexSession.ts | 4 ++-- apps/server/src/services/generator.ts | 10 ++-------- 8 files changed, 42 insertions(+), 13 deletions(-) diff --git a/.env.example b/.env.example index 8299c87..a060268 100644 --- a/.env.example +++ b/.env.example @@ -22,6 +22,10 @@ CODEX_BASE_URL= CODEX_API_KEY= CODEX_MODEL_PROVIDER=w2s CODEX_MODEL= +# wire 协议:responses(默认)|chat(chat-only 端点如智谱 Coding Plan 必需;codex CLI 已在镜像钉 0.92) +CODEX_WIRE_API= +# codex 沙箱档位:workspace-write(默认)|read-only|danger-full-access(排障) +CODEX_SANDBOX= # 并发 codex 会话数 MAX_CONCURRENT=3 # 单次生成超时(毫秒) diff --git a/.vscode/settings.json b/.vscode/settings.json index 39d621e..ab8ead6 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,6 +6,7 @@ "EdgeOne", "edgeone", "huggingface", + "NOCASE", "npmjs", "protoc", "smtogo", diff --git a/Dockerfile b/Dockerfile index 04b438a..4607538 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,9 +28,10 @@ RUN pnpm --filter @words2site/server build # ---- Stage 3: runtime(生产依赖 + 构建产物 + codex CLI) ---- FROM node:22-slim AS runtime ENV NODE_ENV=production DATA_DIR=/data -# codex CLI(npm 包自带平台二进制;认证走 CODEX_BASE_URL/CODEX_API_KEY 环境变量) +# codex CLI(npm 包自带平台二进制;认证走 CODEX_BASE_URL/CODEX_API_KEY 环境变量)。 +# 钉 0.92.0:更高版本移除了 wire_api=chat(智谱 Coding Plan 等 chat-only 端点必需) RUN corepack enable \ - && npm install -g @openai/codex \ + && npm install -g @openai/codex@0.92.0 \ && mkdir -p /data && chown node:node /data WORKDIR /app COPY package.json pnpm-workspace.yaml pnpm-lock.yaml .npmrc ./ diff --git a/apps/server/src/config.ts b/apps/server/src/config.ts index 7639d68..44d8ff2 100644 --- a/apps/server/src/config.ts +++ b/apps/server/src/config.ts @@ -54,6 +54,18 @@ if (provider === "codex") { } } +// codex wire 协议与沙箱档位(fail-fast 校验) +const wireApi = str("CODEX_WIRE_API", "responses"); +if (wireApi !== "responses" && wireApi !== "chat") { + fail(`CODEX_WIRE_API 只能是 responses 或 chat,当前: ${wireApi}`); +} +const sandbox = str("CODEX_SANDBOX", "workspace-write"); +if (!["read-only", "workspace-write", "danger-full-access"].includes(sandbox)) { + fail( + `CODEX_SANDBOX 非法: ${sandbox}(read-only | workspace-write | danger-full-access)`, + ); +} + // DATA_DIR 相对路径一律相对仓库根解析(避免 tsx/dev/cwd 差异) const repoRoot = path.resolve(import.meta.dirname, "../../.."); const dataDir = path.resolve(repoRoot, str("DATA_DIR", "data")); @@ -85,6 +97,9 @@ export const config = { maxConcurrent: num("MAX_CONCURRENT", 3), timeoutMs: num("GEN_TIMEOUT_MS", 240_000), maxRefine: num("MAX_REFINE", 2), + // chat-only 端点(如智谱 Coding Plan)需 chat + codex CLI ≤0.92(更高版本已移除 chat) + wireApi, + sandbox, }, whisper: { diff --git a/apps/server/src/db.ts b/apps/server/src/db.ts index 7e852a3..406720c 100644 --- a/apps/server/src/db.ts +++ b/apps/server/src/db.ts @@ -129,8 +129,12 @@ const stmts = { listQueueAhead: db.prepare( "SELECT COUNT(*) AS n FROM tasks WHERE status = 'queued' AND created_at < ?", ), + // 域名占用:失败任务即释放(部署成功前炸了不占坑),进行中/已发布仍占用防并发撞名 domainTaken: db.prepare( - "SELECT COUNT(*) AS n FROM tasks WHERE domain = ? COLLATE NOCASE AND removed_at IS NULL", + "SELECT COUNT(*) AS n FROM tasks WHERE domain = ? COLLATE NOCASE AND removed_at IS NULL AND status != 'failed'", + ), + domainTakenByOther: db.prepare( + "SELECT COUNT(*) AS n FROM tasks WHERE domain = ? COLLATE NOCASE AND removed_at IS NULL AND status != 'failed' AND id != ?", ), listScreen: db.prepare(` SELECT id, code, domain, prompt, publish_url, screenshot, created_at @@ -195,6 +199,9 @@ export const tasks = { domainTaken(domain: string): boolean { return (stmts.domainTaken.get(domain) as { n: number }).n > 0; }, + domainTakenByOther(domain: string, id: string): boolean { + return (stmts.domainTakenByOther.get(domain, id) as { n: number }).n > 0; + }, listScreen(): Array<{ id: string; code: string | null; diff --git a/apps/server/src/routes/admin.ts b/apps/server/src/routes/admin.ts index 49d4d0b..381c206 100644 --- a/apps/server/src/routes/admin.ts +++ b/apps/server/src/routes/admin.ts @@ -54,6 +54,13 @@ adminRouter.post("/tasks/:id/retry", (req, res) => { res.status(409).json({ error: "仅失败任务可重试" }); return; } + // 失败任务已释放域名;若期间被别的任务占走,重试会与它撞名 + if (t.domain && tasks.domainTakenByOther(t.domain, t.id)) { + res + .status(409) + .json({ error: `域名 ${t.domain} 已被其他任务占用,无法重试` }); + return; + } tasks.update({ id: t.id, status: "queued", diff --git a/apps/server/src/services/codexSession.ts b/apps/server/src/services/codexSession.ts index 7c8a202..318dd6b 100644 --- a/apps/server/src/services/codexSession.ts +++ b/apps/server/src/services/codexSession.ts @@ -81,8 +81,8 @@ export async function probeCodex(): Promise<{ ok: boolean; detail: string }> { if (config.generation.provider === "mock") { return { ok: true, detail: "mock 模式,无需探活" }; } - const args = ["exec", "--sandbox", "read-only", "--skip-git-repo-check"]; const g = config.generation; + const args = ["exec", "--sandbox", g.sandbox, "--skip-git-repo-check"]; const env = { ...process.env } as NodeJS.ProcessEnv; if (g.baseUrl && g.apiKey) { args.push( @@ -95,7 +95,7 @@ export async function probeCodex(): Promise<{ ok: boolean; detail: string }> { `-c`, `model_providers.${g.modelProvider}.env_key=W2S_CODEX_API_KEY`, `-c`, - `model_providers.${g.modelProvider}.wire_api=responses`, + `model_providers.${g.modelProvider}.wire_api=${g.wireApi}`, ); if (g.model) args.push(`-c`, `model=${g.model}`); env.W2S_CODEX_API_KEY = g.apiKey; diff --git a/apps/server/src/services/generator.ts b/apps/server/src/services/generator.ts index 4919475..8c32d0d 100644 --- a/apps/server/src/services/generator.ts +++ b/apps/server/src/services/generator.ts @@ -79,19 +79,13 @@ function codexArgs( `-c`, `model_providers.${g.modelProvider}.env_key=W2S_CODEX_API_KEY`, `-c`, - `model_providers.${g.modelProvider}.wire_api=responses`, + `model_providers.${g.modelProvider}.wire_api=${g.wireApi}`, ); if (g.model) args.push(`-c`, `model=${g.model}`); } else if (g.model) { args.push(`-c`, `model=${g.model}`); } - args.push( - "--sandbox", - "workspace-write", - "--skip-git-repo-check", - "-C", - workdir, - ); + args.push("--sandbox", g.sandbox, "--skip-git-repo-check", "-C", workdir); if (resumeSessionId) args.push("resume", resumeSessionId); args.push(prompt); return args;