Files
author/app/api/update-source-stream/route.js
T

123 lines
4.4 KiB
JavaScript

import { existsSync } from 'fs';
import { join } from 'path';
import { spawn } from 'child_process';
/**
* POST /api/update-source-stream
* SSE 流式更新源码:git pull → npm install → npm run build
* 实时推送每个步骤的进度
*/
export async function POST() {
const cwd = process.cwd();
const gitDir = join(cwd, '.git');
if (!existsSync(gitDir)) {
return new Response(
JSON.stringify({ error: '非源码部署环境,无法执行自动更新' }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
);
}
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
const send = (data) => {
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`));
};
const steps = [
{ step: 1, total: 3, label: '🔄 拉取最新代码', cmd: 'git', args: ['pull'], timeout: 60000 },
{ step: 2, total: 3, label: '📦 安装依赖', cmd: 'npm', args: ['install'], timeout: 300000 },
{ step: 3, total: 3, label: '🔨 构建项目', cmd: 'npm', args: ['run', 'build'], timeout: 300000 },
];
try {
for (const stepInfo of steps) {
send({ step: stepInfo.step, total: stepInfo.total, label: stepInfo.label, status: 'running' });
const result = await runCommand(stepInfo.cmd, stepInfo.args, cwd, stepInfo.timeout);
if (!result.success) {
send({
step: stepInfo.step, total: stepInfo.total, label: stepInfo.label,
status: 'error', log: result.output,
});
send({ done: true, success: false, error: `步骤 ${stepInfo.step} 失败: ${stepInfo.label}` });
controller.close();
return;
}
// 检查 git pull 是否已是最新
if (stepInfo.step === 1 && (result.output.includes('Already up to date') || result.output.includes('已经是最新'))) {
send({
step: stepInfo.step, total: stepInfo.total, label: stepInfo.label,
status: 'done', log: result.output,
});
send({ done: true, success: true, alreadyUpToDate: true });
controller.close();
return;
}
send({
step: stepInfo.step, total: stepInfo.total, label: stepInfo.label,
status: 'done', log: result.lastLines,
});
}
send({ done: true, success: true, alreadyUpToDate: false });
} catch (err) {
send({ done: true, success: false, error: err.message });
}
controller.close();
},
});
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
},
});
}
function runCommand(cmd, args, cwd, timeout) {
return new Promise((resolve) => {
let output = '';
let timer;
// Windows 下用 shell 模式
const proc = spawn(cmd, args, {
cwd,
shell: true,
env: { ...process.env },
stdio: ['ignore', 'pipe', 'pipe'],
});
proc.stdout.on('data', (data) => { output += data.toString(); });
proc.stderr.on('data', (data) => { output += data.toString(); });
proc.on('close', (code) => {
clearTimeout(timer);
const lines = output.trim().split('\n');
resolve({
success: code === 0,
output: output.trim(),
lastLines: lines.slice(-5).join('\n'),
});
});
proc.on('error', (err) => {
clearTimeout(timer);
resolve({ success: false, output: err.message, lastLines: err.message });
});
timer = setTimeout(() => {
proc.kill();
resolve({ success: false, output: '命令执行超时', lastLines: '命令执行超时' });
}, timeout);
});
}