diff --git a/.gitignore b/.gitignore index 3c6e517..1c54420 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,5 @@ core-x86_64-pc-windows-msvc.exe config.json *.exe *.jar -instance \ No newline at end of file +instance +*.mrpack \ No newline at end of file diff --git a/backend/package.json b/backend/package.json index cf46dd7..7f82083 100644 --- a/backend/package.json +++ b/backend/package.json @@ -7,7 +7,7 @@ "type": "module", "main": "main.js", "scripts": { - "test": "tsc&&node dist/main.js", + "test": "set \"DEBUG=true\"&&tsc&&node dist/main.js", "rollup": "rollup -c rollup.config.js", "nexe": "nexe -i ./dist/bundle.js --build -t x86-22.13.0 --output ./dist/core.exe", "build": "npm run rollup && npm run nexe" diff --git a/backend/src/Dex.ts b/backend/src/Dex.ts index 8d0cee1..39b835b 100644 --- a/backend/src/Dex.ts +++ b/backend/src/Dex.ts @@ -80,7 +80,7 @@ export class Dex { } private async _processModpack(buffer: Buffer, filename?: string): Promise { - if (!filename || (!filename.endsWith('.zip') && !filename.endsWith('.mrpack'))) { + if (!filename || !filename.endsWith('.zip')) { return buffer; } @@ -94,7 +94,7 @@ export class Dex { resolve(zipfile); }); }) as Promise); - + 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; diff --git a/backend/src/core.ts b/backend/src/core.ts index 4fb185e..c6191d6 100644 --- a/backend/src/core.ts +++ b/backend/src/core.ts @@ -7,6 +7,7 @@ import { Config, IConfig } from "./utils/config.js"; import { Dex } from "./Dex.js"; import { logger } from "./utils/logger.js"; import { checkJava, JavaCheckResult } from "./utils/utils.js"; +import { Galaxy } from "./galaxy.js"; export class Core { private config: IConfig; private readonly app: Application; @@ -16,16 +17,18 @@ export class Core { private readonly upload: multer.Multer; private task: {} = {}; dex: Dex; + galaxy: Galaxy; constructor(config: IConfig) { this.config = config this.app = express(); this.server = createServer(this.app); - this.upload = multer() this.ws = new WebSocketServer({ server: this.server }) this.ws.on("connection",(e)=>{ this.wsx = e }) this.dex = new Dex(this.ws) + this.galaxy = new Galaxy() + this.upload = multer(); } private async javachecker() { @@ -129,6 +132,8 @@ export class Core { res.status(500).json({ status: 500, message: "Failed to update config" }); } }); + + this.app.use("/galaxy", this.galaxy.getRouter()); } public async start() { diff --git a/backend/src/galaxy.ts b/backend/src/galaxy.ts new file mode 100644 index 0000000..8a16e00 --- /dev/null +++ b/backend/src/galaxy.ts @@ -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 + } +} diff --git a/backend/src/utils/DeEarth.ts b/backend/src/utils/DeEarth.ts index d6d7ca6..71dc066 100644 --- a/backend/src/utils/DeEarth.ts +++ b/backend/src/utils/DeEarth.ts @@ -1,20 +1,27 @@ import fs from "node:fs"; import crypto from "node:crypto"; import { Azip } from "./ziplib.js"; -import got from "got"; +import got, { Got } from "got"; import { Utils } from "./utils.js"; import config from "./config.js"; import { logger } from "./logger.js"; +import toml from "smol-toml"; interface IMixinFile { name: string; data: string; } +interface IInfoFile { + name: string; + data: string; +} + interface IFileInfo { filename: string; hash: string; mixins: IMixinFile[]; + infos: IInfoFile[]; } interface IHashResponse { @@ -27,23 +34,36 @@ interface IProjectInfo { server_side: string; } +interface checkDexpubForClientMods { + serverMods: string[]; + clientMods: string[]; +} + export class DeEarth { private movePath: string; private modsPath: string; private files: IFileInfo[]; private utils: Utils; + private got: Got; constructor(modsPath: string, movePath: string) { this.utils = new Utils(); this.movePath = movePath; this.modsPath = modsPath; this.files = []; + this.got = got.extend({ + prefixUrl: "https://galaxy.tianpao.top/", + headers: { + "User-Agent": "DeEarthX", + }, + responseType: "json", + }); logger.debug("DeEarth instance created", { modsPath, movePath }); } async Main(): Promise { logger.info("Starting DeEarth process"); - + if (!fs.existsSync(this.movePath)) { logger.debug("Creating target directory", { path: this.movePath }); fs.mkdirSync(this.movePath, { recursive: true }); @@ -52,11 +72,11 @@ export class DeEarth { await this.getFilesInfo(); const clientSideMods = await this.identifyClientSideMods(); await this.moveClientSideMods(clientSideMods); - + logger.info("DeEarth process completed"); } - private async identifyClientSideMods(): Promise { + private async identifyClientSideMods(): Promise { // 识别客户端Mod主函数 const clientMods: string[] = []; if (config.filter.hashes) { @@ -69,11 +89,67 @@ export class DeEarth { 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)]; logger.info("Client-side mods identified", { count: uniqueMods.length, mods: uniqueMods }); return uniqueMods; } + private async checkDexpubForClientMods(): Promise { + const clientMods: string[] = []; + const serverMods: string[] = []; + const modIds: string[] = []; + const map: Map = 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 { const hashToFilename = new Map(); const hashes = this.files.map(file => { @@ -164,17 +240,19 @@ export class DeEarth { for (const jarFilename of jarFiles) { const fullPath = `${this.modsPath}/${jarFilename}`; - + try { const fileData = fs.readFileSync(fullPath); const mixins = await this.extractMixins(fileData); - + const infos = await this.extractModInfo(fileData); + this.files.push({ filename: fullPath, hash: crypto.createHash('sha1').update(fileData).digest('hex'), - mixins + mixins, + infos, }); - + logger.debug("File processed", { filename: fullPath, mixinCount: mixins.length }); } catch (error: any) { logger.error("Error processing file", { filename: fullPath, error: error.message }); @@ -189,6 +267,27 @@ export class DeEarth { return fs.readdirSync(this.modsPath).filter(f => f.endsWith(".jar")); } + private async extractModInfo(jarData: Buffer): Promise { + 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 { const mixins: IMixinFile[] = []; const zipEntries = Azip(jarData); diff --git a/backend/src/utils/config.ts b/backend/src/utils/config.ts index 9d1a9db..0916f32 100644 --- a/backend/src/utils/config.ts +++ b/backend/src/utils/config.ts @@ -26,7 +26,7 @@ const DEFAULT_CONFIG: IConfig = { }, filter: { hashes: true, - dexpub: false, + dexpub: true, mixins: true, }, oaf: true diff --git a/front/src/component/Galaxy.vue b/front/src/component/Galaxy.vue index e3f3910..6a2ec70 100644 --- a/front/src/component/Galaxy.vue +++ b/front/src/component/Galaxy.vue @@ -11,9 +11,225 @@

让所有的模组都在这里发光

+ + +
+

+ + 模组提交 +

+
+
+ + + 客户端模组 + 服务端模组 + +
+ +
+ + +

+ 当前已添加 {{ modidList.length }} 个 Modid +

+
+ +
+ + +

+ +

+

点击或拖拽文件到此区域上传

+

+ 支持 .jar 格式文件,可多选 +

+
+ +
+

+ 已选择 {{ fileList.length }} 个文件 +

+ + + {{ uploading ? '上传中...' : '开始上传' }} + +
+
+ +
+ + + {{ submitting ? '提交中...' : `提交${modType === 'client' ? '客户端' : '服务端'}模组` }} + +
+
+
\ No newline at end of file +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([]); +const uploading = ref(false); +const submitting = ref(false); +const fileList = ref([]); + +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; + } + }, + }); +}; + diff --git a/front/src/component/Setting.vue b/front/src/component/Setting.vue index 6d95a02..ab446d5 100644 --- a/front/src/component/Setting.vue +++ b/front/src/component/Setting.vue @@ -60,8 +60,8 @@ const settings: SettingCategory[] = [ }, { key: 'dexpub', - name: 'DePIS过滤', - description: '过滤 DeEarth Public Info Services 平台中记录的客户端文件', + name: 'Galaxy Square 过滤', + description: '过滤 Galaxy Square 平台中记录的客户端文件', path: 'filter.dexpub', defaultValue: false },