🎉 first commit

This commit is contained in:
LIlGG
2025-09-24 13:06:25 +08:00
commit 1f4fb103e9
409 changed files with 61222 additions and 0 deletions

View File

@@ -0,0 +1,308 @@
import { getVercelConnectionSettings, saveVercelConnectionSettings } from '~/lib/.server/connectionSettings';
import { createOrUpdateDeployment, getLatestDeployment } from '~/lib/.server/deployment';
import { createScopedLogger } from '~/lib/.server/logger';
import { DeploymentPlatformEnum, DeploymentStatusEnum } from '~/types/deployment';
import type { VercelProjectInfo } from '~/types/vercel';
import { errorResponse, successResponse } from '~/utils/api-response';
import { isBinaryString } from '~/utils/file-utils';
const logger = createScopedLogger('api.vercel.deploy');
export type GetVercelDeployByProjectIdArgs = {
request: Request;
userId: string;
};
export async function getVercelDeployByProjectId({ request, userId }: GetVercelDeployByProjectIdArgs) {
const url = new URL(request.url);
const projectId = url.searchParams.get('projectId');
const requestToken = url.searchParams.get('token');
// 从用户设置中获取连接信息
let connectionSettings = await getVercelConnectionSettings(userId);
// 如果请求参数中提供了token优先使用请求参数中的信息并更新用户设置
if (requestToken) {
connectionSettings = {
token: requestToken,
};
// 更新用户设置
await saveVercelConnectionSettings(userId, requestToken);
}
// 如果没有连接信息,返回错误
if (!connectionSettings) {
return errorResponse(401, '未连接到Vercel请先设置访问令牌');
}
const { token } = connectionSettings;
if (!projectId) {
return errorResponse(400, '缺少项目ID');
}
try {
// Get project info
const projectResponse = await fetch(`https://api.vercel.com/v9/projects/${projectId}`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (!projectResponse.ok) {
return errorResponse(400, '无法获取项目');
}
const projectData = (await projectResponse.json()) as any;
// Get latest deployment
const deploymentsResponse = await fetch(`https://api.vercel.com/v6/deployments?projectId=${projectId}&limit=1`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (!deploymentsResponse.ok) {
return errorResponse(400, '获取部署信息失败');
}
const deploymentsData = (await deploymentsResponse.json()) as any;
const latestDeployment = deploymentsData.deployments?.[0];
return successResponse(
{
project: {
id: projectData.id,
name: projectData.name,
url: `https://${projectData.name}.vercel.app`,
},
deploy: latestDeployment
? {
id: latestDeployment.id,
state: latestDeployment.state,
url: latestDeployment.url ? `https://${latestDeployment.url}` : `https://${projectData.name}.vercel.app`,
}
: null,
},
'获取部署信息成功',
);
} catch (error) {
logger.error('Error fetching Vercel deployment:', error);
return errorResponse(500, '获取部署失败');
}
}
interface DeployRequestBody {
projectId?: string;
files: Record<string, string>;
chatId: string;
token?: string;
}
export type HandleVercelDeployArgs = {
request: Request;
userId: string;
};
// Existing action function for POST requests
export async function handleVercelDeploy({ request, userId }: HandleVercelDeployArgs) {
try {
const { projectId, files, token: requestToken, chatId } = (await request.json()) as DeployRequestBody;
// 从用户设置中获取连接信息
let connectionSettings = await getVercelConnectionSettings(userId);
// 如果请求体中提供了token优先使用请求体中的信息并更新用户设置
if (requestToken) {
connectionSettings = {
token: requestToken,
};
// 更新用户设置
await saveVercelConnectionSettings(userId, requestToken);
}
// 如果没有连接信息,返回错误
if (!connectionSettings) {
return errorResponse(401, '未连接到Vercel请先设置访问令牌');
}
const { token } = connectionSettings;
const existingDeployment = await getLatestDeployment(userId, chatId, DeploymentPlatformEnum.VERCEL);
let targetProjectId;
if (projectId) {
targetProjectId = projectId;
} else if (existingDeployment?.deploymentId) {
targetProjectId = existingDeployment.deploymentId;
} else {
targetProjectId = undefined;
}
let projectInfo: VercelProjectInfo | undefined;
if (targetProjectId) {
const projectResponse = await fetch(`https://api.vercel.com/v9/projects/${targetProjectId}`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (projectResponse.ok) {
const existingProject = (await projectResponse.json()) as any;
projectInfo = {
id: existingProject.id,
name: existingProject.name,
url: `https://${existingProject.name}.vercel.app`,
chatId,
};
}
}
if (!projectInfo) {
const projectName = `upage-${chatId}-${Date.now()}`.toLocaleLowerCase();
const createProjectResponse = await fetch('https://api.vercel.com/v9/projects', {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: projectName,
framework: null,
}),
});
if (!createProjectResponse.ok) {
const errorData = (await createProjectResponse.json()) as any;
return errorResponse(400, `创建项目失败: ${errorData.error?.message || '未知错误'}`);
}
const newProject = (await createProjectResponse.json()) as any;
targetProjectId = newProject.id;
projectInfo = {
id: newProject.id,
name: newProject.name,
url: `https://${newProject.name}.vercel.app`,
chatId,
};
}
// Prepare files for deployment
const deploymentFiles = [];
for (const [filePath, content] of Object.entries(files)) {
// Ensure file path doesn't start with a slash for Vercel
const normalizedPath = filePath.startsWith('/') ? filePath.substring(1) : filePath;
deploymentFiles.push({
file: normalizedPath,
data: isBinaryString(content) ? Buffer.from(content, 'binary').toString('base64') : content,
encoding: isBinaryString(content) ? 'base64' : 'utf-8',
});
}
// Create a new deployment
const deployResponse = await fetch(`https://api.vercel.com/v13/deployments`, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: projectInfo.name,
project: targetProjectId,
target: 'production',
files: deploymentFiles,
routes: [{ src: '/(.*)', dest: '/$1' }],
}),
});
if (!deployResponse.ok) {
const errorData = (await deployResponse.json()) as any;
return errorResponse(400, `创建部署失败: ${errorData.error?.message || '未知错误'}`);
}
const deployData = (await deployResponse.json()) as any;
// Poll for deployment status
let retryCount = 0;
const maxRetries = 60;
let deploymentUrl = '';
let deploymentState = '';
while (retryCount < maxRetries) {
const statusResponse = await fetch(`https://api.vercel.com/v13/deployments/${deployData.id}`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (statusResponse.ok) {
const status = (await statusResponse.json()) as any;
deploymentState = status.readyState;
const alias = status.alias;
const automaticAliases = status.automaticAliases;
if (status.readyState === 'READY' || status.readyState === 'ERROR') {
if (status.aliasAssigned) {
const diffAlias = alias.filter((item: string) => !automaticAliases.includes(item));
deploymentUrl = `https://${diffAlias[0]}`;
} else {
deploymentUrl = status.url ? `https://${status.url}` : '';
}
break;
}
}
retryCount++;
await new Promise((resolve) => setTimeout(resolve, 2000));
}
if (deploymentState === 'ERROR') {
return errorResponse(500, '部署失败');
}
if (retryCount >= maxRetries) {
return errorResponse(500, '部署超时');
}
const finalUrl = deploymentUrl || projectInfo.url;
// 记录部署信息
try {
await createOrUpdateDeployment({
userId,
chatId,
platform: DeploymentPlatformEnum.VERCEL,
deploymentId: deployData.id,
url: finalUrl,
status: deploymentState === 'READY' ? DeploymentStatusEnum.SUCCESS : DeploymentStatusEnum.PENDING,
metadata: {
projectId: projectInfo.id,
projectName: projectInfo.name,
},
});
logger.info(`为用户 ${userId} 创建了 Vercel 部署记录`);
} catch (error) {
logger.error('创建部署记录失败:', error);
// 不影响主流程,继续返回成功
}
return successResponse(
{
deploy: {
id: deployData.id,
state: deploymentState,
url: finalUrl,
},
project: projectInfo,
},
'部署成功',
);
} catch (error) {
logger.error('Vercel deploy error:', error);
return errorResponse(500, '部署失败');
}
}