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

233 lines
6.8 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";
import { execFileSync } from "node:child_process";
const ROOT = path.resolve(import.meta.dirname, "..", "..");
const binding = JSON.parse(
fs.readFileSync(path.join(ROOT, "sdk/config/v0_vercel_binding.json"), "utf8"),
);
function envFile(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, "")]),
);
}
const credentials = Object.assign(
{},
envFile("/Users/karuo/Documents/开发/3、自营项目/上帝之眼/.env"),
envFile(
"/Users/karuo/Documents/个人/卡若AI/03_卡木/木果_项目模板/v0前端生成与预览交付/.env.private",
),
envFile(path.join(ROOT, ".env")),
process.env,
);
async function jsonRequest(url, token, options = {}) {
let response;
try {
response = await fetch(url, {
...options,
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
...options.headers,
},
});
} catch (cause) {
throw new Error(`network_error:${new URL(url).hostname}:${cause.message}`);
}
const text = await response.text();
const data = text ? JSON.parse(text) : {};
if (!response.ok) {
const error = new Error(`${options.method || "GET"} ${url}: ${response.status}`);
error.status = response.status;
error.data = data;
throw error;
}
return data;
}
function requireTokens() {
for (const key of ["V0_API_KEY", "VERCEL_TOKEN"]) {
if (!credentials[key]) throw new Error(`missing_${key.toLowerCase()}`);
}
}
async function getRepository() {
const url = `https://api.github.com/repos/${binding.github.owner}/${binding.github.repo}`;
const options = { headers: { Accept: "application/vnd.github+json" } };
if (credentials.GITHUB_TOKEN) {
try {
return await jsonRequest(url, credentials.GITHUB_TOKEN, options);
} catch (error) {
if (error.status !== 401) throw error;
}
}
const ghToken = execFileSync("gh", ["auth", "token"], {
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
}).trim();
if (!ghToken) throw new Error("missing_github_token");
return jsonRequest(url, ghToken, options);
}
async function getStatus() {
requireTokens();
const [v0Project, chat, vercelProject, repository] = await Promise.all([
jsonRequest(
`https://api.v0.dev/v1/projects/${binding.v0.projectId}`,
credentials.V0_API_KEY,
),
jsonRequest(
`https://api.v0.dev/v1/chats/${binding.v0.chatId}`,
credentials.V0_API_KEY,
),
jsonRequest(
`https://api.vercel.com/v9/projects/${binding.vercel.projectId}`,
credentials.VERCEL_TOKEN,
),
getRepository(),
]);
const production = vercelProject.targets?.production;
const demoUrl = chat.latestVersion?.demoUrl || chat.demoUrl || null;
let effectiveV0VercelProjectId = v0Project.vercelProjectId || null;
if (!effectiveV0VercelProjectId && demoUrl) {
effectiveV0VercelProjectId =
new URL(demoUrl).searchParams.get("__v0_vercel_project_id") || null;
}
const productionUrl =
binding.vercel.productionUrl ||
(production?.alias?.[0] ? `https://${production.alias[0]}` : null);
let productionHttpStatus = null;
if (productionUrl) {
try {
const response = await fetch(productionUrl, { redirect: "follow" });
productionHttpStatus = response.status;
} catch (cause) {
productionHttpStatus = Number(
execFileSync(
"curl",
["-L", "-sS", "-o", "/dev/null", "-w", "%{http_code}", productionUrl],
{ encoding: "utf8" },
).trim(),
);
}
}
return {
github: {
repository: repository.full_name,
defaultBranch: repository.default_branch,
private: repository.private,
},
vercel: {
projectId: vercelProject.id,
projectName: vercelProject.name,
gitRepository: vercelProject.link
? `${vercelProject.link.org}/${vercelProject.link.repo}`
: null,
deploymentId: production?.id || null,
deploymentStatus: production?.readyState || null,
productionUrl,
productionHttpStatus,
},
v0: {
projectId: v0Project.id,
projectName: v0Project.name,
vercelProjectId: v0Project.vercelProjectId || null,
effectiveVercelProjectId: effectiveV0VercelProjectId,
matchesSourceProject:
effectiveV0VercelProjectId === binding.vercel.projectId,
chatId: chat.id,
chatName: chat.name || chat.title,
latestVersionId: chat.latestVersion?.id || null,
latestVersionStatus: chat.latestVersion?.status || null,
demoUrl,
screenshotUrl: chat.latestVersion?.screenshotUrl || null,
},
};
}
async function waitForChat(previousVersionId) {
for (let attempt = 0; attempt < 90; attempt += 1) {
const chat = await jsonRequest(
`https://api.v0.dev/v1/chats/${binding.v0.chatId}`,
credentials.V0_API_KEY,
);
const version = chat.latestVersion;
if (
version?.id !== previousVersionId &&
["completed", "failed", "error"].includes(version?.status)
) {
return chat;
}
await new Promise((resolve) => setTimeout(resolve, 4000));
}
throw new Error("v0_message_timeout");
}
async function sendMessage(message) {
requireTokens();
const before = await jsonRequest(
`https://api.v0.dev/v1/chats/${binding.v0.chatId}`,
credentials.V0_API_KEY,
);
await jsonRequest(
`https://api.v0.dev/v1/chats/${binding.v0.chatId}/messages`,
credentials.V0_API_KEY,
{
method: "POST",
body: JSON.stringify({ message }),
},
);
const chat = await waitForChat(before.latestVersion?.id);
return {
chatId: chat.id,
versionId: chat.latestVersion?.id || null,
status: chat.latestVersion?.status || null,
demoUrl: chat.latestVersion?.demoUrl || chat.demoUrl || null,
screenshotUrl: chat.latestVersion?.screenshotUrl || null,
};
}
async function main() {
const [command = "status", ...args] = process.argv.slice(2);
if (command === "status") {
console.log(JSON.stringify(await getStatus(), null, 2));
return;
}
if (command === "send") {
const message = args.join(" ").trim();
if (!message) throw new Error("missing_message");
console.log(JSON.stringify(await sendMessage(message), null, 2));
return;
}
throw new Error(`unsupported_command:${command}`);
}
main().catch((error) => {
console.error(
JSON.stringify(
{
status: "failed",
error: error.message,
detail: error.data?.error || error.data || null,
},
null,
2,
),
);
process.exit(1);
});