mirror of
https://github.com/Jerryplusy/rc-plugin.git
synced 2025-10-14 08:09:19 +00:00
✨feat: 添加自动更新按钮
This commit is contained in:
parent
01503ac946
commit
ea3c4ab85d
240
server/app/r/api/update/route.js
Normal file
240
server/app/r/api/update/route.js
Normal 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' },
|
||||
});
|
||||
}
|
||||
}
|
@ -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 (
|
||||
<div className="card bg-base-100 shadow-xl">
|
||||
<div className="card-body">
|
||||
@ -23,15 +62,36 @@ export function BotConfig() {
|
||||
<p>当前最新版本为:{version}</p>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h3 className="font-bold">手动更新</h3>
|
||||
<p>R 插件的自动选择更新 / 强制更新</p>
|
||||
<h3 className="font-bold">更新操作</h3>
|
||||
<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>
|
||||
<button className="btn btn-warning">🚧施工</button>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
|
Loading…
x
Reference in New Issue
Block a user