From ea3c4ab85dc760f98520d2555badedf8fe813f57 Mon Sep 17 00:00:00 2001 From: zhiyu1998 <542716863@qq.com> Date: Mon, 25 Nov 2024 13:07:29 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8feat:=20=E6=B7=BB=E5=8A=A0=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E6=9B=B4=E6=96=B0=E6=8C=89=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/app/r/api/update/route.js | 240 ++++++++++++++++++++++++++ server/components/home/bot-config.jsx | 76 +++++++- 2 files changed, 308 insertions(+), 8 deletions(-) create mode 100644 server/app/r/api/update/route.js diff --git a/server/app/r/api/update/route.js b/server/app/r/api/update/route.js new file mode 100644 index 0000000..3bbd73e --- /dev/null +++ b/server/app/r/api/update/route.js @@ -0,0 +1,240 @@ +import { exec } from 'child_process'; +import { promisify } from 'util'; +import fs from 'fs/promises'; +import path from 'path'; +import { redis } from "../../../../utils/redis.js"; + +const execAsync = promisify(exec); + +// Git错误处理函数 +function handleGitError(error, stderr) { + if (error.message.includes('RPC failed')) { + return '网络连接失败,请检查网络后重试'; + } + if (error.message.includes('early EOF')) { + return '数据传输中断,请重试'; + } + if (error.message.includes('fetch-pack: invalid index-pack output')) { + return '数据包错误,请重试'; + } + if (error.message.includes('Timed out')) { + return '连接超时,请检查网络后重试'; + } + if (error.message.includes('Could not resolve host')) { + return '无法解析主机地址,请检查网络'; + } + if (error.message.includes('Permission denied')) { + return '权限被拒绝,请检查git权限配置'; + } + if (error.message.includes('be overwritten by merge')) { + return '存在冲突,请使用强制更新'; + } + + // 如果是其他错误,返回具体错误信息 + return stderr || error.message || '未知错误'; +} + +async function ensureDirectory(dir) { + try { + await fs.access(dir); + } catch { + await fs.mkdir(dir, { recursive: true }); + } +} + +async function copyConfig(src, dest) { + try { + await ensureDirectory(path.dirname(dest)); + await fs.cp(src, dest, { recursive: true }); + console.log(`成功复制配置文件从 ${src} 到 ${dest}`); + return true; + } catch (error) { + console.error(`复制配置文件失败: ${error.message}`); + return false; + } +} + +// 清理更新状态和临时文件 +async function cleanupUpdate(tempDir) { + try { + // 清理临时文件 + await fs.rm(tempDir, { recursive: true, force: true }); + // 清理Redis中的更新状态 + await redis.del('rconsole:update:status'); + await redis.del('rconsole:update:paths'); + console.log('清理完成'); + } catch (error) { + console.error('清理失败:', error); + } +} + +export async function GET(req) { + try { + const { searchParams } = new URL(req.url); + const isCheck = searchParams.get('check') === 'true'; + const isRestore = searchParams.get('restore') === 'true'; + const isForce = searchParams.get('force') === 'true'; + + // 如果是检查请求 + if (isCheck) { + const updateStatus = await redis.get('rconsole:update:status'); + return new Response(JSON.stringify({ + needsRestore: updateStatus === 'restoring' + }), { + headers: { 'Content-Type': 'application/json' }, + }); + } + + // 如果是恢复请求 + if (isRestore) { + const updateStatus = await redis.get('rconsole:update:status'); + const paths = JSON.parse(await redis.get('rconsole:update:paths') || '{}'); + + if (updateStatus === 'restoring' && paths.tempDir && paths.configDir) { + try { + await copyConfig(paths.tempDir, paths.configDir); + await cleanupUpdate(paths.tempDir); + return new Response(JSON.stringify({ + success: true, + message: '配置恢复完成' + }), { + headers: { 'Content-Type': 'application/json' }, + }); + } catch (error) { + return new Response(JSON.stringify({ + success: false, + message: '配置恢复失败:' + error.message + }), { + headers: { 'Content-Type': 'application/json' }, + }); + } + } + + return new Response(JSON.stringify({ + success: true, + message: '无需恢复' + }), { + headers: { 'Content-Type': 'application/json' }, + }); + } + + const projectRoot = path.join(process.cwd(), '..'); + const tempDir = path.join(projectRoot, 'temp', 'update-tmp'); + const configDir = path.join(projectRoot, 'config'); + + // 检查是否有未完成的更新 + const updateStatus = await redis.get('rconsole:update:status'); + if (updateStatus === 'restoring') { + // 如果有未完成的更新,尝试恢复 + const paths = JSON.parse(await redis.get('rconsole:update:paths') || '{}'); + if (paths.tempDir && paths.configDir) { + try { + await copyConfig(paths.tempDir, paths.configDir); + console.log('恢复了之前未完成的配置文件更新'); + } finally { + await cleanupUpdate(paths.tempDir); + } + } + } + + console.log('开始新的更新流程'); + + // 保存路径信息到Redis + await redis.set('rconsole:update:paths', JSON.stringify({ + tempDir, + configDir + })); + await redis.set('rconsole:update:status', 'started'); + + // 确保临时目录存在 + await ensureDirectory(tempDir); + + // 备份配置文件 + let configBackedUp = false; + try { + await fs.access(configDir); + await copyConfig(configDir, tempDir); + configBackedUp = true; + console.log('配置文件备份成功'); + await redis.set('rconsole:update:status', 'backed_up'); + } catch (error) { + console.log('无配置文件需要备份或备份失败:', error.message); + } + + try { + // 执行git操作 + if (isForce) { + console.log('执行强制更新...'); + await execAsync(`git -C "${projectRoot}" checkout .`); + } + + // 标记状态为需要恢复 + await redis.set('rconsole:update:status', 'restoring'); + + console.log('执行git pull...'); + const { stdout } = await execAsync(`git -C "${projectRoot}" pull --no-rebase`); + + // 恢复配置文件 + if (configBackedUp) { + console.log('开始恢复配置文件...'); + await copyConfig(tempDir, configDir); + } + + // 清理所有临时文件和状态 + await cleanupUpdate(tempDir); + + if (stdout.includes('Already up to date') || stdout.includes('已经是最新')) { + return new Response(JSON.stringify({ + success: true, + message: '已经是最新版本' + }), { + headers: { 'Content-Type': 'application/json' }, + }); + } + + return new Response(JSON.stringify({ + success: true, + message: '更新成功' + }), { + headers: { 'Content-Type': 'application/json' }, + }); + + } catch (error) { + // 如果git操作失败,也尝试恢复配置文件 + if (configBackedUp) { + try { + await copyConfig(tempDir, configDir); + console.log('git操作失败,但配置文件已恢复'); + } catch (restoreError) { + console.error('恢复配置文件失败:', restoreError.message); + } + } + + await cleanupUpdate(tempDir); + + const errorMessage = handleGitError(error, error.stderr); + return new Response(JSON.stringify({ + success: false, + message: errorMessage + }), { + headers: { 'Content-Type': 'application/json' }, + }); + } + + } catch (error) { + console.error('更新过程出错:', error); + + // 确保清理所有临时状态 + const paths = JSON.parse(await redis.get('rconsole:update:paths') || '{}'); + if (paths.tempDir) { + await cleanupUpdate(paths.tempDir); + } + + return new Response(JSON.stringify({ + success: false, + message: '更新过程出错:' + (error.message || '未知错误') + }), { + headers: { 'Content-Type': 'application/json' }, + }); + } +} \ No newline at end of file diff --git a/server/components/home/bot-config.jsx b/server/components/home/bot-config.jsx index a8a8058..265c198 100644 --- a/server/components/home/bot-config.jsx +++ b/server/components/home/bot-config.jsx @@ -2,16 +2,55 @@ import { useEffect, useState } from "react"; import { GIT_COMMIT_URL, GIT_VERSION_URL } from "../../constants/api.js"; export function BotConfig() { - const [version, setVersion] = useState("v0.0.0"); - const [commit, setCommit] = useState(null); + const [updating, setUpdating] = useState(false); + const [updateMessage, setUpdateMessage] = useState(""); useEffect(() => { fetch(GIT_VERSION_URL).then(response => response.json()).then(data => setVersion(data.name)); fetch(GIT_COMMIT_URL).then(response => response.json()).then(data => setCommit(data)); + + const checkUpdateStatus = async () => { + try { + const response = await fetch('/r/api/update?check=true'); + const data = await response.json(); + if (data.needsRestore) { + setUpdateMessage("检测到未完成的更新,正在恢复配置..."); + const restoreResponse = await fetch('/r/api/update?restore=true'); + const restoreData = await restoreResponse.json(); + setUpdateMessage(restoreData.message); + } + } catch (error) { + console.error('检查更新状态失败:', error); + } + }; + + checkUpdateStatus(); }, []); + const handleUpdate = async (isForce = false) => { + try { + setUpdating(true); + setUpdateMessage("正在更新中..."); + + const response = await fetch(`/r/api/update?force=${isForce}`); + const data = await response.json(); + + if (data.success) { + setUpdateMessage(data.message); + fetch(GIT_VERSION_URL).then(response => response.json()).then(data => setVersion(data.name)); + fetch(GIT_COMMIT_URL).then(response => response.json()).then(data => setCommit(data)); + } else { + setUpdateMessage(`更新失败:${data.message}`); + } + } catch (error) { + setUpdateMessage(`更新出错:${error.message}`); + } finally { + setUpdating(false); + } + }; + return (
当前最新版本为:{ version }
+当前最新版本为:{version}
R 插件的自动选择更新 / 强制更新
+选择更新方式进行更新
+ {updateMessage && ( ++ {updateMessage} +
+ )} +