feat: 添加自动更新按钮

This commit is contained in:
zhiyu1998 2024-11-25 13:07:29 +08:00
parent 01503ac946
commit ea3c4ab85d
2 changed files with 308 additions and 8 deletions

View File

@ -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' },
});
}
}

View File

@ -2,16 +2,55 @@ import { useEffect, useState } from "react";
import { GIT_COMMIT_URL, GIT_VERSION_URL } from "../../constants/api.js"; import { GIT_COMMIT_URL, GIT_VERSION_URL } from "../../constants/api.js";
export function BotConfig() { export function BotConfig() {
const [version, setVersion] = useState("v0.0.0"); const [version, setVersion] = useState("v0.0.0");
const [commit, setCommit] = useState(null); const [commit, setCommit] = useState(null);
const [updating, setUpdating] = useState(false);
const [updateMessage, setUpdateMessage] = useState("");
useEffect(() => { useEffect(() => {
fetch(GIT_VERSION_URL).then(response => response.json()).then(data => setVersion(data.name)); 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)); 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 ( return (
<div className="card bg-base-100 shadow-xl"> <div className="card bg-base-100 shadow-xl">
<div className="card-body"> <div className="card-body">
@ -20,23 +59,44 @@ export function BotConfig() {
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div> <div>
<h3 className="font-bold">最新版本</h3> <h3 className="font-bold">最新版本</h3>
<p>当前最新版本为{ version }</p> <p>当前最新版本为{version}</p>
</div> </div>
<button className="btn btn-ghost" <button className="btn btn-ghost"
onClick={ () => fetch(GIT_VERSION_URL).then(response => response.json()).then(data => setVersion(data.name)) }>检查更新 onClick={() => fetch(GIT_VERSION_URL)
.then(response => response.json())
.then(data => setVersion(data.name))}>
检查更新
</button> </button>
</div> </div>
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div> <div>
<h3 className="font-bold">手动更新</h3> <h3 className="font-bold">更新操作</h3>
<p>R 插件的自动选择更新 / 强制更新</p> <p>选择更新方式进行更新</p>
{updateMessage && (
<p className={`text-sm ${updateMessage.includes('失败') || updateMessage.includes('错') ? 'text-error' : 'text-success'}`}>
{updateMessage}
</p>
)}
</div>
<div className="flex gap-2">
<button
className={`btn btn-primary ${updating ? 'loading' : ''}`}
onClick={() => handleUpdate(false)}
disabled={updating}>
普通更新
</button>
<button
className={`btn btn-warning ${updating ? 'loading' : ''}`}
onClick={() => handleUpdate(true)}
disabled={updating}>
强制更新
</button>
</div> </div>
<button className="btn btn-warning">🚧施工</button>
</div> </div>
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div> <div>
<h3 className="font-bold">最近更新</h3> <h3 className="font-bold">最近更新</h3>
<span><a href={ commit?.url }>[{ commit?.author }]{ commit?.message }</a></span> <span><a href={commit?.url}>[{commit?.author}]{commit?.message}</a></span>
</div> </div>
</div> </div>
</div> </div>