Files
workphone-sdk/sdk/scripts/bind_v0_vercel.mjs
2026-07-24 22:29:18 +08:00

265 lines
6.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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);
});