Compare commits
6 Commits
0a47d14627
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45b28b3b34 | ||
|
|
b8b09fdb99 | ||
|
|
295f1fbece | ||
|
|
0f8ceba972 | ||
|
|
6f256048f0 | ||
|
|
475d028ed9 |
1
.gitignore
vendored
@@ -29,3 +29,4 @@ config.json
|
|||||||
*.exe
|
*.exe
|
||||||
*.jar
|
*.jar
|
||||||
instance
|
instance
|
||||||
|
*.mrpack
|
||||||
13
backend/package-lock.json
generated
@@ -18,6 +18,7 @@
|
|||||||
"p-map": "^7.0.3",
|
"p-map": "^7.0.3",
|
||||||
"p-retry": "^7.0.0",
|
"p-retry": "^7.0.0",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
|
"smol-toml": "^1.6.0",
|
||||||
"ws": "^8.18.3",
|
"ws": "^8.18.3",
|
||||||
"yauzl": "^3.2.0"
|
"yauzl": "^3.2.0"
|
||||||
},
|
},
|
||||||
@@ -5723,6 +5724,18 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/smol-toml": {
|
||||||
|
"version": "1.6.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/smol-toml/-/smol-toml-1.6.0.tgz",
|
||||||
|
"integrity": "sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/cyyynthia"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/sort-keys": {
|
"node_modules/sort-keys": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmmirror.com/sort-keys/-/sort-keys-1.1.2.tgz",
|
"resolved": "https://registry.npmmirror.com/sort-keys/-/sort-keys-1.1.2.tgz",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "tsc&&node dist/main.js",
|
"test": "set \"DEBUG=true\"&&tsc&&node dist/main.js",
|
||||||
"rollup": "rollup -c rollup.config.js",
|
"rollup": "rollup -c rollup.config.js",
|
||||||
"nexe": "nexe -i ./dist/bundle.js --build -t x86-22.13.0 --output ./dist/core.exe",
|
"nexe": "nexe -i ./dist/bundle.js --build -t x86-22.13.0 --output ./dist/core.exe",
|
||||||
"build": "npm run rollup && npm run nexe"
|
"build": "npm run rollup && npm run nexe"
|
||||||
@@ -39,6 +39,7 @@
|
|||||||
"p-map": "^7.0.3",
|
"p-map": "^7.0.3",
|
||||||
"p-retry": "^7.0.0",
|
"p-retry": "^7.0.0",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
|
"smol-toml": "^1.6.0",
|
||||||
"ws": "^8.18.3",
|
"ws": "^8.18.3",
|
||||||
"yauzl": "^3.2.0"
|
"yauzl": "^3.2.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { execPromise } from "./utils/utils.js";
|
|||||||
import { MessageWS } from "./utils/ws.js";
|
import { MessageWS } from "./utils/ws.js";
|
||||||
import { logger } from "./utils/logger.js";
|
import { logger } from "./utils/logger.js";
|
||||||
import { yauzl_promise } from "./utils/ziplib.js";
|
import { yauzl_promise } from "./utils/ziplib.js";
|
||||||
|
import yauzl from "yauzl";
|
||||||
|
|
||||||
export class Dex {
|
export class Dex {
|
||||||
wsx!: WebSocketServer;
|
wsx!: WebSocketServer;
|
||||||
@@ -22,11 +23,17 @@ export class Dex {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Main(buffer: Buffer, dser: boolean) {
|
public async Main(buffer: Buffer, dser: boolean, filename?: string) {
|
||||||
try {
|
try {
|
||||||
const first = Date.now();
|
const first = Date.now();
|
||||||
const zps = await this._zips(buffer);
|
const processedBuffer = await this._processModpack(buffer, filename);
|
||||||
|
const zps = await this._zips(processedBuffer);
|
||||||
const { contain, info } = await zps._getinfo();
|
const { contain, info } = await zps._getinfo();
|
||||||
|
if (!contain || !info) {
|
||||||
|
logger.error("Modpack info is empty");
|
||||||
|
this.message.handleError(new Error("It seems that the modpack is not a valid modpack."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
const plat = what_platform(contain);
|
const plat = what_platform(contain);
|
||||||
logger.debug("Platform detected", plat);
|
logger.debug("Platform detected", plat);
|
||||||
logger.debug("Modpack info", info);
|
logger.debug("Modpack info", info);
|
||||||
@@ -72,8 +79,86 @@ export class Dex {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async _processModpack(buffer: Buffer, filename?: string): Promise<Buffer> {
|
||||||
|
if (!filename || !filename.endsWith('.zip')) {
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const zip = await (new Promise((resolve, reject) => {
|
||||||
|
yauzl.fromBuffer(buffer, { lazyEntries: true }, (err, zipfile) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve(zipfile);
|
||||||
|
});
|
||||||
|
}) as Promise<yauzl.ZipFile>);
|
||||||
|
logger.info("Modpack zip file detected,It is a PCL packege,try to extract modpack.mrpack");
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let mrpackBuffer: Buffer | null = null;
|
||||||
|
let hasProcessed = false;
|
||||||
|
|
||||||
|
zip.on('entry', (entry: yauzl.Entry) => {
|
||||||
|
if (hasProcessed || !entry.fileName.endsWith('modpack.mrpack')) {
|
||||||
|
zip.readEntry();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.fileName === 'modpack.mrpack') {
|
||||||
|
hasProcessed = true;
|
||||||
|
zip.openReadStream(entry, (err, stream) => {
|
||||||
|
if (err) {
|
||||||
|
zip.close();
|
||||||
|
reject(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
stream.on('data', (chunk) => {
|
||||||
|
chunks.push(chunk);
|
||||||
|
});
|
||||||
|
stream.on('end', () => {
|
||||||
|
mrpackBuffer = Buffer.concat(chunks);
|
||||||
|
zip.close();
|
||||||
|
resolve(mrpackBuffer);
|
||||||
|
});
|
||||||
|
stream.on('error', (err) => {
|
||||||
|
zip.close();
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
zip.readEntry();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
zip.on('end', () => {
|
||||||
|
if (!hasProcessed) {
|
||||||
|
zip.close();
|
||||||
|
resolve(buffer);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
zip.on('error', (err) => {
|
||||||
|
zip.close();
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
zip.readEntry();
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn('Failed to check for modpack.mrpack, using original buffer', e);
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async _zips(buffer: Buffer) {
|
private async _zips(buffer: Buffer) {
|
||||||
|
if (buffer.length === 0) {
|
||||||
|
throw new Error("zip buffer is empty");
|
||||||
|
}
|
||||||
const zip = await yauzl_promise(buffer);
|
const zip = await yauzl_promise(buffer);
|
||||||
|
let index = 0;
|
||||||
const _getinfo = async () => {
|
const _getinfo = async () => {
|
||||||
const importantFiles = ["manifest.json", "modrinth.index.json"];
|
const importantFiles = ["manifest.json", "modrinth.index.json"];
|
||||||
for await (const entry of zip) {
|
for await (const entry of zip) {
|
||||||
@@ -83,10 +168,13 @@ export class Dex {
|
|||||||
logger.debug("Found important file", { fileName: entry.fileName, info });
|
logger.debug("Found important file", { fileName: entry.fileName, info });
|
||||||
return { contain: entry.fileName, info };
|
return { contain: entry.fileName, info };
|
||||||
}
|
}
|
||||||
|
index++;
|
||||||
}
|
}
|
||||||
throw new Error("No manifest file found in modpack");
|
throw new Error("No manifest file found in modpack");
|
||||||
}
|
}
|
||||||
|
if (index === zip.length) {
|
||||||
|
throw new Error("No manifest file found in modpack");
|
||||||
|
}
|
||||||
const _unzip = async (instancename: string) => {
|
const _unzip = async (instancename: string) => {
|
||||||
logger.info("Starting unzip process", { instancename });
|
logger.info("Starting unzip process", { instancename });
|
||||||
const instancePath = `./instance/${instancename}`;
|
const instancePath = `./instance/${instancename}`;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { Config, IConfig } from "./utils/config.js";
|
|||||||
import { Dex } from "./Dex.js";
|
import { Dex } from "./Dex.js";
|
||||||
import { logger } from "./utils/logger.js";
|
import { logger } from "./utils/logger.js";
|
||||||
import { checkJava, JavaCheckResult } from "./utils/utils.js";
|
import { checkJava, JavaCheckResult } from "./utils/utils.js";
|
||||||
|
import { Galaxy } from "./galaxy.js";
|
||||||
export class Core {
|
export class Core {
|
||||||
private config: IConfig;
|
private config: IConfig;
|
||||||
private readonly app: Application;
|
private readonly app: Application;
|
||||||
@@ -16,16 +17,18 @@ export class Core {
|
|||||||
private readonly upload: multer.Multer;
|
private readonly upload: multer.Multer;
|
||||||
private task: {} = {};
|
private task: {} = {};
|
||||||
dex: Dex;
|
dex: Dex;
|
||||||
|
galaxy: Galaxy;
|
||||||
constructor(config: IConfig) {
|
constructor(config: IConfig) {
|
||||||
this.config = config
|
this.config = config
|
||||||
this.app = express();
|
this.app = express();
|
||||||
this.server = createServer(this.app);
|
this.server = createServer(this.app);
|
||||||
this.upload = multer()
|
|
||||||
this.ws = new WebSocketServer({ server: this.server })
|
this.ws = new WebSocketServer({ server: this.server })
|
||||||
this.ws.on("connection",(e)=>{
|
this.ws.on("connection",(e)=>{
|
||||||
this.wsx = e
|
this.wsx = e
|
||||||
})
|
})
|
||||||
this.dex = new Dex(this.ws)
|
this.dex = new Dex(this.ws)
|
||||||
|
this.galaxy = new Galaxy()
|
||||||
|
this.upload = multer();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async javachecker() {
|
private async javachecker() {
|
||||||
@@ -93,7 +96,7 @@ export class Core {
|
|||||||
logger.info("Starting task", { isServerMode });
|
logger.info("Starting task", { isServerMode });
|
||||||
|
|
||||||
// 非阻塞执行主要任务
|
// 非阻塞执行主要任务
|
||||||
this.dex.Main(req.file.buffer, isServerMode).catch(err => {
|
this.dex.Main(req.file.buffer, isServerMode, req.file.originalname).catch(err => {
|
||||||
logger.error("Task execution failed", err);
|
logger.error("Task execution failed", err);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -129,6 +132,8 @@ export class Core {
|
|||||||
res.status(500).json({ status: 500, message: "Failed to update config" });
|
res.status(500).json({ status: 500, message: "Failed to update config" });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.app.use("/galaxy", this.galaxy.getRouter());
|
||||||
}
|
}
|
||||||
|
|
||||||
public async start() {
|
public async start() {
|
||||||
|
|||||||
84
backend/src/galaxy.ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import express from "express";
|
||||||
|
import toml from "smol-toml";
|
||||||
|
import multer, { Multer } from "multer";
|
||||||
|
import AdmZip from "adm-zip";
|
||||||
|
import { logger } from "./utils/logger.js";
|
||||||
|
import got, { Got } from "got";
|
||||||
|
|
||||||
|
export class Galaxy {
|
||||||
|
private readonly upload: multer.Multer;
|
||||||
|
got: Got;
|
||||||
|
constructor() {
|
||||||
|
this.upload = multer()
|
||||||
|
this.got = got.extend({
|
||||||
|
prefixUrl: "https://galaxy.tianpao.top/",
|
||||||
|
//prefixUrl: "http://localhost:3000/",
|
||||||
|
headers: {
|
||||||
|
"User-Agent": "DeEarthX",
|
||||||
|
},
|
||||||
|
responseType: "json",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
getRouter() {
|
||||||
|
const router = express.Router();
|
||||||
|
router.use(express.json()); // 解析 JSON 请求体
|
||||||
|
router.post("/upload",this.upload.array("files"), (req, res) => {
|
||||||
|
const files = req.files as Express.Multer.File[];
|
||||||
|
if(!files || files.length === 0){
|
||||||
|
res.status(400).json({ status: 400, message: "No file uploaded" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const modids = this.getModids(files);
|
||||||
|
logger.info("Uploaded modids", modids);
|
||||||
|
res.json({modids}).end();
|
||||||
|
});
|
||||||
|
router.post("/submit/:type",(req,res)=>{
|
||||||
|
const type = req.params.type;
|
||||||
|
if(type !== "server" && type !== "client"){
|
||||||
|
res.status(400).json({ status: 400, message: "Invalid type" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const modid = req.body.modids as string;
|
||||||
|
if(!modid){
|
||||||
|
res.status(400).json({ status: 400, message: "No modid provided" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.got.post(`api/mod/submit/${type}`,{
|
||||||
|
json: {
|
||||||
|
modid,
|
||||||
|
}
|
||||||
|
}).then((response)=>{
|
||||||
|
logger.info(`Submitted modids for ${type}:`, response.body);
|
||||||
|
res.json(response.body).end();
|
||||||
|
}).catch((error)=>{
|
||||||
|
logger.error(`Failed to submit modids for ${type}:`, error);
|
||||||
|
res.status(500).json({ status: 500, message: "Failed to submit modids" });
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return router;
|
||||||
|
}
|
||||||
|
|
||||||
|
getModids(files:Express.Multer.File[]):string[] {
|
||||||
|
let modid:string[] = [];
|
||||||
|
for(const file of files){
|
||||||
|
const zip = new AdmZip(file.buffer);
|
||||||
|
const entries = zip.getEntries();
|
||||||
|
for(const entry of entries){
|
||||||
|
if(entry.entryName.endsWith("mods.toml")){
|
||||||
|
const content = entry.getData().toString("utf8");
|
||||||
|
const config = toml.parse(content) as any;
|
||||||
|
modid.push(config.mods[0].modId as string)
|
||||||
|
}else if(entry.entryName.endsWith("neoforge.mods.toml")){
|
||||||
|
const content = entry.getData().toString("utf8");
|
||||||
|
const config = toml.parse(content) as any;
|
||||||
|
modid.push(config.mods[0].modId as string)
|
||||||
|
}else if(entry.entryName.endsWith("fabric.mod.json")){
|
||||||
|
const content = entry.getData().toString("utf8");
|
||||||
|
const config = JSON.parse(content);
|
||||||
|
modid.push(config.id as string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return modid
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import { Fabric } from "./fabric.js";
|
|||||||
import { Forge } from "./forge.js";
|
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";
|
||||||
|
import fs from "node:fs";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 模组加载器接口
|
* 模组加载器接口
|
||||||
@@ -54,4 +55,18 @@ export async function mlsetup(ml: string, mcv: string, mlv: string, path: string
|
|||||||
*/
|
*/
|
||||||
export async function dinstall(ml: string, mcv: string, mlv: string, path: string): Promise<void> {
|
export async function dinstall(ml: string, mcv: string, mlv: string, path: string): Promise<void> {
|
||||||
await modloader(ml, mcv, mlv, path).installer();
|
await modloader(ml, mcv, mlv, path).installer();
|
||||||
|
|
||||||
|
let cmd = '';
|
||||||
|
if (ml === 'forge' || ml === 'neoforge') {
|
||||||
|
cmd = `java -jar forge-${mcv}-${mlv}-installer.jar --installServer`;
|
||||||
|
} else if (ml === 'fabric' || ml === 'fabric-loader') {
|
||||||
|
await fs.promises.writeFile(`${path}/run.bat`,`@echo off\njava -jar fabric-server-launch.jar\n`)
|
||||||
|
await fs.promises.writeFile(`${path}/run.sh`,`#!/bin/bash\njava -jar fabric-server-launch.jar\n`)
|
||||||
|
cmd = `java -jar fabric-installer.jar server -dir . -mcversion ${mcv} -loader ${mlv} -downloadMinecraft`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cmd) {
|
||||||
|
await fs.promises.writeFile(`${path}/install.bat`, `@echo off\n${cmd}\necho Install Successfully,Enter Some Key to Exit!\npause\n`);
|
||||||
|
await fs.promises.writeFile(`${path}/install.sh`, `#!/bin/bash\n${cmd}\n`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,20 +1,27 @@
|
|||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import crypto from "node:crypto";
|
import crypto from "node:crypto";
|
||||||
import { Azip } from "./ziplib.js";
|
import { Azip } from "./ziplib.js";
|
||||||
import got from "got";
|
import got, { Got } from "got";
|
||||||
import { Utils } from "./utils.js";
|
import { Utils } from "./utils.js";
|
||||||
import config from "./config.js";
|
import config from "./config.js";
|
||||||
import { logger } from "./logger.js";
|
import { logger } from "./logger.js";
|
||||||
|
import toml from "smol-toml";
|
||||||
|
|
||||||
interface IMixinFile {
|
interface IMixinFile {
|
||||||
name: string;
|
name: string;
|
||||||
data: string;
|
data: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IInfoFile {
|
||||||
|
name: string;
|
||||||
|
data: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface IFileInfo {
|
interface IFileInfo {
|
||||||
filename: string;
|
filename: string;
|
||||||
hash: string;
|
hash: string;
|
||||||
mixins: IMixinFile[];
|
mixins: IMixinFile[];
|
||||||
|
infos: IInfoFile[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IHashResponse {
|
interface IHashResponse {
|
||||||
@@ -27,17 +34,30 @@ interface IProjectInfo {
|
|||||||
server_side: string;
|
server_side: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface checkDexpubForClientMods {
|
||||||
|
serverMods: string[];
|
||||||
|
clientMods: string[];
|
||||||
|
}
|
||||||
|
|
||||||
export class DeEarth {
|
export class DeEarth {
|
||||||
private movePath: string;
|
private movePath: string;
|
||||||
private modsPath: string;
|
private modsPath: string;
|
||||||
private files: IFileInfo[];
|
private files: IFileInfo[];
|
||||||
private utils: Utils;
|
private utils: Utils;
|
||||||
|
private got: Got;
|
||||||
|
|
||||||
constructor(modsPath: string, movePath: string) {
|
constructor(modsPath: string, movePath: string) {
|
||||||
this.utils = new Utils();
|
this.utils = new Utils();
|
||||||
this.movePath = movePath;
|
this.movePath = movePath;
|
||||||
this.modsPath = modsPath;
|
this.modsPath = modsPath;
|
||||||
this.files = [];
|
this.files = [];
|
||||||
|
this.got = got.extend({
|
||||||
|
prefixUrl: "https://galaxy.tianpao.top/",
|
||||||
|
headers: {
|
||||||
|
"User-Agent": "DeEarthX",
|
||||||
|
},
|
||||||
|
responseType: "json",
|
||||||
|
});
|
||||||
logger.debug("DeEarth instance created", { modsPath, movePath });
|
logger.debug("DeEarth instance created", { modsPath, movePath });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +76,7 @@ export class DeEarth {
|
|||||||
logger.info("DeEarth process completed");
|
logger.info("DeEarth process completed");
|
||||||
}
|
}
|
||||||
|
|
||||||
private async identifyClientSideMods(): Promise<string[]> {
|
private async identifyClientSideMods(): Promise<string[]> { // 识别客户端Mod主函数
|
||||||
const clientMods: string[] = [];
|
const clientMods: string[] = [];
|
||||||
|
|
||||||
if (config.filter.hashes) {
|
if (config.filter.hashes) {
|
||||||
@@ -69,11 +89,67 @@ export class DeEarth {
|
|||||||
clientMods.push(...await this.checkMixinsForClientMods());
|
clientMods.push(...await this.checkMixinsForClientMods());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (config.filter.dexpub) {
|
||||||
|
logger.info("Starting dexpub check for client-side mods");
|
||||||
|
const dexpubMods = await this.checkDexpubForClientMods();
|
||||||
|
clientMods.push(...dexpubMods.clientMods);
|
||||||
|
const serverModsListSet = new Set(dexpubMods.serverMods);
|
||||||
|
for(let i=0;i>=clientMods.length - 1;i--){
|
||||||
|
if (serverModsListSet.has(clientMods[i])){
|
||||||
|
clientMods.splice(i,1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Dexpub check completed", { serverMods: dexpubMods.serverMods, clientMods: dexpubMods.clientMods });
|
||||||
|
}
|
||||||
|
|
||||||
const uniqueMods = [...new Set(clientMods)];
|
const uniqueMods = [...new Set(clientMods)];
|
||||||
logger.info("Client-side mods identified", { count: uniqueMods.length, mods: uniqueMods });
|
logger.info("Client-side mods identified", { count: uniqueMods.length, mods: uniqueMods });
|
||||||
return uniqueMods;
|
return uniqueMods;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async checkDexpubForClientMods(): Promise<checkDexpubForClientMods> {
|
||||||
|
const clientMods: string[] = [];
|
||||||
|
const serverMods: string[] = [];
|
||||||
|
const modIds: string[] = [];
|
||||||
|
const map: Map<string, string> = new Map();
|
||||||
|
for (const file of this.files) {
|
||||||
|
for (const info of file.infos) {
|
||||||
|
const config = JSON.parse(info.data);
|
||||||
|
const keys = Object.keys(config);
|
||||||
|
if (keys.includes("id")) {
|
||||||
|
modIds.push(config.id);
|
||||||
|
map.set(config.id, file.filename);
|
||||||
|
}else if(keys.includes("mods")){
|
||||||
|
modIds.push(config.mods[0].modId);
|
||||||
|
map.set(config.mods[0].modId, file.filename);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const modids = modIds;
|
||||||
|
const modIdToIsTypeMod = await this.got.post(`api/mod/check`,{
|
||||||
|
json: {
|
||||||
|
modids,
|
||||||
|
}
|
||||||
|
}).json<{[modId: string]: boolean}>()
|
||||||
|
const modIdToIsTypeModKeys = Object.keys(modIdToIsTypeMod);
|
||||||
|
for(const modId of modIdToIsTypeModKeys){
|
||||||
|
if(modIdToIsTypeMod[modId]){
|
||||||
|
const MapData = map.get(modId);
|
||||||
|
if(MapData){
|
||||||
|
clientMods.push(MapData);
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
const MapData = map.get(modId);
|
||||||
|
if(MapData){
|
||||||
|
serverMods.push(MapData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.info("Galaxy check client-side mods", { count: clientMods.length, mods: clientMods });
|
||||||
|
return { serverMods, clientMods };
|
||||||
|
}
|
||||||
|
|
||||||
private async checkHashesForClientMods(): Promise<string[]> {
|
private async checkHashesForClientMods(): Promise<string[]> {
|
||||||
const hashToFilename = new Map<string, string>();
|
const hashToFilename = new Map<string, string>();
|
||||||
const hashes = this.files.map(file => {
|
const hashes = this.files.map(file => {
|
||||||
@@ -168,11 +244,13 @@ export class DeEarth {
|
|||||||
try {
|
try {
|
||||||
const fileData = fs.readFileSync(fullPath);
|
const fileData = fs.readFileSync(fullPath);
|
||||||
const mixins = await this.extractMixins(fileData);
|
const mixins = await this.extractMixins(fileData);
|
||||||
|
const infos = await this.extractModInfo(fileData);
|
||||||
|
|
||||||
this.files.push({
|
this.files.push({
|
||||||
filename: fullPath,
|
filename: fullPath,
|
||||||
hash: crypto.createHash('sha1').update(fileData).digest('hex'),
|
hash: crypto.createHash('sha1').update(fileData).digest('hex'),
|
||||||
mixins
|
mixins,
|
||||||
|
infos,
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.debug("File processed", { filename: fullPath, mixinCount: mixins.length });
|
logger.debug("File processed", { filename: fullPath, mixinCount: mixins.length });
|
||||||
@@ -189,6 +267,27 @@ export class DeEarth {
|
|||||||
return fs.readdirSync(this.modsPath).filter(f => f.endsWith(".jar"));
|
return fs.readdirSync(this.modsPath).filter(f => f.endsWith(".jar"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async extractModInfo(jarData: Buffer): Promise<IInfoFile[]> {
|
||||||
|
const infos: IInfoFile[] = [];
|
||||||
|
const zipEntries = Azip(jarData);
|
||||||
|
await Promise.all(zipEntries.map(async (entry) => {
|
||||||
|
try {
|
||||||
|
if (entry.entryName.endsWith("neoforge.mods.toml") || entry.entryName.endsWith("mods.toml")) {
|
||||||
|
const data = await entry.getData();
|
||||||
|
infos.push({ name: entry.entryName, data: JSON.stringify(toml.parse(data.toString())) });
|
||||||
|
} else if (entry.entryName.endsWith("fabric.mod.json")) {
|
||||||
|
const data = await entry.getData();
|
||||||
|
infos.push({ name: entry.entryName, data: data.toString() });
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error(`Error extracting ${entry.entryName}`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return infos;
|
||||||
|
}
|
||||||
|
|
||||||
private async extractMixins(jarData: Buffer): Promise<IMixinFile[]> {
|
private async extractMixins(jarData: Buffer): Promise<IMixinFile[]> {
|
||||||
const mixins: IMixinFile[] = [];
|
const mixins: IMixinFile[] = [];
|
||||||
const zipEntries = Azip(jarData);
|
const zipEntries = Azip(jarData);
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ const DEFAULT_CONFIG: IConfig = {
|
|||||||
},
|
},
|
||||||
filter: {
|
filter: {
|
||||||
hashes: true,
|
hashes: true,
|
||||||
dexpub: false,
|
dexpub: true,
|
||||||
mixins: true,
|
mixins: true,
|
||||||
},
|
},
|
||||||
oaf: true
|
oaf: true
|
||||||
|
|||||||
@@ -1,9 +1,73 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
const env = process.env.DEBUG;
|
const env = process.env.DEBUG;
|
||||||
const isDebug = env === "true";
|
const isDebug = env === "true";
|
||||||
|
|
||||||
// 日志级别枚举
|
// 日志级别枚举
|
||||||
type LogLevel = "debug" | "info" | "warn" | "error";
|
type LogLevel = "debug" | "info" | "warn" | "error";
|
||||||
|
|
||||||
|
// 日志文件路径
|
||||||
|
const logsDir = path.join(process.cwd(), 'logs');
|
||||||
|
let logFilePath: string | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取Asia/Shanghai时区的格式化时间
|
||||||
|
*/
|
||||||
|
function getShanghaiTime(): Date {
|
||||||
|
const now = new Date();
|
||||||
|
const utc = now.getTime() + (now.getTimezoneOffset() * 60000);
|
||||||
|
const shanghaiOffset = 8;
|
||||||
|
return new Date(utc + (3600000 * shanghaiOffset));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化时间为字符串
|
||||||
|
*/
|
||||||
|
function formatTimestamp(date: Date): string {
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
|
const hours = String(date.getHours()).padStart(2, '0');
|
||||||
|
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||||
|
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||||||
|
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成日志文件名,处理同一天多次打开的情况
|
||||||
|
*/
|
||||||
|
function generateLogFileName(): string {
|
||||||
|
const now = getShanghaiTime();
|
||||||
|
const dateStr = now.toISOString().split('T')[0];
|
||||||
|
const timeStr = String(now.getHours()).padStart(2, '0') +
|
||||||
|
String(now.getMinutes()).padStart(2, '0') +
|
||||||
|
String(now.getSeconds()).padStart(2, '0');
|
||||||
|
return `${dateStr}_${timeStr}.log`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化日志文件
|
||||||
|
*/
|
||||||
|
function initLogFile() {
|
||||||
|
if (!fs.existsSync(logsDir)) {
|
||||||
|
fs.mkdirSync(logsDir, { recursive: true });
|
||||||
|
}
|
||||||
|
logFilePath = path.join(logsDir, generateLogFileName());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 写入日志到文件
|
||||||
|
*/
|
||||||
|
function writeToFile(logMessage: string) {
|
||||||
|
if (!logFilePath) {
|
||||||
|
initLogFile();
|
||||||
|
}
|
||||||
|
if (logFilePath) {
|
||||||
|
fs.appendFileSync(logFilePath, logMessage + '\n', 'utf-8');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 日志记录器
|
* 日志记录器
|
||||||
* @param level 日志级别
|
* @param level 日志级别
|
||||||
@@ -11,24 +75,42 @@ type LogLevel = "debug" | "info" | "warn" | "error";
|
|||||||
* @param data 附加数据(可选)
|
* @param data 附加数据(可选)
|
||||||
*/
|
*/
|
||||||
function log(level: LogLevel, msg: string | Error, data?: any) {
|
function log(level: LogLevel, msg: string | Error, data?: any) {
|
||||||
const timestamp = new Date().toLocaleString();
|
const shanghaiTime = getShanghaiTime();
|
||||||
|
const timestamp = formatTimestamp(shanghaiTime);
|
||||||
const prefix = `[${level.toUpperCase()}] [${timestamp}]`;
|
const prefix = `[${level.toUpperCase()}] [${timestamp}]`;
|
||||||
|
|
||||||
// 确保只在调试模式下输出debug日志
|
// 确保只在调试模式下输出debug日志
|
||||||
if (level === "debug" && !isDebug) return;
|
if (level === "debug" && !isDebug) return;
|
||||||
|
|
||||||
|
let logMessage = '';
|
||||||
|
|
||||||
if (msg instanceof Error) {
|
if (msg instanceof Error) {
|
||||||
console.error(`${prefix} ${msg.message}`);
|
const errorMsg = `${prefix} ${msg.message}`;
|
||||||
console.error(msg.stack);
|
const stackMsg = msg.stack || '';
|
||||||
|
const dataMsg = data ? `${prefix} Data: ${JSON.stringify(data, null, 2)}` : '';
|
||||||
|
|
||||||
|
console.error(errorMsg);
|
||||||
|
console.error(stackMsg);
|
||||||
if (data) console.error(`${prefix} Data:`, data);
|
if (data) console.error(`${prefix} Data:`, data);
|
||||||
|
|
||||||
|
logMessage = errorMsg + '\n' + stackMsg;
|
||||||
|
if (data) logMessage += '\n' + dataMsg;
|
||||||
} else {
|
} else {
|
||||||
const logFunc = level === "error" ? console.error :
|
const logFunc = level === "error" ? console.error :
|
||||||
level === "warn" ? console.warn :
|
level === "warn" ? console.warn :
|
||||||
console.log;
|
console.log;
|
||||||
|
|
||||||
logFunc(`${prefix} ${msg}`);
|
const outputMsg = `${prefix} ${msg}`;
|
||||||
|
const dataMsg = data ? `${prefix} Data: ${JSON.stringify(data, null, 2)}` : '';
|
||||||
|
|
||||||
|
logFunc(outputMsg);
|
||||||
if (data) logFunc(`${prefix} Data:`, data);
|
if (data) logFunc(`${prefix} Data:`, data);
|
||||||
|
|
||||||
|
logMessage = outputMsg;
|
||||||
|
if (data) logMessage += '\n' + dataMsg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
writeToFile(logMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const logger = {
|
export const logger = {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import pRetry from "p-retry";
|
|||||||
import fs from "node:fs";
|
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, spawn} from "node:child_process";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Java版本信息接口
|
* Java版本信息接口
|
||||||
@@ -135,20 +135,43 @@ export async function checkJava(): Promise<JavaCheckResult> {
|
|||||||
|
|
||||||
export function execPromise(cmd:string,options?:ExecOptions){
|
export function execPromise(cmd:string,options?:ExecOptions){
|
||||||
logger.debug(`Executing command: ${cmd}`);
|
logger.debug(`Executing command: ${cmd}`);
|
||||||
return new Promise((resolve,reject)=>{
|
return new Promise<number>((resolve,reject)=>{
|
||||||
exec(cmd,options,(err,stdout,stderr)=>{
|
const args = cmd.split(' ');
|
||||||
if(err){
|
const command = args.shift() || '';
|
||||||
logger.error(`Command execution failed: ${cmd}`, err);
|
|
||||||
|
const child = spawn(command, args, {
|
||||||
|
...options,
|
||||||
|
shell: true
|
||||||
|
});
|
||||||
|
|
||||||
|
let stdout = '';
|
||||||
|
let stderr = '';
|
||||||
|
|
||||||
|
child.stdout?.on('data', (data) => {
|
||||||
|
stdout += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
child.stderr?.on('data', (data) => {
|
||||||
|
stderr += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('close', (code) => {
|
||||||
|
if (code !== 0) {
|
||||||
|
logger.error(`Command execution failed: ${cmd}`);
|
||||||
logger.debug(`Stderr: ${stderr}`);
|
logger.debug(`Stderr: ${stderr}`);
|
||||||
reject(err)
|
reject(new Error(`Command failed with exit code ${code}`));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (stdout) logger.debug(`Command stdout: ${stdout}`);
|
if (stdout) logger.debug(`Command stdout: ${stdout}`);
|
||||||
if (stderr) logger.debug(`Command stderr: ${stderr}`);
|
if (stderr) logger.debug(`Command stderr: ${stderr}`);
|
||||||
}).on('exit',(code)=>{
|
|
||||||
logger.debug(`Command completed with exit code: ${code}`);
|
logger.debug(`Command completed with exit code: ${code}`);
|
||||||
resolve(code)
|
resolve(code || 0);
|
||||||
})
|
});
|
||||||
|
|
||||||
|
child.on('error', (err) => {
|
||||||
|
logger.error(`Command execution error: ${cmd}`, err);
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,7 +216,7 @@ export async function fastdownload(data: [string, string]|string[][]) {
|
|||||||
|
|
||||||
export async function Wfastdownload(data: string[][], ws: MessageWS) {
|
export async function Wfastdownload(data: string[][], ws: MessageWS) {
|
||||||
logger.info(`Starting web download of ${data.length} files`);
|
logger.info(`Starting web download of ${data.length} files`);
|
||||||
|
let index = 0;
|
||||||
return await pMap(
|
return await pMap(
|
||||||
data,
|
data,
|
||||||
async (item: string[], idx: number) => {
|
async (item: string[], idx: number) => {
|
||||||
@@ -214,7 +237,7 @@ export async function Wfastdownload(data: string[][], ws: MessageWS) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 更新下载进度
|
// 更新下载进度
|
||||||
ws.download(data.length, idx + 1, filePath);
|
ws.download(data.length, ++index, filePath);
|
||||||
},
|
},
|
||||||
{ retries: 3, onFailedAttempt: (error) => {
|
{ retries: 3, onFailedAttempt: (error) => {
|
||||||
logger.warn(`Download attempt failed for ${url}, retrying (${error.attemptNumber}/3)`);
|
logger.warn(`Download attempt failed for ${url}, retrying (${error.attemptNumber}/3)`);
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 80 KiB |
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { h, ref } from 'vue';
|
import { h, provide, ref } from 'vue';
|
||||||
import { MenuProps, message } from 'ant-design-vue';
|
import { MenuProps, message } from 'ant-design-vue';
|
||||||
import { SettingOutlined, UserOutlined, WindowsOutlined } from '@ant-design/icons-vue';
|
import { SettingOutlined, UploadOutlined, UserOutlined, WindowsOutlined } from '@ant-design/icons-vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import * as shell from '@tauri-apps/plugin-shell';
|
import * as shell from '@tauri-apps/plugin-shell';
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
@@ -24,22 +24,35 @@ document.oncontextmenu = (event: any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
let killCoreProcess: (() => void) | null = null;
|
||||||
// 启动后端核心服务
|
// 启动后端核心服务
|
||||||
message.loading("DeEarthX.Core启动中,请勿操作...").then(() => {
|
message.loading("DeEarthX.Core启动中,请勿操作...").then(() => runCoreProcess());
|
||||||
|
|
||||||
|
function runCoreProcess() {
|
||||||
shell.Command.create("core").spawn()
|
shell.Command.create("core").spawn()
|
||||||
.then(() => {
|
.then((e) => {
|
||||||
// 检查后端服务是否成功启动
|
// 检查后端服务是否成功启动
|
||||||
fetch("http://localhost:37019/", { method: "GET" })
|
fetch("http://localhost:37019/", { method: "GET" })
|
||||||
.catch(() => router.push('/error'))
|
.catch(() => router.push('/error'))
|
||||||
.then(() => message.success("DeEarthX.Core 启动成功"));
|
.then(() => message.success("DeEarthX.Core 启动成功"));
|
||||||
console.log("DeEarthX V3 Core");
|
console.log("DeEarthX V3 Core");
|
||||||
|
// 保存进程对象,用于后续终止
|
||||||
|
killCoreProcess = e.kill;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
message.error("DeEarthX.Core 启动失败,请检查37019端口是否被占用!");
|
message.error("DeEarthX.Core 启动失败,请检查37019端口是否被占用!");
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
|
provide("killCoreProcess", () => {
|
||||||
|
if (killCoreProcess && typeof killCoreProcess === 'function') {
|
||||||
|
killCoreProcess();
|
||||||
|
killCoreProcess = null;
|
||||||
|
message.info("DeEarthX.Core 重新启动!");
|
||||||
|
runCoreProcess();
|
||||||
|
}
|
||||||
|
}); //全局提供kill方法
|
||||||
|
|
||||||
// 导航菜单配置
|
// 导航菜单配置
|
||||||
const selectedKeys = ref<(string | number)[]>(['main']);
|
const selectedKeys = ref<(string | number)[]>(['main']);
|
||||||
@@ -56,6 +69,12 @@ const menuItems: MenuProps['items'] = [
|
|||||||
label: '设置',
|
label: '设置',
|
||||||
title: '设置',
|
title: '设置',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'galaxy',
|
||||||
|
icon: h(UploadOutlined),
|
||||||
|
label: '提交',
|
||||||
|
title: '提交',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'about',
|
key: 'about',
|
||||||
icon: h(UserOutlined),
|
icon: h(UserOutlined),
|
||||||
@@ -70,7 +89,8 @@ const handleMenuClick: MenuProps['onClick'] = (e) => {
|
|||||||
const routeMap: Record<string, string> = {
|
const routeMap: Record<string, string> = {
|
||||||
main: '/',
|
main: '/',
|
||||||
setting: '/setting',
|
setting: '/setting',
|
||||||
about: '/about'
|
about: '/about',
|
||||||
|
galaxy: '/galaxy'
|
||||||
};
|
};
|
||||||
const route = routeMap[e.key] || '/';
|
const route = routeMap[e.key] || '/';
|
||||||
router.push(route);
|
router.push(route);
|
||||||
@@ -88,7 +108,7 @@ const theme = ref({
|
|||||||
<a-config-provider :theme="theme">
|
<a-config-provider :theme="theme">
|
||||||
<div class="tw:h-screen tw:w-screen tw:flex tw:flex-col">
|
<div class="tw:h-screen tw:w-screen tw:flex tw:flex-col">
|
||||||
<a-page-header class="tw:h-16" style="border: 1px solid rgb(235, 237, 240)" title="DeEarthX"
|
<a-page-header class="tw:h-16" style="border: 1px solid rgb(235, 237, 240)" title="DeEarthX"
|
||||||
sub-title="V3" :avatar="{ src: './public/dex.png' }">
|
sub-title="V3" :avatar="{ src: './dex.png' }">
|
||||||
<template #extra>
|
<template #extra>
|
||||||
<a-button @click="openAuthorBilibili">作者B站</a-button>
|
<a-button @click="openAuthorBilibili">作者B站</a-button>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 32 KiB |
@@ -6,7 +6,7 @@ const sponsors = [
|
|||||||
{
|
{
|
||||||
id: "elfidc",
|
id: "elfidc",
|
||||||
name: "亿讯云",
|
name: "亿讯云",
|
||||||
imageUrl: "../src/assets/elfidc.svg",
|
imageUrl: "./elfidc.svg",
|
||||||
type: "金牌赞助",
|
type: "金牌赞助",
|
||||||
url: "https://www.elfidc.com"
|
url: "https://www.elfidc.com"
|
||||||
}
|
}
|
||||||
@@ -17,18 +17,18 @@ const thanksList = [
|
|||||||
{
|
{
|
||||||
id: "user",
|
id: "user",
|
||||||
name: "天跑",
|
name: "天跑",
|
||||||
avatar: "../src/assets/tianpao.jpg",
|
avatar: "./tianpao.jpg",
|
||||||
contribution: "作者"
|
contribution: "作者"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "mirror",
|
id: "mirror",
|
||||||
name: "bangbang93",
|
name: "bangbang93",
|
||||||
avatar: "../src/assets/bb93.jpg",
|
avatar: "./bb93.jpg",
|
||||||
contribution: "BMCLAPI镜像"
|
contribution: "BMCLAPI镜像"
|
||||||
},{
|
},{
|
||||||
id: "mirror",
|
id: "mirror",
|
||||||
name: "z0z0r4",
|
name: "z0z0r4",
|
||||||
avatar: "../src/assets/z0z0r4.jpg",
|
avatar: "./z0z0r4.jpg",
|
||||||
contribution: "MCIM镜像"
|
contribution: "MCIM镜像"
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
235
front/src/component/Galaxy.vue
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
<template>
|
||||||
|
<div class="tw:h-full tw:w-full tw:p-4 tw:overflow-auto tw:bg-gray-50">
|
||||||
|
<div class="tw:max-w-2xl tw:mx-auto">
|
||||||
|
<!-- 标题区域 -->
|
||||||
|
<div class="tw:text-center tw:mb-8">
|
||||||
|
<h1 class="tw:text-2xl tw:font-bold tw:tracking-tight">
|
||||||
|
<span
|
||||||
|
class="tw:bg-gradient-to-r tw:from-cyan-300 tw:to-purple-950 tw:bg-clip-text tw:text-transparent">
|
||||||
|
Galaxy Square
|
||||||
|
</span>
|
||||||
|
</h1>
|
||||||
|
<p class="tw:text-gray-500 tw:mt-2">让所有的模组都在这里发光</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 模组提交 -->
|
||||||
|
<div class="tw:bg-white tw:rounded-lg tw:shadow-sm tw:p-6 tw:mb-6">
|
||||||
|
<h2 class="tw:text-lg tw:font-semibold tw:text-gray-800 tw:mb-4 tw:flex tw:items-center tw:gap-2">
|
||||||
|
<span class="tw:w-2 tw:h-2 tw:bg-purple-500 tw:rounded-full"></span>
|
||||||
|
模组提交
|
||||||
|
</h2>
|
||||||
|
<div class="tw:flex tw:flex-col tw:gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="tw:block tw:text-sm tw:font-medium tw:text-gray-700 tw:mb-2">
|
||||||
|
模组类型
|
||||||
|
</label>
|
||||||
|
<a-radio-group v-model:value="modType" size="default" button-style="solid">
|
||||||
|
<a-radio-button value="client">客户端模组</a-radio-button>
|
||||||
|
<a-radio-button value="server">服务端模组</a-radio-button>
|
||||||
|
</a-radio-group>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="tw:block tw:text-sm tw:font-medium tw:text-gray-700 tw:mb-2">
|
||||||
|
Modid
|
||||||
|
</label>
|
||||||
|
<a-input
|
||||||
|
v-model:value="modidInput"
|
||||||
|
placeholder="请输入 Modid(多个用逗号分隔)或上传文件自动获取"
|
||||||
|
size="large"
|
||||||
|
allow-clear
|
||||||
|
/>
|
||||||
|
<p class="tw:text-xs tw:text-gray-400 tw:mt-1">
|
||||||
|
当前已添加 {{ modidList.length }} 个 Modid
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="tw:block tw:text-sm tw:font-medium tw:text-gray-700 tw:mb-2">
|
||||||
|
上传文件
|
||||||
|
</label>
|
||||||
|
<a-upload-dragger
|
||||||
|
:fileList="fileList"
|
||||||
|
:before-upload="beforeUpload"
|
||||||
|
@remove="handleRemove"
|
||||||
|
accept=".jar"
|
||||||
|
multiple
|
||||||
|
>
|
||||||
|
<p class="tw-ant-upload-drag-icon">
|
||||||
|
<InboxOutlined />
|
||||||
|
</p>
|
||||||
|
<p class="tw-ant-upload-text">点击或拖拽文件到此区域上传</p>
|
||||||
|
<p class="tw-ant-upload-hint">
|
||||||
|
支持 .jar 格式文件,可多选
|
||||||
|
</p>
|
||||||
|
</a-upload-dragger>
|
||||||
|
|
||||||
|
<div v-if="fileList.length > 0" class="tw:mt-4">
|
||||||
|
<p class="tw:text-sm tw:font-medium tw:text-gray-700 tw:mb-2">
|
||||||
|
已选择 {{ fileList.length }} 个文件
|
||||||
|
</p>
|
||||||
|
<a-button
|
||||||
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
:loading="uploading"
|
||||||
|
block
|
||||||
|
@click="handleUpload"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<UploadOutlined />
|
||||||
|
</template>
|
||||||
|
{{ uploading ? '上传中...' : '开始上传' }}
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="modidList.length > 0" class="tw:mt-2">
|
||||||
|
<a-button
|
||||||
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
:loading="submitting"
|
||||||
|
block
|
||||||
|
@click="handleSubmit"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<SendOutlined />
|
||||||
|
</template>
|
||||||
|
{{ submitting ? '提交中...' : `提交${modType === 'client' ? '客户端' : '服务端'}模组` }}
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import { UploadOutlined, InboxOutlined, SendOutlined } from '@ant-design/icons-vue';
|
||||||
|
import { message, Modal } from 'ant-design-vue';
|
||||||
|
import type { UploadFile, UploadProps } from 'ant-design-vue';
|
||||||
|
|
||||||
|
const modType = ref<'client' | 'server'>('client');
|
||||||
|
const modidList = ref<string[]>([]);
|
||||||
|
const uploading = ref(false);
|
||||||
|
const submitting = ref(false);
|
||||||
|
const fileList = ref<UploadFile[]>([]);
|
||||||
|
|
||||||
|
const modidInput = computed({
|
||||||
|
get: () => modidList.value.join(','),
|
||||||
|
set: (value: string) => {
|
||||||
|
modidList.value = value
|
||||||
|
.split(',')
|
||||||
|
.map(id => id.trim())
|
||||||
|
.filter(id => id.length > 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const beforeUpload: UploadProps['beforeUpload'] = (file) => {
|
||||||
|
console.log(file.name);
|
||||||
|
const uploadFile: UploadFile = {
|
||||||
|
uid: `${Date.now()}-${Math.random()}`,
|
||||||
|
name: file.name,
|
||||||
|
status: 'done',
|
||||||
|
url: '',
|
||||||
|
originFileObj: file,
|
||||||
|
};
|
||||||
|
fileList.value = [...fileList.value, uploadFile];
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemove: UploadProps['onRemove'] = (file) => {
|
||||||
|
const index = fileList.value.indexOf(file);
|
||||||
|
const newFileList = fileList.value.slice();
|
||||||
|
newFileList.splice(index, 1);
|
||||||
|
fileList.value = newFileList;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpload = async () => {
|
||||||
|
if (fileList.value.length === 0) {
|
||||||
|
message.warning('请先选择文件');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uploading.value = true;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
fileList.value.forEach((file) => {
|
||||||
|
if (file.originFileObj) {
|
||||||
|
const blob = file.originFileObj;
|
||||||
|
const encodedFileName = encodeURIComponent(file.name);
|
||||||
|
const fileWithCorrectName = new File([blob], encodedFileName, { type: blob.type });
|
||||||
|
formData.append('files', fileWithCorrectName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('http://localhost:37019/galaxy/upload', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
console.log(data);
|
||||||
|
if (data.modids && Array.isArray(data.modids)) {
|
||||||
|
let addedCount = 0;
|
||||||
|
data.modids.forEach((modid: string) => {
|
||||||
|
if (modid && !modidList.value.includes(modid)) {
|
||||||
|
modidList.value.push(modid);
|
||||||
|
addedCount++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
message.success(`成功上传 ${addedCount} 个文件`);
|
||||||
|
} else {
|
||||||
|
message.error('返回数据格式错误');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
message.error('上传失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error('上传出错,请重试');
|
||||||
|
} finally {
|
||||||
|
uploading.value = false;
|
||||||
|
fileList.value = [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '确认提交',
|
||||||
|
content: `确定要提交 ${modidList.value.length} 个${modType.value === 'client' ? '客户端' : '服务端'}模组吗?`,
|
||||||
|
okText: '确定',
|
||||||
|
cancelText: '取消',
|
||||||
|
onOk: async () => {
|
||||||
|
submitting.value = true;
|
||||||
|
try {
|
||||||
|
const apiUrl = modType.value === 'client'
|
||||||
|
? 'http://localhost:37019/galaxy/submit/client'
|
||||||
|
: 'http://localhost:37019/galaxy/submit/server';
|
||||||
|
|
||||||
|
const response = await fetch(apiUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
modids: modidList.value,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
message.success(`${modType.value === 'client' ? '客户端' : '服务端'}模组提交成功`);
|
||||||
|
modidList.value = [];
|
||||||
|
} else {
|
||||||
|
message.error('提交失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error('提交出错,请重试');
|
||||||
|
} finally {
|
||||||
|
submitting.value = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, watch } from 'vue';
|
import { inject, ref, watch } from 'vue';
|
||||||
import { InboxOutlined } from '@ant-design/icons-vue';
|
import { InboxOutlined } from '@ant-design/icons-vue';
|
||||||
import { message, notification, StepsProps } from 'ant-design-vue';
|
import { message, notification, StepsProps } from 'ant-design-vue';
|
||||||
import type { UploadFile, UploadChangeParam } from 'ant-design-vue';
|
import type { UploadFile, UploadChangeParam } from 'ant-design-vue';
|
||||||
@@ -55,6 +55,10 @@ function resetState() {
|
|||||||
currentStep.value = 0;
|
currentStep.value = 0;
|
||||||
unzipProgress.value = { status: 'active', percent: 0, display: true };
|
unzipProgress.value = { status: 'active', percent: 0, display: true };
|
||||||
downloadProgress.value = { status: 'active', percent: 0, display: true };
|
downloadProgress.value = { status: 'active', percent: 0, display: true };
|
||||||
|
const killCoreProcess = inject("killCoreProcess");
|
||||||
|
if (killCoreProcess && typeof killCoreProcess === 'function') {
|
||||||
|
killCoreProcess();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 模式选择相关
|
// 模式选择相关
|
||||||
@@ -217,9 +221,9 @@ function handleStartProcess() {
|
|||||||
<h1 class="tw:text-4xl tw:text-center tw:animate-pulse">DeEarthX</h1>
|
<h1 class="tw:text-4xl tw:text-center tw:animate-pulse">DeEarthX</h1>
|
||||||
<h1 class="tw:text-sm tw:text-gray-500 tw:text-center">让开服变成随时随地的事情!</h1>
|
<h1 class="tw:text-sm tw:text-gray-500 tw:text-center">让开服变成随时随地的事情!</h1>
|
||||||
</div>
|
</div>
|
||||||
<a-upload-dragger :disabled="uploadDisabled" class="tw:w-full tw:max-w-md tw:h-48" name="file" action="/" :multiple="false"
|
<a-upload-dragger :disabled="uploadDisabled" class="tw:w-full tw:max-w-md tw:h-48" name="file"
|
||||||
:before-upload="beforeUpload" @change="handleFileChange" @drop="handleFileDrop" v-model:fileList="uploadedFiles"
|
action="/" :multiple="false" :before-upload="beforeUpload" @change="handleFileChange"
|
||||||
accept=".zip,.mrpack">
|
@drop="handleFileDrop" v-model:fileList="uploadedFiles" accept=".zip,.mrpack">
|
||||||
<p class="ant-upload-drag-icon">
|
<p class="ant-upload-drag-icon">
|
||||||
<inbox-outlined></inbox-outlined>
|
<inbox-outlined></inbox-outlined>
|
||||||
</p>
|
</p>
|
||||||
@@ -227,23 +231,24 @@ function handleStartProcess() {
|
|||||||
<p class="ant-upload-hint">
|
<p class="ant-upload-hint">
|
||||||
请使用.zip(CurseForge、MCBBS)和.mrpack(Modrinth)文件
|
请使用.zip(CurseForge、MCBBS)和.mrpack(Modrinth)文件
|
||||||
</p>
|
</p>
|
||||||
|
<p class="ant-upload-hint">
|
||||||
|
PCL导出的zip整合包请拖拽里面的modpack.mrpack至DeX
|
||||||
|
</p>
|
||||||
</a-upload-dragger>
|
</a-upload-dragger>
|
||||||
<a-select
|
<a-select ref="select" :options="modeOptions" :value="selectedMode"
|
||||||
ref="select"
|
style="width: 120px;margin-top: 32px" @select="handleModeSelect"></a-select>
|
||||||
:options="modeOptions"
|
<a-button :disabled="startButtonDisabled" type="primary" @click="handleStartProcess"
|
||||||
:value="selectedMode"
|
style="margin-top: 6px">
|
||||||
style="width: 120px;margin-top: 32px"
|
|
||||||
@select="handleModeSelect"
|
|
||||||
></a-select>
|
|
||||||
<a-button :disabled="startButtonDisabled" type="primary" @click="handleStartProcess" style="margin-top: 6px">
|
|
||||||
开始
|
开始
|
||||||
</a-button>
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="showSteps" class="tw:fixed tw:bottom-2 tw:left-1/2 tw:-translate-x-1/2 tw:w-full tw:max-w-3xl tw:h-16 tw:flex tw:justify-center tw:items-center tw:text-sm">
|
<div v-if="showSteps"
|
||||||
|
class="tw:fixed tw:bottom-2 tw:left-1/2 tw:-translate-x-1/2 tw:w-full tw:max-w-3xl tw:h-16 tw:flex tw:justify-center tw:items-center tw:text-sm">
|
||||||
<a-steps :current="currentStep" :items="stepItems" />
|
<a-steps :current="currentStep" :items="stepItems" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="showSteps" ref="logContainer" class="tw:absolute tw:right-2 tw:bottom-20 tw:h-80 tw:w-64 tw:rounded-xl tw:overflow-y-auto">
|
<div v-if="showSteps" ref="logContainer"
|
||||||
|
class="tw:absolute tw:right-2 tw:bottom-20 tw:h-80 tw:w-64 tw:rounded-xl tw:overflow-y-auto">
|
||||||
<a-card title="制作进度" :bordered="true" class="tw:h-full">
|
<a-card title="制作进度" :bordered="true" class="tw:h-full">
|
||||||
<div v-if="unzipProgress.display">
|
<div v-if="unzipProgress.display">
|
||||||
<h1 class="tw:text-sm">解压进度</h1>
|
<h1 class="tw:text-sm">解压进度</h1>
|
||||||
|
|||||||
@@ -60,8 +60,8 @@ const settings: SettingCategory[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'dexpub',
|
key: 'dexpub',
|
||||||
name: 'DePIS过滤',
|
name: 'Galaxy Square 过滤',
|
||||||
description: '过滤 DeEarth Public Info Services 平台中记录的客户端文件',
|
description: '过滤 Galaxy Square 平台中记录的客户端文件',
|
||||||
path: 'filter.dexpub',
|
path: 'filter.dexpub',
|
||||||
defaultValue: false
|
defaultValue: false
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import Main from "../component/Main.vue";
|
|||||||
import Setting from "../component/Setting.vue";
|
import Setting from "../component/Setting.vue";
|
||||||
import About from "../component/About.vue";
|
import About from "../component/About.vue";
|
||||||
import Error from "../component/Error.vue";
|
import Error from "../component/Error.vue";
|
||||||
|
import Galaxy from "../component/Galaxy.vue";
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(),
|
history: createWebHistory(),
|
||||||
@@ -21,6 +22,9 @@ const router = createRouter({
|
|||||||
},{
|
},{
|
||||||
path: "/error",
|
path: "/error",
|
||||||
component: Error
|
component: Error
|
||||||
|
},{
|
||||||
|
path: "/galaxy",
|
||||||
|
component: Galaxy
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|||||||