Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
# 单次生成超时(毫秒)
Expand Down
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"EdgeOne",
"edgeone",
"huggingface",
"NOCASE",
"npmjs",
"protoc",
"smtogo",
Expand Down
5 changes: 3 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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 ./
Expand Down
15 changes: 15 additions & 0 deletions apps/server/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
Expand Down Expand Up @@ -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: {
Expand Down
9 changes: 8 additions & 1 deletion apps/server/src/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down
7 changes: 7 additions & 0 deletions apps/server/src/routes/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions apps/server/src/services/codexSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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;
Expand Down
10 changes: 2 additions & 8 deletions apps/server/src/services/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading