123 lines
4.4 KiB
JavaScript
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);
|
|
});
|
|
}
|