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