From f2817ab8fd2d69a06ecdf4f40e02342427b2831d Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 9 May 2026 08:07:24 +0000 Subject: [PATCH] feat: improve admin upgrade logs --- scripts/admin-upgrade-runner.mjs | 40 +++++++++- src/app/api/admin/upgrade/route.ts | 2 +- src/components/admin/system-upgrade-tab.tsx | 88 +++++++++++++++++---- 3 files changed, 110 insertions(+), 20 deletions(-) diff --git a/scripts/admin-upgrade-runner.mjs b/scripts/admin-upgrade-runner.mjs index 026ea56..0511313 100755 --- a/scripts/admin-upgrade-runner.mjs +++ b/scripts/admin-upgrade-runner.mjs @@ -84,6 +84,7 @@ async function main() { message: '正在检查升级包与运行环境', startedAt: state.startedAt || new Date().toISOString(), }); + logStep('开始升级任务', `任务 ${jobId} 使用${mode === 'hot' ? '热更新' : '冷更新'}模式,升级包 ${packageName}`); if (mode !== 'hot' && mode !== 'cold') { throw new Error('升级方式无效'); @@ -95,6 +96,7 @@ async function main() { throw new Error('仅支持 .tar、.tar.gz、.tgz 升级包'); } + logStep('校验升级包', '正在读取压缩包目录并检查格式'); run('tar', tarReadArgs('list', packagePath), { cwd: projectRoot, label: '检查升级包结构' }); resetDir(extractDir); @@ -107,6 +109,7 @@ async function main() { } const validation = validateFiles(files, mode); + logStep('升级包内容', `校验通过,共 ${files.length} 个文件:${files.slice(0, 20).join('、')}${files.length > 20 ? ` 等 ${files.length} 个文件` : ''}`); updateState({ step: 'validated', progress: 14, @@ -117,6 +120,7 @@ async function main() { }); updateState({ step: 'backup_data', progress: 22, message: '正在创建数据库、存储与环境配置备份' }); + logStep('创建数据备份', '开始备份数据库、存储目录和环境配置'); const backupFile = runCapture('bash', ['./scripts/backup-create.sh'], { cwd: projectRoot, label: '创建数据备份', @@ -126,18 +130,25 @@ async function main() { throw new Error('数据备份创建失败'); } updateState({ backupFile }); + logStep('数据备份完成', `备份文件:${backupFile}`); updateState({ step: 'backup_source', progress: 30, message: '正在创建源码快照' }); + logStep('创建源码快照', '开始保存升级前源码状态'); createSourceBackup(sourceBackupFile); updateState({ sourceBackupFile }); + logStep('源码快照完成', `快照文件:${sourceBackupFile}`); updateState({ step: 'apply', progress: 42, message: '正在应用升级包文件' }); + logStep('应用升级文件', '开始覆盖升级包中的文件'); updateState({ preExistingFiles: files.filter(file => fs.existsSync(path.join(projectRoot, file))) }); applyFiles(payloadRoot, files); + logStep('升级文件应用完成', `已应用 ${files.filter(file => file !== 'manifest.json').length} 个文件`); if (mode === 'hot') { updateState({ step: 'verify_hot', progress: 70, message: '正在验证热更新文件' }); + logStep('热更新验证', '正在执行 TypeScript 校验,确认补丁不会破坏现有代码'); run('pnpm', ['run', 'ts-check'], { cwd: projectRoot, label: 'TypeScript 校验' }); + logStep('热更新完成', '升级成功,平台未重启,前端业务不中断'); updateState({ status: 'succeeded', step: 'completed', @@ -152,20 +163,30 @@ async function main() { const dependencyChanged = files.some(file => file === 'package.json' || file === 'pnpm-lock.yaml'); if (dependencyChanged) { updateState({ step: 'install', progress: 54, message: '依赖文件发生变化,正在安装依赖' }); + logStep('安装依赖', '检测到 package.json 或 pnpm-lock.yaml 变化,开始安装依赖'); run('pnpm', ['install', '--frozen-lockfile', '--prod=false'], { cwd: projectRoot, label: '安装依赖' }); + logStep('依赖安装完成', '依赖安装已完成'); } updateState({ step: 'ts_check', progress: 64, message: '正在执行 TypeScript 校验' }); + logStep('代码校验', '开始执行 TypeScript 校验'); run('pnpm', ['run', 'ts-check'], { cwd: projectRoot, label: 'TypeScript 校验' }); + logStep('代码校验完成', 'TypeScript 校验已通过'); updateState({ step: 'build', progress: 75, message: '正在构建平台' }); + logStep('平台构建', '开始构建生产版本'); run('pnpm', ['run', 'build'], { cwd: projectRoot, label: '构建平台' }); + logStep('平台构建完成', '生产构建已完成'); updateState({ step: 'restart', progress: 86, message: '正在重启平台进程' }); + logStep('重启平台', '冷更新需要重启平台进程,重启后升级状态会从磁盘继续读取'); restartPlatform(); + logStep('平台重启命令完成', '平台重启命令已执行,开始等待健康检查'); updateState({ step: 'health_check', progress: 94, message: '正在检查平台健康状态' }); + logStep('健康检查', '正在确认平台接口恢复正常'); waitForHealth(); + logStep('冷更新完成', '平台已重启并通过健康检查'); updateState({ status: 'succeeded', @@ -179,6 +200,7 @@ async function main() { async function rollbackAfterFailure(message) { const originalError = message; + logStep('升级失败', `失败原因:${originalError}`); updateState({ status: 'rolling_back', step: 'rolling_back', @@ -188,27 +210,35 @@ async function rollbackAfterFailure(message) { }); if (fs.existsSync(sourceBackupFile)) { + logStep('回滚源码', '正在恢复升级前源码快照,并移除升级中新建的文件'); restoreSourceBackup(sourceBackupFile); + logStep('源码回滚完成', '源码已恢复到升级开始前状态'); } if (state.backupFile && fs.existsSync(state.backupFile)) { + logStep('回滚数据', '正在恢复数据库、存储目录和环境配置备份'); run('bash', ['./scripts/backup-restore.sh', state.backupFile], { cwd: projectRoot, label: '恢复数据备份', env: { COZE_WORKSPACE_PATH: projectRoot }, }); + logStep('数据回滚完成', '数据库、存储目录和环境配置已恢复'); } if (mode === 'cold') { try { + logStep('回滚后重建', '冷更新失败后正在重新构建回滚版本'); run('pnpm', ['run', 'build'], { cwd: projectRoot, label: '回滚后重新构建' }); + logStep('回滚后重启', '正在重启回滚后的平台版本'); restartPlatform(); waitForHealth(); + logStep('回滚后健康检查通过', '平台已恢复到升级前版本'); } catch (error) { throw new Error(`回滚后平台恢复检查失败: ${error instanceof Error ? error.message : String(error)}`); } } + logStep('自动回滚完成', '升级失败,但已自动恢复到升级开始前状态'); updateState({ status: 'rolled_back', step: 'rolled_back', @@ -281,10 +311,14 @@ function updateState(patch) { function log(line) { const timestamped = `[${new Date().toISOString()}] ${line}`; - const logs = [...(state.logs || []), timestamped].slice(-300); + const logs = [...(state.logs || []), timestamped].slice(-1000); updateState({ logs }); } +function logStep(title, detail = '') { + log(detail ? `${title}:${detail}` : title); +} + function ensureDir(dir) { fs.mkdirSync(dir, { recursive: true, mode: 0o700 }); } @@ -300,7 +334,7 @@ function run(command, commandArgs, options = {}) { function runCapture(command, commandArgs, options = {}) { const label = options.label || command; - log(`${label}: ${command} ${commandArgs.join(' ')}`); + logStep(label, `执行命令 ${command} ${commandArgs.join(' ')}`); const result = spawnSync(command, commandArgs, { cwd: options.cwd || projectRoot, env: { ...process.env, COREPACK_HOME: process.env.COREPACK_HOME || '/tmp/corepack', ...(options.env || {}) }, @@ -309,7 +343,7 @@ function runCapture(command, commandArgs, options = {}) { }); const output = `${result.stdout || ''}${result.stderr || ''}`.trim(); if (output) { - for (const line of output.split(/\r?\n/).slice(-80)) log(`${label}: ${line}`); + for (const line of output.split(/\r?\n/).slice(-180)) log(`${label}输出:${line}`); } if (result.status !== 0) { throw new Error(`${label}失败,退出码 ${result.status ?? 'unknown'}`); diff --git a/src/app/api/admin/upgrade/route.ts b/src/app/api/admin/upgrade/route.ts index 76a3f10..f75b936 100644 --- a/src/app/api/admin/upgrade/route.ts +++ b/src/app/api/admin/upgrade/route.ts @@ -50,7 +50,7 @@ export async function GET(request: NextRequest) { const states = await readStates(); return NextResponse.json({ latest: states[0] || null, - history: states.slice(0, 12), + history: states, stateDir: getUpgradeStateRoot(), running: states.some(job => RUNNING_STATUSES.has(job.status)), }); diff --git a/src/components/admin/system-upgrade-tab.tsx b/src/components/admin/system-upgrade-tab.tsx index 683bf10..04be83a 100644 --- a/src/components/admin/system-upgrade-tab.tsx +++ b/src/components/admin/system-upgrade-tab.tsx @@ -3,6 +3,8 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { AlertTriangle, + ChevronDown, + ChevronUp, CheckCircle2, FileArchive, Flame, @@ -85,6 +87,8 @@ export default function SystemUpgradeTab() { const [submitting, setSubmitting] = useState(false); const [loading, setLoading] = useState(true); const [upgradeData, setUpgradeData] = useState({ latest: null, history: [], running: false, stateDir: '' }); + const [currentLogJobIds, setCurrentLogJobIds] = useState>(new Set()); + const [expandedHistoryId, setExpandedHistoryId] = useState(null); const fileInputRef = useRef(null); const latest = upgradeData.latest; @@ -99,8 +103,12 @@ export default function SystemUpgradeTab() { }); const data = await res.json().catch(() => ({})); if (!res.ok) throw new Error(data.error || '读取升级状态失败'); + const latestJob = data.latest || null; + if (latestJob && RUNNING_STATUSES.has(latestJob.status)) { + setCurrentLogJobIds(previous => new Set(previous).add(latestJob.id)); + } setUpgradeData({ - latest: data.latest || null, + latest: latestJob, history: Array.isArray(data.history) ? data.history : [], running: data.running === true, stateDir: data.stateDir || '', @@ -154,6 +162,9 @@ export default function SystemUpgradeTab() { const data = await res.json().catch(() => ({})); if (!res.ok) throw new Error(data.error || '创建升级任务失败'); + if (data.job?.id) { + setCurrentLogJobIds(previous => new Set(previous).add(data.job.id)); + } toast.success(mode === 'hot' ? '热更新任务已启动' : '冷更新任务已启动'); setSelectedFile(null); if (fileInputRef.current) fileInputRef.current.value = ''; @@ -267,7 +278,11 @@ export default function SystemUpgradeTab() { 正在加载升级状态 ) : latest ? ( - + ) : (
@@ -285,7 +300,7 @@ export default function SystemUpgradeTab() { 升级历史 - 显示最近 12 次升级任务,便于核对成功、失败与回滚记录。 + 保留全部升级任务,便于随时查看升级内容、执行日志、失败原因与回滚记录。 {upgradeData.history.length === 0 ? ( @@ -293,16 +308,33 @@ export default function SystemUpgradeTab() { ) : (
{upgradeData.history.map(job => ( -
-
- - {job.mode === 'hot' ? '热更新' : '冷更新'} +
+
+
+ + {job.mode === 'hot' ? '热更新' : '冷更新'} +
+
+
{job.packageName}
+
{job.message}
+
+
+
{formatDate(job.updatedAt)}
+ +
-
-
{job.packageName}
-
{job.message}
-
-
{formatDate(job.updatedAt)}
+ {expandedHistoryId === job.id && ( +
+ +
+ )}
))}
@@ -346,7 +378,16 @@ function ModeCard({ ); } -function UpgradeStatusPanel({ job }: { job: UpgradeJob }) { +function UpgradeStatusPanel({ + job, + showLogs, + logTitle = '执行日志', +}: { + job: UpgradeJob; + showLogs: boolean; + logTitle?: string; +}) { + const changedFiles = job.changedFiles || []; return (
@@ -370,10 +411,22 @@ function UpgradeStatusPanel({ job }: { job: UpgradeJob }) { + {job.backupFile && } {job.sourceBackupFile && }
+ {changedFiles.length > 0 && ( +
+
升级内容
+
+ {changedFiles.map(file => ( +
{file}
+ ))} +
+
+ )} + {job.error && ( @@ -382,13 +435,16 @@ function UpgradeStatusPanel({ job }: { job: UpgradeJob }) { )} - {job.logs?.length > 0 && ( + {showLogs && job.logs?.length > 0 && ( <>
-
执行日志
+
+ {logTitle} + {job.logs.length} 行 +
-              {job.logs.slice(-120).join('\n')}
+              {job.logs.join('\n')}