chore:AI大修改
This commit is contained in:
@@ -9,7 +9,7 @@ import { dinstall, mlsetup } from "./modloader/index.js";
|
|||||||
import config from "./utils/config.js";
|
import config from "./utils/config.js";
|
||||||
import { execPromise } from "./utils/utils.js";
|
import { execPromise } from "./utils/utils.js";
|
||||||
import { MessageWS } from "./utils/ws.js";
|
import { MessageWS } from "./utils/ws.js";
|
||||||
import { debug } from "./utils/logger.js";
|
import { logger } from "./utils/logger.js";
|
||||||
|
|
||||||
export class Dex {
|
export class Dex {
|
||||||
wsx!: WebSocketServer;
|
wsx!: WebSocketServer;
|
||||||
@@ -24,136 +24,118 @@ export class Dex {
|
|||||||
public async Main(buffer: Buffer, dser: boolean) {
|
public async Main(buffer: Buffer, dser: boolean) {
|
||||||
try {
|
try {
|
||||||
const first = Date.now();
|
const first = Date.now();
|
||||||
const { contain, info } = await this._getinfo(buffer).catch((e) => {
|
const { contain, info } = await this._getinfo(buffer);
|
||||||
throw new Error(e);
|
|
||||||
});
|
|
||||||
const plat = what_platform(contain);
|
const plat = what_platform(contain);
|
||||||
debug(plat);
|
logger.debug("Platform detected", plat);
|
||||||
debug(info);
|
logger.debug("Modpack info", info);
|
||||||
const mpname = info.name;
|
const mpname = info.name;
|
||||||
const unpath = `./instance/${mpname}`;
|
const unpath = `./instance/${mpname}`;
|
||||||
|
// 解压和下载(并行处理)
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this._unzip(buffer, mpname),
|
this._unzip(buffer, mpname),
|
||||||
await platform(plat).downloadfile(info, unpath, this.message),
|
platform(plat).downloadfile(info, unpath, this.message)
|
||||||
]).catch((e) => {
|
]);
|
||||||
throw new Error(e);
|
|
||||||
}); // 解压和下载
|
|
||||||
this.message.statusChange(); //改变状态
|
this.message.statusChange(); //改变状态
|
||||||
await new DeEarth(`${unpath}/mods`, `./.rubbish/${mpname}`)
|
await new DeEarth(`${unpath}/mods`, `./.rubbish/${mpname}`).Main();
|
||||||
.Main()
|
|
||||||
.catch((e) => {
|
|
||||||
throw new Error(e);
|
|
||||||
});
|
|
||||||
this.message.statusChange(); //改变状态(DeEarth筛选模组完毕)
|
this.message.statusChange(); //改变状态(DeEarth筛选模组完毕)
|
||||||
const mlinfo = await platform(plat)
|
const mlinfo = await platform(plat).getinfo(info);
|
||||||
.getinfo(info)
|
|
||||||
.catch((e) => {
|
|
||||||
throw new Error(e);
|
|
||||||
});
|
|
||||||
if (dser) {
|
if (dser) {
|
||||||
await mlsetup(
|
await mlsetup(
|
||||||
mlinfo.loader,
|
mlinfo.loader,
|
||||||
mlinfo.minecraft,
|
mlinfo.minecraft,
|
||||||
mlinfo.loader_version,
|
mlinfo.loader_version,
|
||||||
unpath
|
unpath
|
||||||
).catch((e) => {
|
); // 安装服务端
|
||||||
throw new Error(e);
|
} else {
|
||||||
}); //安装服务端
|
|
||||||
}
|
|
||||||
if (!dser) {
|
|
||||||
dinstall(
|
dinstall(
|
||||||
mlinfo.loader,
|
mlinfo.loader,
|
||||||
mlinfo.minecraft,
|
mlinfo.minecraft,
|
||||||
mlinfo.loader_version,
|
mlinfo.loader_version,
|
||||||
unpath
|
unpath
|
||||||
).catch((e) => {
|
);
|
||||||
throw new Error(e);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
const latest = Date.now();
|
const latest = Date.now();
|
||||||
this.message.finish(first, latest); //完成
|
this.message.finish(first, latest); //完成
|
||||||
if (config.oaf) {
|
if (config.oaf) {
|
||||||
await execPromise(`start ${p.join("./instance")}`).catch((e) => {
|
await execPromise(`start ${p.join("./instance")}`);
|
||||||
throw new Error(e);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
//await this._unzip(buffer);
|
|
||||||
|
logger.info(`Task completed in ${latest - first}ms`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const err = e as Error;
|
const err = e as Error;
|
||||||
|
logger.error("Main process failed", err);
|
||||||
this.message.handleError(err);
|
this.message.handleError(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _getinfo(buffer: Buffer) {
|
private async _getinfo(buffer: Buffer) {
|
||||||
const important_name = ["manifest.json", "modrinth.index.json"];
|
const importantFiles = ["manifest.json", "modrinth.index.json"];
|
||||||
let contain: string = "";
|
|
||||||
let info: any = {};
|
|
||||||
const zip = await yauzl_promise(buffer);
|
const zip = await yauzl_promise(buffer);
|
||||||
|
|
||||||
for await (const entry of zip) {
|
for await (const entry of zip) {
|
||||||
if (important_name.includes(entry.fileName)) {
|
if (importantFiles.includes(entry.fileName)) {
|
||||||
contain = entry.fileName;
|
const content = await entry.ReadEntry;
|
||||||
info = JSON.parse((await entry.ReadEntry).toString());
|
const info = JSON.parse(content.toString());
|
||||||
break;
|
logger.debug("Found important file", { fileName: entry.fileName, info });
|
||||||
|
return { contain: entry.fileName, info };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { contain, info };
|
|
||||||
|
throw new Error("No manifest file found in modpack");
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _unzip(buffer: Buffer, instancename: string) {
|
private async _unzip(buffer: Buffer, instancename: string) {
|
||||||
/* 解压Zip */
|
logger.info("Starting unzip process", { instancename });
|
||||||
const zip = await yauzl_promise(buffer);
|
const zip = await yauzl_promise(buffer);
|
||||||
|
const instancePath = `./instance/${instancename}`;
|
||||||
let index = 1;
|
let index = 1;
|
||||||
|
|
||||||
for await (const entry of zip) {
|
for await (const entry of zip) {
|
||||||
const ew = entry.fileName.endsWith("/");
|
const isDir = entry.fileName.endsWith("/");
|
||||||
if (ew) {
|
|
||||||
await fs.promises.mkdir(
|
if (isDir) {
|
||||||
`./instance/${instancename}/${entry.fileName}`,
|
await fs.promises.mkdir(`${instancePath}/${entry.fileName}`, {
|
||||||
{
|
recursive: true,
|
||||||
recursive: true,
|
});
|
||||||
}
|
} else if (entry.fileName.startsWith("overrides/")) {
|
||||||
);
|
// 跳过黑名单文件
|
||||||
}
|
|
||||||
if (!ew && entry.fileName.startsWith("overrides/")) {
|
|
||||||
const dirPath = `./instance/${instancename}/${entry.fileName
|
|
||||||
.substring(0, entry.fileName.lastIndexOf("/"))
|
|
||||||
.replace("overrides/", "")}`;
|
|
||||||
if (this._ublack(entry.fileName)) {
|
if (this._ublack(entry.fileName)) {
|
||||||
index++;
|
index++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 创建目标目录
|
||||||
|
const targetPath = entry.fileName.replace("overrides/", "");
|
||||||
|
const dirPath = `${instancePath}/${targetPath.substring(0, targetPath.lastIndexOf("/"))}`;
|
||||||
await fs.promises.mkdir(dirPath, { recursive: true });
|
await fs.promises.mkdir(dirPath, { recursive: true });
|
||||||
|
|
||||||
|
// 解压文件
|
||||||
const stream = await entry.openReadStream;
|
const stream = await entry.openReadStream;
|
||||||
const write = fs.createWriteStream(
|
const write = fs.createWriteStream(`${instancePath}/${targetPath}`);
|
||||||
`./instance/${instancename}/${entry.fileName.replace(
|
|
||||||
"overrides/",
|
|
||||||
""
|
|
||||||
)}`
|
|
||||||
);
|
|
||||||
await pipeline(stream, write);
|
await pipeline(stream, write);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.message.unzip(entry.fileName, zip.length, index);
|
this.message.unzip(entry.fileName, zip.length, index);
|
||||||
// this.ws.send(
|
|
||||||
// JSON.stringify({
|
|
||||||
// status: "unzip",
|
|
||||||
// result: { name: entry.fileName, total: zip.length, current: index },
|
|
||||||
// })
|
|
||||||
// );
|
|
||||||
index++;
|
index++;
|
||||||
}
|
}
|
||||||
/* 解压完成 */
|
|
||||||
|
logger.info("Unzip process completed", { instancename, totalFiles: zip.length });
|
||||||
}
|
}
|
||||||
|
|
||||||
private _ublack(filename: string) {
|
/**
|
||||||
if (filename === "overrides/") {
|
* 检查文件是否在解压黑名单中
|
||||||
return true;
|
* @param filename 文件名
|
||||||
}
|
* @returns 是否在黑名单中
|
||||||
if (
|
*/
|
||||||
filename.includes("shaderpacks") ||
|
private _ublack(filename: string): boolean {
|
||||||
filename.includes("essential") ||
|
const blacklist = [
|
||||||
filename.includes("resourcepacks") ||
|
"overrides/",
|
||||||
filename === "overrides/options.txt"
|
"overrides/options.txt",
|
||||||
) {
|
"shaderpacks",
|
||||||
return true;
|
"essential",
|
||||||
}
|
"resourcepacks"
|
||||||
|
];
|
||||||
|
|
||||||
|
return blacklist.some(item => filename.includes(item));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import websocket, { WebSocketServer } from "ws"
|
|||||||
import { createServer, Server } from "node:http";
|
import { createServer, Server } from "node:http";
|
||||||
import { Config, IConfig } from "./utils/config.js";
|
import { Config, IConfig } from "./utils/config.js";
|
||||||
import { Dex } from "./Dex.js";
|
import { Dex } from "./Dex.js";
|
||||||
import { exec } from "node:child_process";
|
import { logger } from "./utils/logger.js";
|
||||||
|
import { checkJava, JavaCheckResult } from "./utils/utils.js";
|
||||||
export class Core {
|
export class Core {
|
||||||
private config: IConfig;
|
private config: IConfig;
|
||||||
private readonly app: Application;
|
private readonly app: Application;
|
||||||
@@ -27,56 +28,119 @@ export class Core {
|
|||||||
this.dex = new Dex(this.ws)
|
this.dex = new Dex(this.ws)
|
||||||
}
|
}
|
||||||
|
|
||||||
javachecker(){
|
private async javachecker() {
|
||||||
exec("java -version",(err,stdout,stderr)=>{
|
try {
|
||||||
if(err){
|
const result: JavaCheckResult = await checkJava();
|
||||||
this.wsx.send(JSON.stringify({
|
|
||||||
type:"error",
|
if (result.exists && result.version) {
|
||||||
message:"jini"
|
logger.info(`Java detected: ${result.version.fullVersion} (${result.version.vendor})`);
|
||||||
}))
|
|
||||||
|
if (this.wsx) {
|
||||||
|
this.wsx.send(JSON.stringify({
|
||||||
|
type: "info",
|
||||||
|
message: `Java detected: ${result.version.fullVersion} (${result.version.vendor})`,
|
||||||
|
data: result.version
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.error("Java check failed", result.error);
|
||||||
|
|
||||||
|
if (this.wsx) {
|
||||||
|
this.wsx.send(JSON.stringify({
|
||||||
|
type: "error",
|
||||||
|
message: result.error || "Java not found or version check failed",
|
||||||
|
data: result
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
} catch (error) {
|
||||||
|
logger.error("Java check exception", error as Error);
|
||||||
|
|
||||||
|
if (this.wsx) {
|
||||||
|
this.wsx.send(JSON.stringify({
|
||||||
|
type: "error",
|
||||||
|
message: "Java check encountered an exception"
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
express() {
|
private express() {
|
||||||
this.app.use(cors());
|
this.app.use(cors());
|
||||||
this.app.use(express.json());
|
this.app.use(express.json());
|
||||||
this.app.get('/',(req,res)=>{
|
|
||||||
res.json({
|
|
||||||
status:200,
|
|
||||||
by:"DeEarthX.Core",
|
|
||||||
qqg:"559349662",
|
|
||||||
bilibili:"https://space.bilibili.com/1728953419"
|
|
||||||
})
|
|
||||||
})
|
|
||||||
this.app.post("/start", this.upload.single("file"), (req, res) => {
|
|
||||||
if (!req.file) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!req.query.mode){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.dex.Main(req.file.buffer,req.query.mode == "server") //Dex
|
|
||||||
//this.dex.Main(req.file.buffer)
|
|
||||||
res.json({
|
|
||||||
status:200,
|
|
||||||
message:"task is peding"
|
|
||||||
})
|
|
||||||
})
|
|
||||||
this.app.get('/config/get', (req, res) => {
|
|
||||||
res.json(this.config)
|
|
||||||
})
|
|
||||||
|
|
||||||
|
// 健康检查路由
|
||||||
|
this.app.get('/', (req, res) => {
|
||||||
|
res.json({
|
||||||
|
status: 200,
|
||||||
|
by: "DeEarthX.Core",
|
||||||
|
qqg: "559349662",
|
||||||
|
bilibili: "https://space.bilibili.com/1728953419"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 启动任务路由
|
||||||
|
this.app.post("/start", this.upload.single("file"), (req, res) => {
|
||||||
|
try {
|
||||||
|
if (!req.file) {
|
||||||
|
return res.status(400).json({ status: 400, message: "No file uploaded" });
|
||||||
|
}
|
||||||
|
if (!req.query.mode) {
|
||||||
|
return res.status(400).json({ status: 400, message: "Mode parameter missing" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const isServerMode = req.query.mode === "server";
|
||||||
|
logger.info("Starting task", { isServerMode });
|
||||||
|
|
||||||
|
// 非阻塞执行主要任务
|
||||||
|
this.dex.Main(req.file.buffer, isServerMode).catch(err => {
|
||||||
|
logger.error("Task execution failed", err);
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({ status: 200, message: "Task is pending" });
|
||||||
|
} catch (err) {
|
||||||
|
const error = err as Error;
|
||||||
|
logger.error("/start route error", error);
|
||||||
|
res.status(500).json({ status: 500, message: "Internal server error" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取配置路由
|
||||||
|
this.app.get('/config/get', (req, res) => {
|
||||||
|
try {
|
||||||
|
res.json(this.config);
|
||||||
|
} catch (err) {
|
||||||
|
const error = err as Error;
|
||||||
|
logger.error("/config/get route error", error);
|
||||||
|
res.status(500).json({ status: 500, message: "Failed to get config" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新配置路由
|
||||||
this.app.post('/config/post', (req, res) => {
|
this.app.post('/config/post', (req, res) => {
|
||||||
Config.write_config(req.body)
|
try {
|
||||||
res.json({ status: 200 })
|
Config.writeConfig(req.body);
|
||||||
})
|
this.config = req.body; // 更新内存中的配置
|
||||||
|
logger.info("Config updated");
|
||||||
|
res.json({ status: 200 });
|
||||||
|
} catch (err) {
|
||||||
|
const error = err as Error;
|
||||||
|
logger.error("/config/post route error", error);
|
||||||
|
res.status(500).json({ status: 500, message: "Failed to update config" });
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
start() {
|
public async start() {
|
||||||
this.express()
|
this.express();
|
||||||
this.server.listen(37019, () => {
|
this.server.listen(37019, async () => {
|
||||||
console.log("Server is running on http://localhost:37019")
|
logger.info("Server is running on http://localhost:37019");
|
||||||
})
|
await this.javachecker(); // 启动时检查Java
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理服务器错误
|
||||||
|
this.server.on('error', (err) => {
|
||||||
|
logger.error("Server error", err);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,39 +3,55 @@ import { Forge } from "./forge.js";
|
|||||||
import { Minecraft } from "./minecraft.js";
|
import { Minecraft } from "./minecraft.js";
|
||||||
import { NeoForge } from "./neoforge.js";
|
import { NeoForge } from "./neoforge.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模组加载器接口
|
||||||
|
*/
|
||||||
interface XModloader {
|
interface XModloader {
|
||||||
setup(): Promise<void>;
|
setup(): Promise<void>;
|
||||||
installer(): Promise<void>;
|
installer(): Promise<void>;
|
||||||
}
|
}
|
||||||
export function modloader(ml:string,mcv:string,mlv:string,path:string){
|
/**
|
||||||
let modloader:XModloader
|
* 创建模组加载器实例
|
||||||
switch (ml) {
|
* @param ml 加载器类型
|
||||||
|
* @param mcv Minecraft版本
|
||||||
|
* @param mlv 加载器版本
|
||||||
|
* @param path 安装路径
|
||||||
|
* @returns 加载器实例
|
||||||
|
*/
|
||||||
|
export function modloader(ml: string, mcv: string, mlv: string, path: string): XModloader {
|
||||||
|
switch (ml) {
|
||||||
case "fabric":
|
case "fabric":
|
||||||
modloader = new Fabric(mcv,mlv,path)
|
|
||||||
break;
|
|
||||||
case "fabric-loader":
|
case "fabric-loader":
|
||||||
modloader = new Fabric(mcv,mlv,path)
|
return new Fabric(mcv, mlv, path);
|
||||||
break;
|
|
||||||
case "forge":
|
case "forge":
|
||||||
modloader = new Forge(mcv,mlv,path)
|
return new Forge(mcv, mlv, path);
|
||||||
break;
|
|
||||||
case "neoforge":
|
case "neoforge":
|
||||||
modloader = new NeoForge(mcv,mlv,path)
|
return new NeoForge(mcv, mlv, path);
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
modloader = new Minecraft(ml,mcv,mlv,path)
|
return new Minecraft(ml, mcv, mlv, path);
|
||||||
break;
|
}
|
||||||
}
|
|
||||||
return modloader
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function mlsetup(ml:string,mcv:string,mlv:string,path:string){
|
/**
|
||||||
const minecraft = new Minecraft(ml,mcv,mlv,path);
|
* 设置模组加载器
|
||||||
//console.log(ml)
|
* @param ml 加载器类型
|
||||||
await modloader(ml,mcv,mlv,path).setup()
|
* @param mcv Minecraft版本
|
||||||
await minecraft.setup()
|
* @param mlv 加载器版本
|
||||||
|
* @param path 安装路径
|
||||||
|
*/
|
||||||
|
export async function mlsetup(ml: string, mcv: string, mlv: string, path: string): Promise<void> {
|
||||||
|
const minecraft = new Minecraft(ml, mcv, mlv, path);
|
||||||
|
await modloader(ml, mcv, mlv, path).setup();
|
||||||
|
await minecraft.setup();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function dinstall(ml:string,mcv:string,mlv:string,path:string){
|
/**
|
||||||
await modloader(ml,mcv,mlv,path).installer();
|
* 安装模组加载器
|
||||||
|
* @param ml 加载器类型
|
||||||
|
* @param mcv Minecraft版本
|
||||||
|
* @param mlv 加载器版本
|
||||||
|
* @param path 安装路径
|
||||||
|
*/
|
||||||
|
export async function dinstall(ml: string, mcv: string, mlv: string, path: string): Promise<void> {
|
||||||
|
await modloader(ml, mcv, mlv, path).installer();
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,8 @@
|
|||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用配置接口
|
||||||
|
*/
|
||||||
export interface IConfig {
|
export interface IConfig {
|
||||||
mirror: {
|
mirror: {
|
||||||
bmclapi: boolean;
|
bmclapi: boolean;
|
||||||
@@ -9,32 +13,65 @@ export interface IConfig {
|
|||||||
dexpub: boolean;
|
dexpub: boolean;
|
||||||
mixins: boolean;
|
mixins: boolean;
|
||||||
};
|
};
|
||||||
oaf: boolean
|
oaf: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认配置
|
||||||
|
*/
|
||||||
|
const DEFAULT_CONFIG: IConfig = {
|
||||||
|
mirror: {
|
||||||
|
bmclapi: true,
|
||||||
|
mcimirror: true,
|
||||||
|
},
|
||||||
|
filter: {
|
||||||
|
hashes: true,
|
||||||
|
dexpub: false,
|
||||||
|
mixins: true,
|
||||||
|
},
|
||||||
|
oaf: true
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配置文件路径
|
||||||
|
*/
|
||||||
|
const CONFIG_PATH = "./config.json";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配置管理器
|
||||||
|
*/
|
||||||
export class Config {
|
export class Config {
|
||||||
private readonly default_config: IConfig = {
|
/**
|
||||||
mirror: {
|
* 获取配置
|
||||||
bmclapi: true,
|
* @returns 配置对象
|
||||||
mcimirror: true,
|
*/
|
||||||
},
|
public static getConfig(): IConfig {
|
||||||
filter: {
|
if (!fs.existsSync(CONFIG_PATH)) {
|
||||||
hashes: true,
|
fs.writeFileSync(CONFIG_PATH, JSON.stringify(DEFAULT_CONFIG, null, 2));
|
||||||
dexpub: false,
|
return DEFAULT_CONFIG;
|
||||||
mixins: true,
|
}
|
||||||
},
|
|
||||||
oaf:true
|
try {
|
||||||
};
|
const content = fs.readFileSync(CONFIG_PATH, "utf-8");
|
||||||
config(): IConfig {
|
return JSON.parse(content);
|
||||||
if (!fs.existsSync("./config.json")) {
|
} catch (err) {
|
||||||
fs.writeFileSync("./config.json", JSON.stringify(this.default_config));
|
console.error("Failed to read config file, using defaults", err);
|
||||||
return this.default_config;
|
return DEFAULT_CONFIG;
|
||||||
}
|
}
|
||||||
return JSON.parse(fs.readFileSync("./config.json", "utf-8"));
|
|
||||||
}
|
}
|
||||||
static write_config(config: IConfig) {
|
|
||||||
fs.writeFileSync("./config.json", JSON.stringify(config));
|
/**
|
||||||
|
* 写入配置
|
||||||
|
* @param config 配置对象
|
||||||
|
*/
|
||||||
|
public static writeConfig(config: IConfig): void {
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to write config file", err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new Config().config();
|
// 默认导出配置实例
|
||||||
|
export default Config.getConfig();
|
||||||
|
|||||||
@@ -1,17 +1,42 @@
|
|||||||
const env = process.env.DEBUG;
|
const env = process.env.DEBUG;
|
||||||
|
const isDebug = env === "true";
|
||||||
|
|
||||||
export function debug(msg: any){
|
// 日志级别枚举
|
||||||
if (env === "true"){
|
type LogLevel = "debug" | "info" | "warn" | "error";
|
||||||
if(msg instanceof Error){
|
|
||||||
console.log(`[ERROR] [${new Date().toLocaleString()}] `);
|
/**
|
||||||
console.log(msg);
|
* 日志记录器
|
||||||
}
|
* @param level 日志级别
|
||||||
if (typeof msg === "string"){
|
* @param msg 日志内容
|
||||||
console.log(`[DEBUG] [${new Date().toLocaleString()}] ` + msg);
|
* @param data 附加数据(可选)
|
||||||
}
|
*/
|
||||||
if (typeof msg === "object"){
|
function log(level: LogLevel, msg: string | Error, data?: any) {
|
||||||
console.log(`[OBJ] [${new Date().toLocaleString()}] `);
|
const timestamp = new Date().toLocaleString();
|
||||||
console.log(msg);
|
const prefix = `[${level.toUpperCase()}] [${timestamp}]`;
|
||||||
}
|
|
||||||
}
|
// 确保只在调试模式下输出debug日志
|
||||||
|
if (level === "debug" && !isDebug) return;
|
||||||
|
|
||||||
|
if (msg instanceof Error) {
|
||||||
|
console.error(`${prefix} ${msg.message}`);
|
||||||
|
console.error(msg.stack);
|
||||||
|
if (data) console.error(`${prefix} Data:`, data);
|
||||||
|
} else {
|
||||||
|
const logFunc = level === "error" ? console.error :
|
||||||
|
level === "warn" ? console.warn :
|
||||||
|
console.log;
|
||||||
|
|
||||||
|
logFunc(`${prefix} ${msg}`);
|
||||||
|
if (data) logFunc(`${prefix} Data:`, data);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const logger = {
|
||||||
|
debug: (msg: string | Error, data?: any) => log("debug", msg, data),
|
||||||
|
info: (msg: string | Error, data?: any) => log("info", msg, data),
|
||||||
|
warn: (msg: string | Error, data?: any) => log("warn", msg, data),
|
||||||
|
error: (msg: string | Error, data?: any) => log("error", msg, data)
|
||||||
|
};
|
||||||
|
|
||||||
|
// 保持向后兼容
|
||||||
|
export const debug = logger.debug;
|
||||||
|
|||||||
@@ -6,7 +6,29 @@ import fs from "node:fs";
|
|||||||
import fse from "fs-extra";
|
import fse from "fs-extra";
|
||||||
import { WebSocket } from "ws";
|
import { WebSocket } from "ws";
|
||||||
import { ExecOptions, exec} from "node:child_process";
|
import { ExecOptions, exec} from "node:child_process";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Java版本信息接口
|
||||||
|
*/
|
||||||
|
export interface JavaVersion {
|
||||||
|
major: number;
|
||||||
|
minor: number;
|
||||||
|
patch: number;
|
||||||
|
fullVersion: string;
|
||||||
|
vendor: string;
|
||||||
|
runtimeVersion?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Java检测结果接口
|
||||||
|
*/
|
||||||
|
export interface JavaCheckResult {
|
||||||
|
exists: boolean;
|
||||||
|
version?: JavaVersion;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
import { MessageWS } from "./ws.js";
|
import { MessageWS } from "./ws.js";
|
||||||
|
import { logger } from "./logger.js";
|
||||||
|
|
||||||
export class Utils {
|
export class Utils {
|
||||||
public modrinth_url: string;
|
public modrinth_url: string;
|
||||||
@@ -50,90 +72,157 @@ export function version_compare(v1: string, v2: string) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测Java是否安装并获取版本信息
|
||||||
|
* @returns Java检测结果
|
||||||
|
*/
|
||||||
|
export async function checkJava(): Promise<JavaCheckResult> {
|
||||||
|
try {
|
||||||
|
const output = await new Promise<string>((resolve, reject) => {
|
||||||
|
exec("java -version", (err, stdout, stderr) => {
|
||||||
|
if (err) {
|
||||||
|
logger.error("Java check failed", err);
|
||||||
|
reject(new Error("Java not found"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Java版本信息输出在stderr中
|
||||||
|
resolve(stderr);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.debug(`Java version output: ${output}`);
|
||||||
|
|
||||||
|
// 解析Java版本信息
|
||||||
|
const versionRegex = /version "(\d+)(\.(\d+))?(\.(\d+))?/;
|
||||||
|
const vendorRegex = /(Java\(TM\)|OpenJDK).*Runtime Environment.*by (.*)/;
|
||||||
|
|
||||||
|
const versionMatch = output.match(versionRegex);
|
||||||
|
const vendorMatch = output.match(vendorRegex);
|
||||||
|
|
||||||
|
if (!versionMatch) {
|
||||||
|
return {
|
||||||
|
exists: true,
|
||||||
|
error: "Failed to parse Java version"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const major = parseInt(versionMatch[1], 10);
|
||||||
|
const minor = versionMatch[3] ? parseInt(versionMatch[3], 10) : 0;
|
||||||
|
const patch = versionMatch[5] ? parseInt(versionMatch[5], 10) : 0;
|
||||||
|
|
||||||
|
const versionInfo: JavaVersion = {
|
||||||
|
major,
|
||||||
|
minor,
|
||||||
|
patch,
|
||||||
|
fullVersion: versionMatch[0].replace("version ", ""),
|
||||||
|
vendor: vendorMatch ? vendorMatch[2] : "Unknown"
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.info(`Java detected: ${JSON.stringify(versionInfo)}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
exists: true,
|
||||||
|
version: versionInfo
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Java check error", error as Error);
|
||||||
|
return {
|
||||||
|
exists: false,
|
||||||
|
error: (error as Error).message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function execPromise(cmd:string,options?:ExecOptions){
|
export function execPromise(cmd:string,options?:ExecOptions){
|
||||||
|
logger.debug(`Executing command: ${cmd}`);
|
||||||
return new Promise((resolve,reject)=>{
|
return new Promise((resolve,reject)=>{
|
||||||
exec(cmd,options,(err,stdout,stderr)=>{
|
exec(cmd,options,(err,stdout,stderr)=>{
|
||||||
if(err){
|
if(err){
|
||||||
|
logger.error(`Command execution failed: ${cmd}`, err);
|
||||||
|
logger.debug(`Stderr: ${stderr}`);
|
||||||
reject(err)
|
reject(err)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (stdout) logger.debug(`Command stdout: ${stdout}`);
|
||||||
|
if (stderr) logger.debug(`Command stderr: ${stderr}`);
|
||||||
}).on('exit',(code)=>{
|
}).on('exit',(code)=>{
|
||||||
|
logger.debug(`Command completed with exit code: ${code}`);
|
||||||
resolve(code)
|
resolve(code)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fastdownload(data: [string, string]|string[][]) {
|
export async function fastdownload(data: [string, string]|string[][]) {
|
||||||
let _data = undefined;
|
// 确保downloadList始终是[string, string][]类型
|
||||||
if(Array.isArray(data[0])){
|
const downloadList: [string, string][] = Array.isArray(data[0])
|
||||||
_data = data
|
? (data as string[][]).map(item => item as [string, string])
|
||||||
}else{
|
: [data as [string, string]];
|
||||||
_data = [data]
|
logger.info(`Starting fast download of ${downloadList.length} files`);
|
||||||
}
|
|
||||||
return await pMap(
|
return await pMap(
|
||||||
_data,
|
downloadList,
|
||||||
async (e:any) => {
|
async (item: [string, string]) => {
|
||||||
|
const [url, filePath] = item;
|
||||||
try {
|
try {
|
||||||
await pRetry(
|
await pRetry(
|
||||||
async () => {
|
async () => {
|
||||||
if (!fs.existsSync(e[1])) {
|
if (!fs.existsSync(filePath)) {
|
||||||
await got
|
logger.debug(`Downloading ${url} to ${filePath}`);
|
||||||
.get(e[0], {
|
const res = await got.get(url, {
|
||||||
responseType: "buffer",
|
responseType: "buffer",
|
||||||
headers: {
|
headers: { "user-agent": "DeEarthX" },
|
||||||
"user-agent": "DeEarthX",
|
});
|
||||||
},
|
fse.outputFileSync(filePath, res.rawBody);
|
||||||
})
|
logger.debug(`Downloaded ${url} successfully`);
|
||||||
.then((res) => {
|
} else {
|
||||||
fse.outputFileSync(e[1], res.rawBody);
|
logger.debug(`File already exists, skipping: ${filePath}`);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ retries: 3 }
|
{ retries: 3, onFailedAttempt: (error) => {
|
||||||
|
logger.warn(`Download attempt failed for ${url}, retrying (${error.attemptNumber}/3)`);
|
||||||
|
}}
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
//LOGGER.error({ err: e });
|
logger.error(`Failed to download ${url} after 3 attempts`, error);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ concurrency: 16 }
|
{ concurrency: 16 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function Wfastdownload(data: string[][],ws:MessageWS) {
|
export async function Wfastdownload(data: string[][], ws: MessageWS) {
|
||||||
let index = 1;
|
logger.info(`Starting web download of ${data.length} files`);
|
||||||
|
|
||||||
return await pMap(
|
return await pMap(
|
||||||
data,
|
data,
|
||||||
async (e:any) => {
|
async (item: string[], idx: number) => {
|
||||||
|
const [url, filePath] = item;
|
||||||
try {
|
try {
|
||||||
await pRetry(
|
await pRetry(
|
||||||
async () => {
|
async () => {
|
||||||
if (!fs.existsSync(e[1])) {
|
if (!fs.existsSync(filePath)) {
|
||||||
await got
|
logger.debug(`Downloading ${url} to ${filePath}`);
|
||||||
.get(e[0], {
|
const res = await got.get(url, {
|
||||||
responseType: "buffer",
|
responseType: "buffer",
|
||||||
headers: {
|
headers: { "user-agent": "DeEarthX" },
|
||||||
"user-agent": "DeEarthX",
|
});
|
||||||
},
|
fse.outputFileSync(filePath, res.rawBody);
|
||||||
})
|
logger.debug(`Downloaded ${url} successfully`);
|
||||||
.then((res) => {
|
} else {
|
||||||
fse.outputFileSync(e[1], res.rawBody);
|
logger.debug(`File already exists, skipping: ${filePath}`);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
ws.download(data.length,index,e[1])
|
|
||||||
// ws.send(JSON.stringify({
|
// 更新下载进度
|
||||||
// status:"downloading",
|
ws.download(data.length, idx + 1, filePath);
|
||||||
// result:{
|
|
||||||
// total:data.length,
|
|
||||||
// index:index,
|
|
||||||
// name:e[1]
|
|
||||||
// }
|
|
||||||
// }))
|
|
||||||
index++
|
|
||||||
},
|
},
|
||||||
{ retries: 3 }
|
{ retries: 3, onFailedAttempt: (error) => {
|
||||||
|
logger.warn(`Download attempt failed for ${url}, retrying (${error.attemptNumber}/3)`);
|
||||||
|
}}
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
//LOGGER.error({ err: e });
|
logger.error(`Failed to download ${url} after 3 attempts`, error);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ concurrency: 16 }
|
{ concurrency: 16 }
|
||||||
|
|||||||
@@ -1,56 +1,83 @@
|
|||||||
import websocket, { WebSocketServer } from "ws";
|
import websocket, { WebSocketServer } from "ws";
|
||||||
|
import { logger } from "./logger.js";
|
||||||
|
|
||||||
export class MessageWS {
|
export class MessageWS {
|
||||||
ws!: websocket;
|
private ws!: websocket;
|
||||||
|
|
||||||
constructor(ws: websocket) {
|
constructor(ws: websocket) {
|
||||||
this.ws = ws;
|
this.ws = ws;
|
||||||
|
|
||||||
|
// 监听WebSocket错误
|
||||||
|
this.ws.on('error', (err) => {
|
||||||
|
logger.error("WebSocket error", err);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听连接关闭
|
||||||
|
this.ws.on('close', (code, reason) => {
|
||||||
|
logger.info("WebSocket connection closed", { code, reason: reason.toString() });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
finish(first: number, latest: number) {
|
/**
|
||||||
this.ws.send(
|
* 发送完成消息
|
||||||
JSON.stringify({
|
* @param startTime 开始时间
|
||||||
status: "finish",
|
* @param endTime 结束时间
|
||||||
result: latest - first,
|
*/
|
||||||
})
|
finish(startTime: number, endTime: number) {
|
||||||
);
|
this.send("finish", endTime - startTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送解压进度消息
|
||||||
|
* @param entryName 文件名
|
||||||
|
* @param total 总文件数
|
||||||
|
* @param current 当前文件索引
|
||||||
|
*/
|
||||||
unzip(entryName: string, total: number, current: number) {
|
unzip(entryName: string, total: number, current: number) {
|
||||||
this.ws.send(
|
this.send("unzip", { name: entryName, total, current });
|
||||||
JSON.stringify({
|
|
||||||
status: "unzip",
|
|
||||||
result: { name: entryName, total, current },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送下载进度消息
|
||||||
|
* @param total 总文件数
|
||||||
|
* @param index 当前文件索引
|
||||||
|
* @param name 文件名
|
||||||
|
*/
|
||||||
download(total: number, index: number, name: string) {
|
download(total: number, index: number, name: string) {
|
||||||
this.ws.send(
|
this.send("downloading", { total, index, name });
|
||||||
JSON.stringify({
|
|
||||||
status: "downloading",
|
|
||||||
result: {
|
|
||||||
total,
|
|
||||||
index,
|
|
||||||
name,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送状态变更消息
|
||||||
|
*/
|
||||||
statusChange() {
|
statusChange() {
|
||||||
this.ws.send(
|
this.send("changed", undefined);
|
||||||
JSON.stringify({
|
|
||||||
status: "changed",
|
|
||||||
result: undefined,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送错误消息
|
||||||
|
* @param error 错误对象
|
||||||
|
*/
|
||||||
handleError(error: Error) {
|
handleError(error: Error) {
|
||||||
this.ws.send(
|
this.send("error", error.message);
|
||||||
JSON.stringify({
|
}
|
||||||
status: "error",
|
|
||||||
result: error.message,
|
/**
|
||||||
})
|
* 通用消息发送方法
|
||||||
);
|
* @param status 消息状态
|
||||||
|
* @param result 消息内容
|
||||||
|
*/
|
||||||
|
private send(status: string, result: any) {
|
||||||
|
try {
|
||||||
|
if (this.ws.readyState === websocket.OPEN) {
|
||||||
|
const message = JSON.stringify({ status, result });
|
||||||
|
logger.debug("Sending WebSocket message", { status, result });
|
||||||
|
this.ws.send(message);
|
||||||
|
} else {
|
||||||
|
logger.warn(`WebSocket not open, cannot send message: ${status}`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.error("Failed to send WebSocket message", err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user