feat: 添加更新功能控制台日志 (console.log/error),标记每步执行与错误输出
This commit is contained in:
@@ -1,15 +1,31 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { checkForUpdate } from "@/lib/update";
|
import { checkForUpdate } from "@/lib/update";
|
||||||
|
|
||||||
|
function apiLog(msg: string) {
|
||||||
|
const t = new Date().toISOString().slice(0, 19).replace("T", " ");
|
||||||
|
console.log(`[${t}] [更新检查] ${msg}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function apiErr(msg: string) {
|
||||||
|
const t = new Date().toISOString().slice(0, 19).replace("T", " ");
|
||||||
|
console.error(`[${t}] [更新检查] ${msg}`);
|
||||||
|
}
|
||||||
|
|
||||||
// GET /api/update/check - 检查是否有可用更新
|
// GET /api/update/check - 检查是否有可用更新
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
|
apiLog("收到版本检查请求");
|
||||||
try {
|
try {
|
||||||
const versionInfo = await checkForUpdate();
|
const versionInfo = await checkForUpdate();
|
||||||
|
const hasUpdate = versionInfo.hasUpdate;
|
||||||
|
const latestVer = versionInfo.latest?.version || "无";
|
||||||
|
apiLog(`检查完成: ${hasUpdate ? "有新版本 " + latestVer : "已是最新"}`);
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: versionInfo,
|
data: versionInfo,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
const msg = error instanceof Error ? error.message : "未知错误";
|
||||||
|
apiErr(`检查异常: ${msg}`);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ success: false, error: "检查更新失败" },
|
{ success: false, error: "检查更新失败" },
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
|
|||||||
@@ -1,9 +1,21 @@
|
|||||||
|
const API_TAG = "SSE-API";
|
||||||
|
function apiLog(msg: string) {
|
||||||
|
const t = new Date().toISOString().slice(0, 19).replace("T", " ");
|
||||||
|
console.log(`[${t}] [${API_TAG}] ${msg}`);
|
||||||
|
}
|
||||||
|
function apiErr(msg: string) {
|
||||||
|
const t = new Date().toISOString().slice(0, 19).replace("T", " ");
|
||||||
|
console.error(`[${t}] [${API_TAG}] ${msg}`);
|
||||||
|
}
|
||||||
|
|
||||||
import { executeUpdate } from "@/lib/update";
|
import { executeUpdate } from "@/lib/update";
|
||||||
|
|
||||||
// POST /api/update/execute - 执行容器更新(SSE 流式输出日志)
|
// POST /api/update/execute - 执行容器更新(SSE 流式输出日志)
|
||||||
export async function POST() {
|
export async function POST() {
|
||||||
const encoder = new TextEncoder();
|
const encoder = new TextEncoder();
|
||||||
|
|
||||||
|
apiLog("收到更新请求,创建 SSE 流");
|
||||||
|
|
||||||
const stream = new ReadableStream({
|
const stream = new ReadableStream({
|
||||||
async start(controller) {
|
async start(controller) {
|
||||||
// 发送日志的辅助函数
|
// 发送日志的辅助函数
|
||||||
@@ -13,8 +25,11 @@ export async function POST() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
apiLog("开始执行 executeUpdate...");
|
||||||
const result = await executeUpdate(sendLog);
|
const result = await executeUpdate(sendLog);
|
||||||
|
|
||||||
|
apiLog(`executeUpdate 完成: success=${result.success}, message="${result.message}"`);
|
||||||
|
|
||||||
// 发送最终结果
|
// 发送最终结果
|
||||||
const finalData = JSON.stringify({
|
const finalData = JSON.stringify({
|
||||||
done: true,
|
done: true,
|
||||||
@@ -25,12 +40,14 @@ export async function POST() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errMsg =
|
const errMsg =
|
||||||
error instanceof Error ? error.message : "未知错误";
|
error instanceof Error ? error.message : "未知错误";
|
||||||
|
apiErr(`SSE 流异常: ${errMsg}`);
|
||||||
controller.enqueue(
|
controller.enqueue(
|
||||||
encoder.encode(
|
encoder.encode(
|
||||||
`data: ${JSON.stringify({ done: true, success: false, message: errMsg })}\n\n`
|
`data: ${JSON.stringify({ done: true, success: false, message: errMsg })}\n\n`
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
|
apiLog("关闭 SSE 流");
|
||||||
controller.close();
|
controller.close();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+90
-25
@@ -44,6 +44,53 @@ export type LogCallback = (message: string) => void;
|
|||||||
/** 空回调 */
|
/** 空回调 */
|
||||||
const noopLog: LogCallback = () => {};
|
const noopLog: LogCallback = () => {};
|
||||||
|
|
||||||
|
/** 带时间戳的控制台日志 */
|
||||||
|
function consoleLog(tag: string, msg: string): void {
|
||||||
|
const t = new Date().toISOString().slice(0, 19).replace("T", " ");
|
||||||
|
console.log(`[${t}] [更新:${tag}] ${msg}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 带时间戳的控制台错误 */
|
||||||
|
function consoleErr(tag: string, msg: string): void {
|
||||||
|
const t = new Date().toISOString().slice(0, 19).replace("T", " ");
|
||||||
|
console.error(`[${t}] [更新:${tag}] ${msg}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行命令并记录到控制台
|
||||||
|
*/
|
||||||
|
function execSyncAndLog(
|
||||||
|
command: string,
|
||||||
|
tag: string,
|
||||||
|
options: Parameters<typeof execSync>[1] & { encoding?: "utf-8" } = {}
|
||||||
|
): string {
|
||||||
|
consoleLog(tag, `执行: ${command}`);
|
||||||
|
try {
|
||||||
|
const stdout = execSync(command, {
|
||||||
|
encoding: "utf-8",
|
||||||
|
windowsHide: true,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
const out = stdout?.trim();
|
||||||
|
if (out) {
|
||||||
|
const lines = out.split("\n").filter(Boolean);
|
||||||
|
// 最多打印 5 行,避免刷屏
|
||||||
|
lines.slice(0, 5).forEach((l) => consoleLog(tag, ` │ ${l}`));
|
||||||
|
if (lines.length > 5) consoleLog(tag, ` │ ... 共 ${lines.length} 行`);
|
||||||
|
}
|
||||||
|
consoleLog(tag, "✓ 完成");
|
||||||
|
return stdout || "";
|
||||||
|
} catch (err) {
|
||||||
|
const e = err as Error & { stderr?: Buffer };
|
||||||
|
const stderr = e.stderr?.toString().trim();
|
||||||
|
if (stderr) {
|
||||||
|
stderr.split("\n").filter(Boolean).forEach((l) => consoleErr(tag, ` │ ${l}`));
|
||||||
|
}
|
||||||
|
consoleErr(tag, `失败: ${e.message}`);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 读取并解析本地的 .version 文件
|
* 读取并解析本地的 .version 文件
|
||||||
*/
|
*/
|
||||||
@@ -182,10 +229,15 @@ export function compareVersions(v1: string, v2: string): number {
|
|||||||
* 检查是否有可用更新
|
* 检查是否有可用更新
|
||||||
*/
|
*/
|
||||||
export async function checkForUpdate(): Promise<VersionInfo> {
|
export async function checkForUpdate(): Promise<VersionInfo> {
|
||||||
|
consoleLog("检查", "开始检查版本更新");
|
||||||
|
|
||||||
const current = readLocalVersionFile();
|
const current = readLocalVersionFile();
|
||||||
|
consoleLog("检查", `当前版本: v${current.version} (${current.release_date || "无日期"})`);
|
||||||
|
|
||||||
const remoteCommit = getRemoteCommitHash();
|
const remoteCommit = getRemoteCommitHash();
|
||||||
|
|
||||||
if (!remoteCommit) {
|
if (!remoteCommit) {
|
||||||
|
consoleErr("检查", "无法连接到远程仓库");
|
||||||
return {
|
return {
|
||||||
current,
|
current,
|
||||||
latest: null,
|
latest: null,
|
||||||
@@ -194,10 +246,15 @@ export async function checkForUpdate(): Promise<VersionInfo> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
consoleLog("检查", `远程 HEAD: ${remoteCommit}`);
|
||||||
const remoteVersionFile = getRemoteVersionFile();
|
const remoteVersionFile = getRemoteVersionFile();
|
||||||
|
|
||||||
if (remoteVersionFile) {
|
if (remoteVersionFile) {
|
||||||
const cmp = compareVersions(remoteVersionFile.version, current.version);
|
const cmp = compareVersions(remoteVersionFile.version, current.version);
|
||||||
|
consoleLog(
|
||||||
|
"检查",
|
||||||
|
`远程版本: v${remoteVersionFile.version} → ${cmp > 0 ? "有新版本" : "已是最新"}`
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
current,
|
current,
|
||||||
latest: remoteVersionFile,
|
latest: remoteVersionFile,
|
||||||
@@ -206,7 +263,7 @@ export async function checkForUpdate(): Promise<VersionInfo> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 远程能连上但 .version 文件不存在或解析失败
|
consoleErr("检查", "远程仓库中未找到 .version 文件");
|
||||||
return {
|
return {
|
||||||
current,
|
current,
|
||||||
latest: null,
|
latest: null,
|
||||||
@@ -223,15 +280,13 @@ export async function checkForUpdate(): Promise<VersionInfo> {
|
|||||||
* 检查 Docker 是否可用
|
* 检查 Docker 是否可用
|
||||||
*/
|
*/
|
||||||
function checkDockerAvailable(): { ok: boolean; message: string } {
|
function checkDockerAvailable(): { ok: boolean; message: string } {
|
||||||
|
consoleLog("Docker", "检查 Docker 守护进程...");
|
||||||
try {
|
try {
|
||||||
execSync("docker info", {
|
execSyncAndLog("docker info", "Docker");
|
||||||
encoding: "utf-8",
|
consoleLog("Docker", "✓ Docker 正常");
|
||||||
timeout: 10000,
|
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
|
||||||
windowsHide: true,
|
|
||||||
});
|
|
||||||
return { ok: true, message: "" };
|
return { ok: true, message: "" };
|
||||||
} catch {
|
} catch {
|
||||||
|
consoleErr("Docker", "✗ Docker 不可用");
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
message:
|
message:
|
||||||
@@ -256,14 +311,19 @@ export async function executeUpdate(
|
|||||||
`recipe_tool_update_${Date.now()}`
|
`recipe_tool_update_${Date.now()}`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
consoleLog("执行", "=== 开始执行更新 ===");
|
||||||
|
consoleLog("执行", `临时目录: ${tmpDir}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
onLog("开始更新流程");
|
onLog("开始更新流程");
|
||||||
|
|
||||||
// 0. 检查 Docker 是否可用
|
// 0. 检查 Docker 是否可用
|
||||||
onLog("[0/4] 检查 Docker 环境");
|
onLog("[0/4] 检查 Docker 环境");
|
||||||
|
consoleLog("执行", "步骤 0/4 — 检查 Docker 环境");
|
||||||
const dockerCheck = checkDockerAvailable();
|
const dockerCheck = checkDockerAvailable();
|
||||||
if (!dockerCheck.ok) {
|
if (!dockerCheck.ok) {
|
||||||
onLog(`✗ ${dockerCheck.message}`);
|
onLog(`✗ ${dockerCheck.message}`);
|
||||||
|
consoleErr("执行", "Docker 检查未通过,更新终止");
|
||||||
return { success: false, message: dockerCheck.message };
|
return { success: false, message: dockerCheck.message };
|
||||||
}
|
}
|
||||||
onLog("✓ Docker 环境正常");
|
onLog("✓ Docker 环境正常");
|
||||||
@@ -271,16 +331,17 @@ export async function executeUpdate(
|
|||||||
// 1. 拉取最新代码
|
// 1. 拉取最新代码
|
||||||
const url = getAuthenticatedRemoteUrl();
|
const url = getAuthenticatedRemoteUrl();
|
||||||
onLog("[1/4] 克隆最新代码");
|
onLog("[1/4] 克隆最新代码");
|
||||||
execSync(`git clone --depth 1 "${url}" "${tmpDir}"`, {
|
consoleLog("执行", "步骤 1/4 — 克隆代码");
|
||||||
encoding: "utf-8",
|
execSyncAndLog(
|
||||||
timeout: 120000,
|
`git clone --depth 1 "${url}" "${tmpDir}"`,
|
||||||
windowsHide: true,
|
"git-clone",
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
{ timeout: 120000, stdio: ["ignore", "pipe", "pipe"] }
|
||||||
});
|
);
|
||||||
onLog("✓ 代码拉取完成");
|
onLog("✓ 代码拉取完成");
|
||||||
|
|
||||||
// 2. 生成 .env 文件
|
// 2. 生成 .env 文件
|
||||||
onLog("[2/4] 准备环境变量");
|
onLog("[2/4] 准备环境变量");
|
||||||
|
consoleLog("执行", "步骤 2/4 — 准备环境变量");
|
||||||
const envContent = [
|
const envContent = [
|
||||||
`DB_HOST=${process.env.DB_HOST || "127.0.0.1"}`,
|
`DB_HOST=${process.env.DB_HOST || "127.0.0.1"}`,
|
||||||
`DB_PORT=${process.env.DB_PORT || "3306"}`,
|
`DB_PORT=${process.env.DB_PORT || "3306"}`,
|
||||||
@@ -293,36 +354,40 @@ export async function executeUpdate(
|
|||||||
`ONEDEV_ACCESS_TOKEN=${process.env.ONEDEV_ACCESS_TOKEN || ""}`,
|
`ONEDEV_ACCESS_TOKEN=${process.env.ONEDEV_ACCESS_TOKEN || ""}`,
|
||||||
].join("\n");
|
].join("\n");
|
||||||
fs.writeFileSync(path.join(tmpDir, ".env"), envContent, "utf-8");
|
fs.writeFileSync(path.join(tmpDir, ".env"), envContent, "utf-8");
|
||||||
|
consoleLog("执行", `.env 已写入 ${envContent.split("\n").length} 行`);
|
||||||
onLog("✓ 环境变量已准备");
|
onLog("✓ 环境变量已准备");
|
||||||
|
|
||||||
// 3. 构建新镜像
|
// 3. 构建新镜像
|
||||||
onLog("[3/4] 构建新 Docker 镜像");
|
onLog("[3/4] 构建新 Docker 镜像");
|
||||||
execSync(`cd "${tmpDir}" && docker-compose -p recipe_tool build`, {
|
consoleLog("执行", "步骤 3/4 — 构建镜像(可能耗时较长)");
|
||||||
encoding: "utf-8",
|
execSyncAndLog(
|
||||||
timeout: 600000,
|
`cd "${tmpDir}" && docker-compose -p recipe_tool build`,
|
||||||
windowsHide: true,
|
"docker-build",
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
{ timeout: 600000, stdio: ["ignore", "pipe", "pipe"] }
|
||||||
});
|
);
|
||||||
onLog("✓ 镜像构建完成");
|
onLog("✓ 镜像构建完成");
|
||||||
|
|
||||||
// 4. 重启服务
|
// 4. 重启服务
|
||||||
onLog("[4/4] 重启服务");
|
onLog("[4/4] 重启服务");
|
||||||
execSync(`cd "${tmpDir}" && docker-compose -p recipe_tool up -d`, {
|
consoleLog("执行", "步骤 4/4 — 重启服务");
|
||||||
encoding: "utf-8",
|
execSyncAndLog(
|
||||||
timeout: 60000,
|
`cd "${tmpDir}" && docker-compose -p recipe_tool up -d`,
|
||||||
windowsHide: true,
|
"docker-up",
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
{ timeout: 60000, stdio: ["ignore", "pipe", "pipe"] }
|
||||||
});
|
);
|
||||||
onLog("✓ 服务已重启");
|
onLog("✓ 服务已重启");
|
||||||
|
|
||||||
|
consoleLog("执行", "=== 更新成功 ===");
|
||||||
return { success: true, message: "更新完成" };
|
return { success: true, message: "更新完成" };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errMsg = error instanceof Error ? error.message : "未知错误";
|
const errMsg = error instanceof Error ? error.message : "未知错误";
|
||||||
|
consoleErr("执行", `=== 更新失败: ${errMsg} ===`);
|
||||||
onLog(`✗ 更新失败: ${errMsg}`);
|
onLog(`✗ 更新失败: ${errMsg}`);
|
||||||
return { success: false, message: errMsg };
|
return { success: false, message: errMsg };
|
||||||
} finally {
|
} finally {
|
||||||
try {
|
try {
|
||||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||||
|
consoleLog("执行", `临时目录已清理: ${tmpDir}`);
|
||||||
} catch {
|
} catch {
|
||||||
// ignore cleanup errors
|
// ignore cleanup errors
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user