import { getVercelConnectionSettings, saveVercelConnectionSettings } from '~/.server/service/connection-settings'; import { createOrUpdateDeployment, getLatestDeployment } from '~/.server/service/deployment'; import { errorResponse, successResponse } from '~/.server/utils/api-response'; import { DeploymentPlatformEnum, DeploymentStatusEnum } from '~/types/deployment'; import type { VercelProjectInfo } from '~/types/vercel'; import { isBinaryString } from '~/utils/file-utils'; import { createScopedLogger } from '~/utils/logger'; 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; 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, '部署失败'); } }