#!/usr/bin/env node import fs from "node:fs"; import path from "node:path"; import process from "node:process"; const ROOT = path.resolve(import.meta.dirname, "..", ".."); const VERCEL_API = "https://api.vercel.com"; const V0_API = "https://api.v0.dev"; const PROJECT_NAME = "workphone-console"; const GITHUB_REPO = "fnvtk/workphone-sdk"; const V0_PROJECT_ID = "3vTFXIHvZVu"; const V0_CHAT_ID = "llvv3dzaeNB"; function readEnvFile(file) { if (!fs.existsSync(file)) return {}; return Object.fromEntries( fs .readFileSync(file, "utf8") .split(/\r?\n/) .map((line) => line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*?)\s*$/)) .filter(Boolean) .map((match) => [ match[1], match[2].replace(/^["']|["']$/g, ""), ]), ); } function loadCredentials() { const sources = [ readEnvFile( "/Users/karuo/Documents/开发/3、自营项目/上帝之眼/.env", ), readEnvFile( "/Users/karuo/Documents/个人/卡若AI/03_卡木(木)/木果_项目模板/v0前端生成与预览交付/.env.private", ), readEnvFile(path.join(ROOT, ".env")), process.env, ]; return Object.assign({}, ...sources); } async function request(base, pathname, token, options = {}) { const response = await fetch(`${base}${pathname}`, { ...options, headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", ...options.headers, }, }); const text = await response.text(); let data = {}; try { data = text ? JSON.parse(text) : {}; } catch { data = { raw: text.slice(0, 500) }; } if (!response.ok) { const error = new Error( `${options.method || "GET"} ${pathname}: ${response.status}`, ); error.status = response.status; error.data = data; throw error; } return data; } async function getOrCreateVercelProject(token) { try { return await request( VERCEL_API, `/v9/projects/${PROJECT_NAME}`, token, ); } catch (error) { if (error.status !== 404) throw error; } return request(VERCEL_API, "/v11/projects", token, { method: "POST", body: JSON.stringify({ name: PROJECT_NAME, framework: "vite", gitRepository: { type: "github", repo: GITHUB_REPO, }, installCommand: "npm ci --prefix sdk/android-ui", buildCommand: "npm run build --prefix sdk/android-ui", outputDirectory: "sdk/android-ui/dist", }), }); } async function configureVercelProject(token) { return request( VERCEL_API, `/v9/projects/${PROJECT_NAME}`, token, { method: "PATCH", body: JSON.stringify({ framework: "vite", installCommand: "npm ci --prefix sdk/android-ui", buildCommand: "npm run build --prefix sdk/android-ui", outputDirectory: "sdk/android-ui/dist", ssoProtection: { deploymentType: "preview" }, }), }, ); } async function deploy(token) { return request( VERCEL_API, "/v13/deployments?skipAutoDetectionConfirmation=1", token, { method: "POST", body: JSON.stringify({ name: PROJECT_NAME, target: "production", gitSource: { type: "github", org: "fnvtk", repo: "workphone-sdk", ref: "main", }, projectSettings: { framework: "vite", installCommand: "npm ci --prefix sdk/android-ui", buildCommand: "npm run build --prefix sdk/android-ui", outputDirectory: "sdk/android-ui/dist", }, }), }, ); } async function waitForDeployment(token, deploymentId) { for (let attempt = 0; attempt < 60; attempt += 1) { const deployment = await request( VERCEL_API, `/v13/deployments/${deploymentId}`, token, ); if (deployment.readyState === "READY") return deployment; if (["ERROR", "CANCELED"].includes(deployment.readyState)) { throw new Error(`deployment_${deployment.readyState.toLowerCase()}`); } await new Promise((resolve) => setTimeout(resolve, 5000)); } throw new Error("deployment_timeout"); } async function bindV0(v0Token, vercelProjectId) { const project = await request( V0_API, `/v1/projects/${V0_PROJECT_ID}`, v0Token, ); if (project.vercelProjectId === vercelProjectId) return project; await request( V0_API, `/v1/projects/${V0_PROJECT_ID}`, v0Token, { method: "PATCH", body: JSON.stringify({ vercelProjectId }), }, ); const verified = await request( V0_API, `/v1/projects/${V0_PROJECT_ID}`, v0Token, ); if (verified.vercelProjectId !== vercelProjectId) { const error = new Error("v0_vercel_binding_not_persisted"); error.data = { projectId: V0_PROJECT_ID, expectedVercelProjectId: vercelProjectId, actualVercelProjectId: verified.vercelProjectId || null, action: "保留现有项目与对话,禁止自动新建重复项目;等待 v0 官方绑定 API 可回读后续跑。", }; throw error; } return verified; } async function triggerV0Deployment(v0Token) { const chat = await request( V0_API, `/v1/chats/${V0_CHAT_ID}`, v0Token, ); return request(V0_API, "/v1/deployments", v0Token, { method: "POST", body: JSON.stringify({ projectId: V0_PROJECT_ID, chatId: V0_CHAT_ID, versionId: chat.latestVersion.id, }), }); } async function main() { const credentials = loadCredentials(); if (!credentials.VERCEL_TOKEN) throw new Error("missing_vercel_token"); if (!credentials.V0_API_KEY) throw new Error("missing_v0_api_key"); await request(VERCEL_API, "/v2/user", credentials.VERCEL_TOKEN); const project = await getOrCreateVercelProject(credentials.VERCEL_TOKEN); await configureVercelProject(credentials.VERCEL_TOKEN); const deployment = await deploy(credentials.VERCEL_TOKEN); const ready = await waitForDeployment( credentials.VERCEL_TOKEN, deployment.id, ); const v0Project = await bindV0(credentials.V0_API_KEY, project.id); const v0Deployment = await triggerV0Deployment(credentials.V0_API_KEY); const productionUrl = `https://${ready.alias?.[0] || ready.url}`; const check = await fetch(productionUrl, { redirect: "follow" }); if (!check.ok) throw new Error(`production_http_${check.status}`); console.log( JSON.stringify( { github: GITHUB_REPO, branch: "main", vercelProjectId: project.id, deploymentId: ready.id, productionUrl, productionHttpStatus: check.status, v0ProjectId: v0Project.id, v0ChatId: V0_CHAT_ID, v0DeploymentId: v0Deployment.id, status: "completed", }, null, 2, ), ); } main().catch((error) => { console.error( JSON.stringify( { status: "failed", error: error.message, detail: error.data?.error || error.data || null, }, null, 2, ), ); process.exit(1); });