From 67a134feb965ffb9323479d2d0ee81cc72ed0e0a Mon Sep 17 00:00:00 2001 From: Manus AI Date: Fri, 24 Jul 2026 18:22:44 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E5=A2=9E=E5=8A=A0=20v0=20=E4=B8=8E=20?= =?UTF-8?q?Vercel=20=E8=87=AA=E5=8A=A8=E7=BB=91=E5=AE=9A=E9=83=A8=E7=BD=B2?= =?UTF-8?q?=E9=97=AD=E7=8E=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sdk/scripts/bind_v0_vercel.mjs | 259 +++++++++++++++++++++++++++++++++ 1 file changed, 259 insertions(+) create mode 100644 sdk/scripts/bind_v0_vercel.mjs diff --git a/sdk/scripts/bind_v0_vercel.mjs b/sdk/scripts/bind_v0_vercel.mjs new file mode 100644 index 0000000000..8f6ab8cb8f --- /dev/null +++ b/sdk/scripts/bind_v0_vercel.mjs @@ -0,0 +1,259 @@ +#!/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(path.join(ROOT, ".env")), + readEnvFile( + "/Users/karuo/Documents/开发/3、自营项目/上帝之眼/.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, + }, + rootDirectory: ".", + 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", + rootDirectory: ".", + 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", + rootDirectory: ".", + 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; + + try { + return await request( + V0_API, + `/v1/projects/${V0_PROJECT_ID}`, + v0Token, + { + method: "PATCH", + body: JSON.stringify({ vercelProjectId }), + }, + ); + } catch (error) { + if (![404, 405, 422].includes(error.status)) throw error; + return request(V0_API, "/v1/projects", v0Token, { + method: "POST", + body: JSON.stringify({ + name: "工作手机控制台", + description: "工作手机前端,GitHub main 为唯一源码真源", + vercelProjectId, + }), + }); + } +} + +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); +});