265 lines
6.9 KiB
JavaScript
265 lines
6.9 KiB
JavaScript
#!/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);
|
||
});
|