diff --git a/app/api/update/check/route.ts b/app/api/update/check/route.ts index d7e2fc5..55d9ef5 100644 --- a/app/api/update/check/route.ts +++ b/app/api/update/check/route.ts @@ -1,15 +1,31 @@ import { NextResponse } from "next/server"; 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 - 检查是否有可用更新 export async function GET() { + apiLog("收到版本检查请求"); try { const versionInfo = await checkForUpdate(); + const hasUpdate = versionInfo.hasUpdate; + const latestVer = versionInfo.latest?.version || "无"; + apiLog(`检查完成: ${hasUpdate ? "有新版本 " + latestVer : "已是最新"}`); return NextResponse.json({ success: true, data: versionInfo, }); } catch (error) { + const msg = error instanceof Error ? error.message : "未知错误"; + apiErr(`检查异常: ${msg}`); return NextResponse.json( { success: false, error: "检查更新失败" }, { status: 500 } diff --git a/app/api/update/execute/route.ts b/app/api/update/execute/route.ts index 6322098..d5aca4e 100644 --- a/app/api/update/execute/route.ts +++ b/app/api/update/execute/route.ts @@ -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"; // POST /api/update/execute - 执行容器更新(SSE 流式输出日志) export async function POST() { const encoder = new TextEncoder(); + apiLog("收到更新请求,创建 SSE 流"); + const stream = new ReadableStream({ async start(controller) { // 发送日志的辅助函数 @@ -13,8 +25,11 @@ export async function POST() { }; try { + apiLog("开始执行 executeUpdate..."); const result = await executeUpdate(sendLog); + apiLog(`executeUpdate 完成: success=${result.success}, message="${result.message}"`); + // 发送最终结果 const finalData = JSON.stringify({ done: true, @@ -25,12 +40,14 @@ export async function POST() { } catch (error) { const errMsg = error instanceof Error ? error.message : "未知错误"; + apiErr(`SSE 流异常: ${errMsg}`); controller.enqueue( encoder.encode( `data: ${JSON.stringify({ done: true, success: false, message: errMsg })}\n\n` ) ); } finally { + apiLog("关闭 SSE 流"); controller.close(); } }, diff --git a/lib/update.ts b/lib/update.ts index 9bfeec4..a05e2b3 100644 --- a/lib/update.ts +++ b/lib/update.ts @@ -44,6 +44,53 @@ export type LogCallback = (message: string) => void; /** 空回调 */ 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[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 文件 */ @@ -182,10 +229,15 @@ export function compareVersions(v1: string, v2: string): number { * 检查是否有可用更新 */ export async function checkForUpdate(): Promise { + consoleLog("检查", "开始检查版本更新"); + const current = readLocalVersionFile(); + consoleLog("检查", `当前版本: v${current.version} (${current.release_date || "无日期"})`); + const remoteCommit = getRemoteCommitHash(); if (!remoteCommit) { + consoleErr("检查", "无法连接到远程仓库"); return { current, latest: null, @@ -194,10 +246,15 @@ export async function checkForUpdate(): Promise { }; } + consoleLog("检查", `远程 HEAD: ${remoteCommit}`); const remoteVersionFile = getRemoteVersionFile(); if (remoteVersionFile) { const cmp = compareVersions(remoteVersionFile.version, current.version); + consoleLog( + "检查", + `远程版本: v${remoteVersionFile.version} → ${cmp > 0 ? "有新版本" : "已是最新"}` + ); return { current, latest: remoteVersionFile, @@ -206,7 +263,7 @@ export async function checkForUpdate(): Promise { }; } - // 远程能连上但 .version 文件不存在或解析失败 + consoleErr("检查", "远程仓库中未找到 .version 文件"); return { current, latest: null, @@ -223,15 +280,13 @@ export async function checkForUpdate(): Promise { * 检查 Docker 是否可用 */ function checkDockerAvailable(): { ok: boolean; message: string } { + consoleLog("Docker", "检查 Docker 守护进程..."); try { - execSync("docker info", { - encoding: "utf-8", - timeout: 10000, - stdio: ["ignore", "pipe", "pipe"], - windowsHide: true, - }); + execSyncAndLog("docker info", "Docker"); + consoleLog("Docker", "✓ Docker 正常"); return { ok: true, message: "" }; } catch { + consoleErr("Docker", "✗ Docker 不可用"); return { ok: false, message: @@ -256,14 +311,19 @@ export async function executeUpdate( `recipe_tool_update_${Date.now()}` ); + consoleLog("执行", "=== 开始执行更新 ==="); + consoleLog("执行", `临时目录: ${tmpDir}`); + try { onLog("开始更新流程"); // 0. 检查 Docker 是否可用 onLog("[0/4] 检查 Docker 环境"); + consoleLog("执行", "步骤 0/4 — 检查 Docker 环境"); const dockerCheck = checkDockerAvailable(); if (!dockerCheck.ok) { onLog(`✗ ${dockerCheck.message}`); + consoleErr("执行", "Docker 检查未通过,更新终止"); return { success: false, message: dockerCheck.message }; } onLog("✓ Docker 环境正常"); @@ -271,16 +331,17 @@ export async function executeUpdate( // 1. 拉取最新代码 const url = getAuthenticatedRemoteUrl(); onLog("[1/4] 克隆最新代码"); - execSync(`git clone --depth 1 "${url}" "${tmpDir}"`, { - encoding: "utf-8", - timeout: 120000, - windowsHide: true, - stdio: ["ignore", "pipe", "pipe"], - }); + consoleLog("执行", "步骤 1/4 — 克隆代码"); + execSyncAndLog( + `git clone --depth 1 "${url}" "${tmpDir}"`, + "git-clone", + { timeout: 120000, stdio: ["ignore", "pipe", "pipe"] } + ); onLog("✓ 代码拉取完成"); // 2. 生成 .env 文件 onLog("[2/4] 准备环境变量"); + consoleLog("执行", "步骤 2/4 — 准备环境变量"); const envContent = [ `DB_HOST=${process.env.DB_HOST || "127.0.0.1"}`, `DB_PORT=${process.env.DB_PORT || "3306"}`, @@ -293,36 +354,40 @@ export async function executeUpdate( `ONEDEV_ACCESS_TOKEN=${process.env.ONEDEV_ACCESS_TOKEN || ""}`, ].join("\n"); fs.writeFileSync(path.join(tmpDir, ".env"), envContent, "utf-8"); + consoleLog("执行", `.env 已写入 ${envContent.split("\n").length} 行`); onLog("✓ 环境变量已准备"); // 3. 构建新镜像 onLog("[3/4] 构建新 Docker 镜像"); - execSync(`cd "${tmpDir}" && docker-compose -p recipe_tool build`, { - encoding: "utf-8", - timeout: 600000, - windowsHide: true, - stdio: ["ignore", "pipe", "pipe"], - }); + consoleLog("执行", "步骤 3/4 — 构建镜像(可能耗时较长)"); + execSyncAndLog( + `cd "${tmpDir}" && docker-compose -p recipe_tool build`, + "docker-build", + { timeout: 600000, stdio: ["ignore", "pipe", "pipe"] } + ); onLog("✓ 镜像构建完成"); // 4. 重启服务 onLog("[4/4] 重启服务"); - execSync(`cd "${tmpDir}" && docker-compose -p recipe_tool up -d`, { - encoding: "utf-8", - timeout: 60000, - windowsHide: true, - stdio: ["ignore", "pipe", "pipe"], - }); + consoleLog("执行", "步骤 4/4 — 重启服务"); + execSyncAndLog( + `cd "${tmpDir}" && docker-compose -p recipe_tool up -d`, + "docker-up", + { timeout: 60000, stdio: ["ignore", "pipe", "pipe"] } + ); onLog("✓ 服务已重启"); + consoleLog("执行", "=== 更新成功 ==="); return { success: true, message: "更新完成" }; } catch (error) { const errMsg = error instanceof Error ? error.message : "未知错误"; + consoleErr("执行", `=== 更新失败: ${errMsg} ===`); onLog(`✗ 更新失败: ${errMsg}`); return { success: false, message: errMsg }; } finally { try { fs.rmSync(tmpDir, { recursive: true, force: true }); + consoleLog("执行", `临时目录已清理: ${tmpDir}`); } catch { // ignore cleanup errors }