Use BMCLAPI mirror & update build

Route Minecraft/Fabric/Forge downloads through BMCLAPI mirror and update installer logic. Added mirror rewrite utility (rewriteToMirror), mirror-aware downloadFile, and mirrorFetch; switched version manifest and metadata endpoints to bmclapi2.bangbang93.com and wired maven/assets hosts into xmcl/installer calls. Cleaned up compiled electron JS files and consolidated runtime files under electron-dist; updated electron-builder.yml (compression, asar, NSIS settings, license/installer assets and languages). Minor env/path changes (VITE_APP_ICON paths), added .npmrc (electron_mirror), new Koring.yml, pnpm-workspace and public installer assets. Also added/updated multiple frontend pages, store components, and TypeScript handlers to integrate the changes.
This commit is contained in:
2026-08-04 01:28:16 +08:00
parent 06269698a6
commit cb0afd99c0
73 changed files with 3108 additions and 2898 deletions
-180
View File
@@ -1,180 +0,0 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.microsoftLoginStart = microsoftLoginStart;
exports.microsoftLoginCallback = microsoftLoginCallback;
exports.offlineLogin = offlineLogin;
exports.validateMinecraftToken = validateMinecraftToken;
const crypto = __importStar(require("crypto"));
const electron_1 = __importDefault(require("electron"));
const { net } = electron_1.default;
const MC_SERVICES_API = 'https://api.minecraftservices.com';
const XBOX_AUTH_URL = 'https://user.auth.xboxlive.com/user/authenticate';
const XBOX_XSTS_URL = 'https://xsts.auth.xboxlive.com/xsts/authorize';
const MC_AUTH_URL = 'https://api.minecraftservices.com/authentication/login_with_xbox';
const MC_PROFILE_URL = 'https://api.minecraftservices.com/minecraft/profile';
function generateState() {
return crypto.randomBytes(16).toString('hex');
}
function generateVerifier() {
return crypto.randomBytes(32).toString('base64url');
}
function generateChallenge(verifier) {
return crypto.createHash('sha256').update(verifier).digest('base64url');
}
async function microsoftLoginStart(clientId, redirectUri) {
const state = generateState();
const verifier = generateVerifier();
const challenge = generateChallenge(verifier);
const params = new URLSearchParams({
client_id: clientId,
response_type: 'code',
redirect_uri: redirectUri || 'http://localhost:3000/callback',
scope: 'XboxLive.signin offline_access',
state,
code_challenge: challenge,
code_challenge_method: 'S256',
});
const authUrl = `https://login.live.com/oauth20_authorize.srf?${params.toString()}`;
return { state, authUrl, verifier };
}
async function exchangeCodeForToken(code, clientId, redirectUri, verifier) {
const response = await net.fetch('https://login.live.com/oauth20_token.srf', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
client_id: clientId,
code,
grant_type: 'authorization_code',
redirect_uri: redirectUri,
code_verifier: verifier,
}).toString(),
});
if (!response.ok)
throw new Error(`MS token exchange failed: ${response.status}`);
return response.json();
}
async function authenticateWithXbox(msToken) {
const response = await net.fetch(XBOX_AUTH_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
Properties: {
AuthMethod: 'RPS',
SiteName: 'user.auth.xboxlive.com',
RpsTicket: msToken,
},
RelyingParty: 'http://auth.xboxlive.com',
TokenType: 'JWT',
}),
});
if (!response.ok)
throw new Error(`Xbox auth failed: ${response.status}`);
return response.json();
}
async function authorizeWithXsts(xboxToken) {
const response = await net.fetch(XBOX_XSTS_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
Properties: {
SandboxId: 'RETAIL',
UserTokens: [xboxToken],
},
RelyingParty: 'rp://api.minecraftservices.com/',
TokenType: 'JWT',
}),
});
if (!response.ok)
throw new Error(`Xbox XSTS failed: ${response.status}`);
return response.json();
}
async function authenticateWithMinecraft(xstsToken) {
const response = await net.fetch(MC_AUTH_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
identityToken: `XBL3.0 x=${xstsToken}`,
}),
});
if (!response.ok)
throw new Error(`MC auth failed: ${response.status}`);
return response.json();
}
async function getMinecraftProfile(accessToken) {
const response = await net.fetch(MC_PROFILE_URL, {
headers: { Authorization: `Bearer ${accessToken}` },
});
if (!response.ok)
throw new Error(`MC profile failed: ${response.status}`);
return response.json();
}
async function microsoftLoginCallback(code, clientId, redirectUri) {
const redirect = redirectUri || 'http://localhost:3000/callback';
const msToken = await exchangeCodeForToken(code, clientId, redirect, '');
const xboxAuth = await authenticateWithXbox(msToken.access_token);
const xstsAuth = await authorizeWithXsts(xboxAuth.Token);
const mcAuth = await authenticateWithMinecraft(xstsAuth.Token);
const profile = await getMinecraftProfile(mcAuth.access_token);
return {
username: profile.name,
uuid: profile.id,
accessToken: mcAuth.access_token,
expiresAt: Date.now() + mcAuth.expires_in * 1000,
};
}
async function offlineLogin(username) {
const uuid = crypto.createHash('md5').update(`OfflinePlayer:${username}`).digest('hex');
const formattedUuid = `${uuid.slice(0, 8)}-${uuid.slice(8, 12)}-${uuid.slice(12, 16)}-${uuid.slice(16, 20)}-${uuid.slice(20)}`;
return {
username,
uuid: formattedUuid,
accessToken: '',
};
}
async function validateMinecraftToken(accessToken) {
try {
const response = await net.fetch(`${MC_SERVICES_API}/minecraft/profile`, {
headers: { Authorization: `Bearer ${accessToken}` },
});
return response.ok;
}
catch {
return false;
}
}
-93
View File
@@ -1,93 +0,0 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.writeCrashLog = writeCrashLog;
exports.readCrashLog = readCrashLog;
exports.clearCrashLog = clearCrashLog;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const electron_1 = __importDefault(require("electron"));
const { app } = electron_1.default;
const LOG_FILE = 'koring-crash.log';
const MAX_LINES = 1000;
function logPath() {
if (app.isPackaged) {
return path.join(path.dirname(app.getPath('exe')), LOG_FILE);
}
return path.join(__dirname, '..', LOG_FILE);
}
function trimFile(filePath) {
try {
if (!fs.existsSync(filePath))
return;
const lines = fs.readFileSync(filePath, 'utf-8').split('\n').filter(Boolean);
if (lines.length > MAX_LINES) {
fs.writeFileSync(filePath, lines.slice(-MAX_LINES).join('\n') + '\n', 'utf-8');
}
}
catch { }
}
function writeCrashLog(entry) {
const filePath = logPath();
try {
const line = JSON.stringify(entry) + '\n';
fs.appendFileSync(filePath, line, 'utf-8');
trimFile(filePath);
}
catch { }
}
function readCrashLog() {
const filePath = logPath();
try {
if (!fs.existsSync(filePath))
return '';
return fs.readFileSync(filePath, 'utf-8');
}
catch {
return '';
}
}
function clearCrashLog() {
const filePath = logPath();
try {
if (fs.existsSync(filePath)) {
fs.writeFileSync(filePath, '', 'utf-8');
}
}
catch { }
}
-181
View File
@@ -1,181 +0,0 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getVersionList = getVersionList;
exports.getForgeVersions = getForgeVersions;
exports.getFabricVersions = getFabricVersions;
exports.installMinecraft = installMinecraft;
exports.installModLoader = installModLoader;
const electron_1 = __importDefault(require("electron"));
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const https = __importStar(require("https"));
const http = __importStar(require("http"));
const { net } = electron_1.default;
const VERSION_MANIFEST_URL = 'https://launchermeta.mojang.com/mc/game/version_manifest.json';
const FORGE_VERSION_LIST_URL = 'https://files.minecraftforge.net/net/minecraftforge/forge/json';
const FABRIC_VERSION_LIST_URL = 'https://meta.fabricmc.net/v2/versions';
async function downloadFile(url, dest, onProgress) {
return new Promise((resolve, reject) => {
const client = url.startsWith('https') ? https : http;
const request = client.get(url, (response) => {
if (response.statusCode === 302 || response.statusCode === 301) {
downloadFile(response.headers.location, dest, onProgress).then(resolve).catch(reject);
return;
}
if (response.statusCode !== 200) {
reject(new Error(`Download failed: ${response.statusCode}`));
return;
}
const totalBytes = parseInt(response.headers['content-length'] || '0', 10);
let downloadedBytes = 0;
const dir = path.dirname(dest);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
const file = fs.createWriteStream(dest);
response.on('data', (chunk) => {
downloadedBytes += chunk.length;
if (onProgress && totalBytes > 0) {
onProgress({
stage: 'downloading',
current: downloadedBytes,
total: totalBytes,
message: `${Math.round((downloadedBytes / totalBytes) * 100)}%`,
});
}
});
response.pipe(file);
file.on('finish', () => {
file.close();
resolve();
});
file.on('error', (err) => {
fs.unlink(dest, () => { });
reject(err);
});
});
request.on('error', reject);
});
}
async function getVersionList(type) {
const response = await net.fetch(VERSION_MANIFEST_URL);
if (!response.ok)
throw new Error(`Failed to fetch version manifest: ${response.status}`);
const manifest = await response.json();
if (type && type !== 'all') {
manifest.versions = manifest.versions.filter((v) => v.type === type);
}
return manifest;
}
async function getForgeVersions(mcVersion) {
try {
const response = await net.fetch(FORGE_VERSION_LIST_URL);
if (!response.ok)
throw new Error(`Forge version list failed: ${response.status}`);
const data = await response.json();
const versions = data.versions[mcVersion || ''] || [];
return { versions };
}
catch {
return { versions: [] };
}
}
async function getFabricVersions(mcVersion) {
try {
const url = mcVersion
? `${FABRIC_VERSION_LIST_URL}/loader/${mcVersion}`
: `${FABRIC_VERSION_LIST_URL}/loader`;
const response = await net.fetch(url);
if (!response.ok)
throw new Error(`Fabric version list failed: ${response.status}`);
const data = await response.json();
return { versions: data.map((v) => v.version) };
}
catch {
return { versions: [] };
}
}
async function installMinecraft(version, gamePath, javaPath, downloadThreads, callbacks) {
const versionDir = path.join(gamePath, 'versions', version);
if (!fs.existsSync(versionDir)) {
fs.mkdirSync(versionDir, { recursive: true });
}
// Download version manifest
const manifestResponse = await net.fetch(VERSION_MANIFEST_URL);
const manifest = await manifestResponse.json();
const versionInfo = manifest.versions.find((v) => v.id === version);
if (!versionInfo)
throw new Error(`Version ${version} not found`);
callbacks?.onProgress?.({ stage: 'downloading', current: 0, total: 100, message: 'Downloading version manifest...' });
// Download version JSON
const versionJsonPath = path.join(versionDir, `${version}.json`);
await downloadFile(versionInfo.url, versionJsonPath, callbacks?.onProgress);
const versionJson = JSON.parse(fs.readFileSync(versionJsonPath, 'utf-8'));
// Download client jar
const clientJar = versionJson.downloads?.client;
if (clientJar) {
callbacks?.onProgress?.({ stage: 'downloading', current: 1, total: 3, message: 'Downloading client jar...' });
const jarPath = path.join(versionDir, `${version}.jar`);
await downloadFile(clientJar.url, jarPath, callbacks?.onProgress);
}
callbacks?.onProgress?.({ stage: 'downloading', current: 2, total: 3, message: 'Installing libraries...' });
// Download libraries
const libraries = versionJson.libraries || [];
for (const lib of libraries) {
if (lib.downloads?.artifact?.url) {
const libPath = path.join(gamePath, 'libraries', lib.downloads.artifact.path);
if (!fs.existsSync(libPath)) {
await downloadFile(lib.downloads.artifact.url, libPath);
}
}
}
callbacks?.onProgress?.({ stage: 'downloading', current: 3, total: 3, message: 'Installation complete' });
return { version, gamePath };
}
async function installModLoader(mcVersion, gamePath, loaderType, loaderVersion, javaPath, callbacks) {
callbacks?.onProgress?.({ stage: 'installing', current: 0, total: 100, message: `Installing ${loaderType}...` });
// Placeholder: actual implementation depends on loader type
// This would call the appropriate installer for Forge/Fabric/Quilt/NeoForge
callbacks?.onProgress?.({ stage: 'installing', current: 100, total: 100, message: `${loaderType} installed` });
return {
loaderType,
mcVersion,
loaderVersion: loaderVersion || 'latest',
};
}
+70 -11
View File
@@ -6,14 +6,63 @@ import * as http from 'http';
const { net } = electron;
const VERSION_MANIFEST_URL = 'https://launchermeta.mojang.com/mc/game/version_manifest.json';
const FORGE_VERSION_LIST_URL = 'https://files.minecraftforge.net/net/minecraftforge/forge/json';
const FABRIC_VERSION_LIST_URL = 'https://meta.fabricmc.net/v2/versions';
// ==================== BMCLAPI 镜像源配置 ====================
// 使用 BMCLAPIbmclapi2.bangbang93.com)镜像加速国内资源下载
// 版本清单:launchermeta.mojang.com → bmclapi2.bangbang93.com
// Fabric 元数据:meta.fabricmc.net → bmclapi2.bangbang93.com/fabric-meta
const VERSION_MANIFEST_URL = 'https://bmclapi2.bangbang93.com/mc/game/version_manifest.json';
const FABRIC_VERSION_LIST_URL = 'https://bmclapi2.bangbang93.com/fabric-meta/v2/versions';
// BMCLAPI 域名替换规则(按顺序匹配,带路径前缀的规则优先)
// 版本 JSON/客户端:launchermeta|launcher|piston-meta|piston-data.mojang.com → bmclapi2.bangbang93.com
// Assetsresources.download.minecraft.net → bmclapi2.bangbang93.com/assets
// Librarieslibraries.minecraft.net → bmclapi2.bangbang93.com/maven
// Forgefiles.minecraftforge.net/maven → bmclapi2.bangbang93.com/maven
// NeoForgemaven.neoforged.net/releases → bmclapi2.bangbang93.com/maven
// Fabricmeta.fabricmc.net → bmclapi2.bangbang93.com/fabric-metamaven.fabricmc.net → bmclapi2.bangbang93.com/maven
const MIRROR_RULES: [string, string][] = [
['files.minecraftforge.net/maven', 'bmclapi2.bangbang93.com/maven'],
['maven.neoforged.net/releases', 'bmclapi2.bangbang93.com/maven'],
['launchermeta.mojang.com', 'bmclapi2.bangbang93.com'],
['launcher.mojang.com', 'bmclapi2.bangbang93.com'],
['piston-meta.mojang.com', 'bmclapi2.bangbang93.com'],
['piston-data.mojang.com', 'bmclapi2.bangbang93.com'],
['resources.download.minecraft.net', 'bmclapi2.bangbang93.com/assets'],
['libraries.minecraft.net', 'bmclapi2.bangbang93.com/maven'],
['maven.minecraftforge.net', 'bmclapi2.bangbang93.com/maven'],
['maven.neoforged.net', 'bmclapi2.bangbang93.com/maven'],
['meta.fabricmc.net', 'bmclapi2.bangbang93.com/fabric-meta'],
['maven.fabricmc.net', 'bmclapi2.bangbang93.com/maven'],
];
// 将官方资源 URL 改写为 BMCLAPI 镜像 URL(未命中的 URL 原样返回)
export function rewriteToMirror(url: string): string {
try {
const u = new URL(url);
for (const [from, to] of MIRROR_RULES) {
const slashIdx = from.indexOf('/');
const fromHost = slashIdx === -1 ? from : from.slice(0, slashIdx);
const fromPath = slashIdx === -1 ? '' : from.slice(slashIdx);
if (u.host !== fromHost) continue;
if (fromPath && !u.pathname.startsWith(fromPath)) continue;
// 命中规则,替换主机(并附加镜像路径前缀)
const toSlashIdx = to.indexOf('/');
u.host = toSlashIdx === -1 ? to : to.slice(0, toSlashIdx);
const toPath = toSlashIdx === -1 ? '' : to.slice(toSlashIdx);
u.pathname = toPath + (fromPath ? u.pathname.slice(fromPath.length) : u.pathname);
return u.toString();
}
return url;
} catch {
return url;
}
}
interface VersionInfo {
id: string;
type: string;
url: string;
releaseTime: string;
}
interface VersionManifest {
@@ -29,9 +78,11 @@ interface DownloadProgress {
}
async function downloadFile(url: string, dest: string, onProgress?: (progress: DownloadProgress) => void): Promise<void> {
// 下载前统一改写为 BMCLAPI 镜像地址
url = rewriteToMirror(url);
return new Promise((resolve, reject) => {
const client = url.startsWith('https') ? https : http;
const request = client.get(url, (response) => {
// 根据协议选择 http / https 客户端(分开调用以避免 TS 联合类型签名不兼容)
const handleResponse = (response: http.IncomingMessage) => {
if (response.statusCode === 302 || response.statusCode === 301) {
downloadFile(response.headers.location!, dest, onProgress).then(resolve).catch(reject);
return;
@@ -52,7 +103,7 @@ async function downloadFile(url: string, dest: string, onProgress?: (progress: D
const file = fs.createWriteStream(dest);
response.on('data', (chunk) => {
response.on('data', (chunk: Buffer) => {
downloadedBytes += chunk.length;
if (onProgress && totalBytes > 0) {
onProgress({
@@ -75,7 +126,14 @@ async function downloadFile(url: string, dest: string, onProgress?: (progress: D
fs.unlink(dest, () => {});
reject(err);
});
});
};
let request: http.ClientRequest;
if (url.startsWith('https')) {
request = https.get(url, handleResponse);
} else {
request = http.get(url, handleResponse);
}
request.on('error', reject);
});
@@ -95,11 +153,12 @@ export async function getVersionList(type?: string): Promise<VersionManifest> {
export async function getForgeVersions(mcVersion?: string) {
try {
const response = await net.fetch(FORGE_VERSION_LIST_URL);
// BMCLAPI Forge 版本列表接口(需指定 MC 版本)
if (!mcVersion) return { versions: [] };
const response = await net.fetch(`https://bmclapi2.bangbang93.com/forge/minecraft/${mcVersion}`);
if (!response.ok) throw new Error(`Forge version list failed: ${response.status}`);
const data = await response.json() as { versions: Record<string, string[]> };
const versions = data.versions[mcVersion || ''] || [];
return { versions };
const data = await response.json() as { version: string }[];
return { versions: data.map((v) => v.version) };
} catch {
return { versions: [] };
}
-295
View File
@@ -1,295 +0,0 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.createInstance = createInstance;
exports.listInstances = listInstances;
exports.getInstanceInfo = getInstanceInfo;
exports.deleteInstance = deleteInstance;
exports.updateInstance = updateInstance;
exports.installInstanceGame = installInstanceGame;
exports.launchInstance = launchInstance;
exports.diagnoseInstance = diagnoseInstance;
exports.getMinecraftVersionList = getMinecraftVersionList;
exports.getForgeVersionList = getForgeVersionList;
exports.getFabricVersionList = getFabricVersionList;
exports.getQuiltVersionList = getQuiltVersionList;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const core_1 = require("@xmcl/core");
const installer_1 = require("@xmcl/installer");
const INSTANCE_CONFIG_FILE = 'instance.json';
function getInstanceConfigPath(instancePath) {
return path.join(instancePath, INSTANCE_CONFIG_FILE);
}
function countFiles(dir, ext) {
if (!fs.existsSync(dir))
return 0;
return fs.readdirSync(dir).filter((f) => {
if (ext)
return f.endsWith(ext);
return fs.statSync(path.join(dir, f)).isFile();
}).length;
}
function getInstanceIssues(instancePath, runtime) {
const issues = [];
if (!fs.existsSync(path.join(instancePath, 'versions', runtime.minecraft, `${runtime.minecraft}.json`))) {
issues.push(`Version JSON not found for ${runtime.minecraft}`);
}
if (!fs.existsSync(path.join(instancePath, 'versions', runtime.minecraft, `${runtime.minecraft}.jar`))) {
issues.push(`Client JAR not found for ${runtime.minecraft}`);
}
return issues;
}
async function createInstance(name, gamePath, runtime, options) {
const instancePath = path.join(gamePath, 'instances', name);
if (fs.existsSync(instancePath)) {
throw new Error(`Instance already exists: ${name}`);
}
fs.mkdirSync(instancePath, { recursive: true });
const now = Date.now();
const config = {
name,
author: options?.author || '',
description: options?.description || '',
runtime,
java: options?.java,
minMemory: options?.minMemory,
maxMemory: options?.maxMemory,
vmOptions: options?.vmOptions,
mcOptions: options?.mcOptions,
creationDate: now,
lastAccessDate: now,
lastPlayedDate: 0,
playtime: 0,
};
fs.writeFileSync(getInstanceConfigPath(instancePath), JSON.stringify(config, null, 2), 'utf-8');
// Create standard directories
for (const dir of ['mods', 'config', 'resourcepacks', 'shaderpacks', 'saves', 'screenshots', 'logs']) {
fs.mkdirSync(path.join(instancePath, dir), { recursive: true });
}
return getInstanceInfo(name, gamePath);
}
async function listInstances(gamePath) {
const instancesPath = path.join(gamePath, 'instances');
const instances = [];
if (!fs.existsSync(instancesPath)) {
return instances;
}
const entries = fs.readdirSync(instancesPath, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isDirectory())
continue;
try {
instances.push(await getInstanceInfo(entry.name, gamePath));
}
catch {
// Skip invalid instances
}
}
return instances;
}
async function getInstanceInfo(name, gamePath) {
const instancePath = path.join(gamePath, 'instances', name);
const configPath = getInstanceConfigPath(instancePath);
if (!fs.existsSync(configPath)) {
throw new Error(`Instance not found: ${name}`);
}
const configRaw = fs.readFileSync(configPath, 'utf-8');
const config = JSON.parse(configRaw);
const issues = getInstanceIssues(instancePath, config.runtime);
return {
name,
path: instancePath,
config,
modCount: countFiles(path.join(instancePath, 'mods'), '.jar'),
resourcePackCount: countFiles(path.join(instancePath, 'resourcepacks'), '.zip'),
screenshotCount: countFiles(path.join(instancePath, 'screenshots')),
saveCount: countFiles(path.join(instancePath, 'saves')),
healthy: issues.length === 0,
issues,
};
}
async function deleteInstance(name, gamePath) {
const instancePath = path.join(gamePath, 'instances', name);
if (!fs.existsSync(instancePath)) {
throw new Error(`Instance not found: ${name}`);
}
fs.rmSync(instancePath, { recursive: true, force: true });
return { deleted: name };
}
async function updateInstance(name, gamePath, patch) {
const instancePath = path.join(gamePath, 'instances', name);
const configPath = getInstanceConfigPath(instancePath);
if (!fs.existsSync(configPath)) {
throw new Error(`Instance not found: ${name}`);
}
const configRaw = fs.readFileSync(configPath, 'utf-8');
const config = JSON.parse(configRaw);
const updated = { ...config, ...patch };
fs.writeFileSync(configPath, JSON.stringify(updated, null, 2), 'utf-8');
return getInstanceInfo(name, gamePath);
}
async function installInstanceGame(name, gamePath, callbacks) {
const instance = await getInstanceInfo(name, gamePath);
const { runtime } = instance.config;
const instancePath = instance.path;
callbacks?.onProgress?.({ stage: 'installing-minecraft', current: 0, total: 100, message: `Installing Minecraft ${runtime.minecraft}...` });
// Install Minecraft
// First get version list to find the version info
const versionList = await (0, installer_1.getVersionList)();
const versionInfo = versionList.versions.find((v) => v.id === runtime.minecraft);
if (!versionInfo) {
throw new Error(`Minecraft version ${runtime.minecraft} not found`);
}
await (0, installer_1.install)({ id: versionInfo.id, url: versionInfo.url }, instancePath);
// Install mod loaders
if (runtime.forge) {
callbacks?.onProgress?.({ stage: 'installing-forge', current: 0, total: 100, message: `Installing Forge ${runtime.forge}...` });
await (0, installer_1.installForge)({ version: runtime.forge, mcversion: runtime.minecraft }, instancePath);
}
if (runtime.fabricLoader) {
callbacks?.onProgress?.({ stage: 'installing-fabric', current: 0, total: 100, message: `Installing Fabric ${runtime.fabricLoader}...` });
await (0, installer_1.installFabric)({
minecraftVersion: runtime.minecraft,
version: runtime.fabricLoader,
minecraft: instancePath,
});
}
if (runtime.quiltLoader) {
callbacks?.onProgress?.({ stage: 'installing-quilt', current: 0, total: 100, message: `Installing Quilt ${runtime.quiltLoader}...` });
await (0, installer_1.installQuiltVersion)({
minecraftVersion: runtime.minecraft,
version: runtime.quiltLoader,
minecraft: instancePath,
});
}
if (runtime.neoForged) {
callbacks?.onProgress?.({ stage: 'installing-neoforge', current: 0, total: 100, message: `Installing NeoForge ${runtime.neoForged}...` });
await (0, installer_1.installNeoForged)('neoforge', runtime.neoForged, instancePath, {});
}
// Note: OptiFine requires downloading the installer JAR first
// if (runtime.optifine) {
// callbacks?.onProgress?.({ stage: 'installing-optifine', current: 0, total: 100, message: `Installing OptiFine ${runtime.optifine}...` });
// await installOptifine(runtime.optifine, instancePath);
// }
callbacks?.onProgress?.({ stage: 'installing-dependencies', current: 0, total: 100, message: 'Installing dependencies...' });
// Install all dependencies (libraries + assets)
const resolved = await core_1.Version.parse(instancePath, runtime.minecraft);
await (0, installer_1.installDependencies)(resolved);
callbacks?.onProgress?.({ stage: 'done', current: 100, total: 100, message: 'Installation complete' });
// Update last access date
await updateInstance(name, gamePath, { lastAccessDate: Date.now() });
return getInstanceInfo(name, gamePath);
}
async function launchInstance(name, gamePath, options) {
const instance = await getInstanceInfo(name, gamePath);
const { runtime } = instance.config;
const resolved = await core_1.Version.parse(instance.path, runtime.minecraft);
const javaPath = options.javaPath || instance.config.java || 'java';
const mcProcess = await (0, core_1.launch)({
gameProfile: {
id: options.uuid,
name: options.username,
},
javaPath,
version: resolved,
gamePath: instance.path,
minMemory: instance.config.minMemory || 1024,
maxMemory: instance.config.maxMemory || 4096,
extraExecOption: { detached: true, stdio: 'ignore' },
server: options.server ? { ip: options.server.host, port: options.server.port } : undefined,
});
const watcher = (0, core_1.createMinecraftProcessWatcher)(mcProcess);
watcher.on('minecraft-window-ready', () => {
options.onEvent?.({ event: 'window-ready' });
});
watcher.on('minecraft-exit', ({ code }) => {
options.onEvent?.({ event: 'exit', code });
});
// Update playtime tracking
const startTime = Date.now();
mcProcess.on('exit', async () => {
const elapsed = Date.now() - startTime;
try {
const info = await getInstanceInfo(name, gamePath);
await updateInstance(name, gamePath, {
lastPlayedDate: Date.now(),
playtime: (info.config.playtime || 0) + elapsed,
});
}
catch {
// Ignore errors during playtime update
}
});
// Update last access date
await updateInstance(name, gamePath, { lastAccessDate: Date.now() });
return {
pid: mcProcess.pid || 0,
version: runtime.minecraft,
username: options.username,
};
}
async function diagnoseInstance(name, gamePath) {
const instance = await getInstanceInfo(name, gamePath);
return {
healthy: instance.healthy,
issues: instance.issues,
};
}
// Version list APIs
async function getMinecraftVersionList(type) {
const manifest = await (0, installer_1.getVersionList)();
let versions = manifest.versions;
if (type && type !== 'all') {
versions = versions.filter((v) => v.type === type);
}
return { versions };
}
async function getForgeVersionList(mcVersion) {
const list = await (0, installer_1.getForgeVersionList)({ minecraft: mcVersion });
return { versions: list.versions };
}
async function getFabricVersionList(mcVersion) {
if (mcVersion) {
const loaders = await (0, installer_1.getFabricLoaders)();
return { versions: loaders.map((l) => l.version) };
}
const loaders = await (0, installer_1.getFabricLoaders)();
return { versions: loaders.map((l) => l.version) };
}
async function getQuiltVersionList(mcVersion) {
const loaders = await (0, installer_1.getQuiltLoaderVersionsByMinecraft)({ minecraftVersion: mcVersion || '*' });
return { versions: loaders.map((l) => l.loader.version) };
}
+79 -12
View File
@@ -14,6 +14,18 @@ import {
getFabricLoaders,
getQuiltLoaderVersionsByMinecraft,
} from '@xmcl/installer';
import { rewriteToMirror } from './installer';
// ==================== BMCLAPI 镜像源配置 ====================
// 版本清单(@xmcl/installer 的 getVersionList 通过 remote 参数覆盖)
const BMCLAPI_VERSION_MANIFEST = 'https://bmclapi2.bangbang93.com/mc/game/version_manifest.json';
// Maven 仓库镜像(Libraries / Forge / NeoForge / Fabric 构件)
const BMCLAPI_MAVEN = 'https://bmclapi2.bangbang93.com/maven';
// 资源文件镜像(assets
const BMCLAPI_ASSETS = 'https://bmclapi2.bangbang93.com/assets';
// 供 @xmcl/installer FetchOptions 使用的镜像 fetchmeta.fabricmc.net 等元数据接口走镜像)
const mirrorFetch: typeof fetch = (url, init) => fetch(rewriteToMirror(String(url)), init);
export interface InstanceRuntime {
minecraft: string;
@@ -232,18 +244,24 @@ export async function installInstanceGame(
callbacks?.onProgress?.({ stage: 'installing-minecraft', current: 0, total: 100, message: `Installing Minecraft ${runtime.minecraft}...` });
// Install Minecraft
// First get version list to find the version info
const versionList = await getVersionList();
// First get version list to find the version info(走 BMCLAPI 镜像)
const versionList = await getVersionList({ remote: BMCLAPI_VERSION_MANIFEST, fetch: mirrorFetch });
const versionInfo = versionList.versions.find((v) => v.id === runtime.minecraft);
if (!versionInfo) {
throw new Error(`Minecraft version ${runtime.minecraft} not found`);
}
await xmclInstall({ id: versionInfo.id, url: versionInfo.url }, instancePath);
await xmclInstall({ id: versionInfo.id, url: versionInfo.url }, instancePath, {
// 版本 JSON / 客户端 JAR 走镜像(BMCLAPI 会回源官方地址)
json: (v) => rewriteToMirror(v.url),
client: (v) => (v.downloads.client ? rewriteToMirror(v.downloads.client.url) : []),
mavenHost: BMCLAPI_MAVEN,
assetsHost: BMCLAPI_ASSETS,
});
// Install mod loaders
if (runtime.forge) {
callbacks?.onProgress?.({ stage: 'installing-forge', current: 0, total: 100, message: `Installing Forge ${runtime.forge}...` });
await installForge({ version: runtime.forge, mcversion: runtime.minecraft }, instancePath);
await installForge({ version: runtime.forge, mcversion: runtime.minecraft }, instancePath, { mavenHost: BMCLAPI_MAVEN });
}
if (runtime.fabricLoader) {
@@ -252,6 +270,7 @@ export async function installInstanceGame(
minecraftVersion: runtime.minecraft,
version: runtime.fabricLoader,
minecraft: instancePath,
fetch: mirrorFetch,
});
}
@@ -266,7 +285,7 @@ export async function installInstanceGame(
if (runtime.neoForged) {
callbacks?.onProgress?.({ stage: 'installing-neoforge', current: 0, total: 100, message: `Installing NeoForge ${runtime.neoForged}...` });
await installNeoForged('neoforge', runtime.neoForged, instancePath, {});
await installNeoForged('neoforge', runtime.neoForged, instancePath, { mavenHost: BMCLAPI_MAVEN });
}
// Note: OptiFine requires downloading the installer JAR first
@@ -277,9 +296,9 @@ export async function installInstanceGame(
callbacks?.onProgress?.({ stage: 'installing-dependencies', current: 0, total: 100, message: 'Installing dependencies...' });
// Install all dependencies (libraries + assets)
// Install all dependencies (libraries + assets)(走 BMCLAPI 镜像)
const resolved: ResolvedVersion = await Version.parse(instancePath, runtime.minecraft);
await installDependencies(resolved);
await installDependencies(resolved, { mavenHost: BMCLAPI_MAVEN, assetsHost: BMCLAPI_ASSETS });
callbacks?.onProgress?.({ stage: 'done', current: 100, total: 100, message: 'Installation complete' });
@@ -370,12 +389,13 @@ export async function diagnoseInstance(
// Version list APIs
export async function getMinecraftVersionList(type?: string) {
const manifest = await getVersionList();
// 版本清单一律走 BMCLAPI 镜像
const manifest = await getVersionList({ remote: BMCLAPI_VERSION_MANIFEST, fetch: mirrorFetch });
let versions = manifest.versions;
if (type && type !== 'all') {
versions = versions.filter((v) => v.type === type);
versions = versions.filter((v: { type: string }) => v.type === type);
}
return { versions };
return { versions, latest: manifest.latest };
}
export async function getForgeVersionList(mcVersion?: string) {
@@ -384,11 +404,12 @@ export async function getForgeVersionList(mcVersion?: string) {
}
export async function getFabricVersionList(mcVersion?: string) {
// Fabric 元数据走 BMCLAPI 镜像(meta.fabricmc.net → fabric-meta
if (mcVersion) {
const loaders = await getFabricLoaders();
const loaders = await getFabricLoaders({ fetch: mirrorFetch });
return { versions: loaders.map((l) => l.version) };
}
const loaders = await getFabricLoaders();
const loaders = await getFabricLoaders({ fetch: mirrorFetch });
return { versions: loaders.map((l) => l.version) };
}
@@ -396,3 +417,49 @@ export async function getQuiltVersionList(mcVersion?: string) {
const loaders = await getQuiltLoaderVersionsByMinecraft({ minecraftVersion: mcVersion || '*' });
return { versions: loaders.map((l) => l.loader.version) };
}
// 扫描目录中已安装版本的信息
export interface ScannedVersion {
id: string;
type: string;
releaseTime?: string;
/** JAR 文件是否存在 */
hasJar: boolean;
/** JSON 文件是否存在 */
hasJson: boolean;
}
// 扫描指定目录,返回 versions 子目录中所有已安装版本的详细信息
export function scanGameDirectories(gamePath: string): ScannedVersion[] {
const versionsPath = path.join(gamePath, 'versions');
if (!fs.existsSync(versionsPath)) return [];
try {
return fs.readdirSync(versionsPath, { withFileTypes: true })
.filter((e) => e.isDirectory())
.map((e) => {
const id = e.name;
const versionDir = path.join(versionsPath, id);
const jsonPath = path.join(versionDir, `${id}.json`);
const jarPath = path.join(versionDir, `${id}.jar`);
const hasJson = fs.existsSync(jsonPath);
const hasJar = fs.existsSync(jarPath);
let type = "unknown";
let releaseTime: string | undefined;
// 尝试读取 version JSON 获取类型和时间
if (hasJson) {
try {
const raw = fs.readFileSync(jsonPath, 'utf-8');
const json = JSON.parse(raw);
type = json.type || "unknown";
releaseTime = json.releaseTime;
} catch {}
}
return { id, type, releaseTime, hasJar, hasJson };
});
} catch {
return [];
}
}
-175
View File
@@ -1,175 +0,0 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.requestDeviceCode = requestDeviceCode;
exports.pollForTokenOnce = pollForTokenOnce;
exports.refreshAccessToken = refreshAccessToken;
exports.decodeIdToken = decodeIdToken;
exports.saveKoringAuth = saveKoringAuth;
exports.readKoringAuth = readKoringAuth;
exports.deleteKoringAuth = deleteKoringAuth;
const https = __importStar(require("https"));
const auth_1 = require("../auth");
const CLIENT_ID = '547qe8ky1pr69f08b71kj';
const DEVICE_AUTH_URL = 'https://oac.lingke.ink/oidc/device/auth';
const TOKEN_URL = 'https://oac.lingke.ink/oidc/token';
const SCOPE = 'openid offline_access profile';
function postForm(url, data) {
return new Promise((resolve, reject) => {
const params = new URLSearchParams(data);
const body = params.toString().replace(/\+/g, '%20');
const urlObj = new URL(url);
console.log(`[koring-auth] POST ${url}`);
console.log(`[koring-auth] body: ${body}`);
const req = https.request({
hostname: urlObj.hostname,
port: urlObj.port || 443,
path: urlObj.pathname,
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': Buffer.byteLength(body),
},
}, (res) => {
let raw = '';
res.on('data', (chunk) => (raw += chunk));
res.on('end', () => {
console.log(`[koring-auth] response (${res.statusCode}): ${raw}`);
try {
resolve(JSON.parse(raw));
}
catch {
reject(new Error(`Invalid response: ${raw}`));
}
});
});
req.on('error', reject);
req.write(body);
req.end();
});
}
function parseJwt(token) {
try {
const payload = token.split('.')[1];
const decoded = Buffer.from(payload, 'base64url').toString('utf-8');
return JSON.parse(decoded);
}
catch {
return {};
}
}
async function requestDeviceCode() {
const res = await postForm(DEVICE_AUTH_URL, {
client_id: CLIENT_ID,
scope: SCOPE,
});
if (res.error)
throw new Error(res.error_description || res.error);
return res;
}
async function pollForTokenOnce(device_code) {
const res = await postForm(TOKEN_URL, {
client_id: CLIENT_ID,
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
device_code,
});
if (res.access_token) {
return res;
}
if (res.error === 'expired_token' || res.error === 'access_denied') {
throw new Error(res.error);
}
// authorization_pending or slow_down — throw so caller can retry
throw new Error(res.error || 'authorization_pending');
}
async function refreshAccessToken(refresh_token) {
const res = await postForm(TOKEN_URL, {
client_id: CLIENT_ID,
grant_type: 'refresh_token',
refresh_token,
});
if (res.error)
throw new Error(res.error_description || res.error);
return res;
}
function decodeIdToken(id_token) {
const payload = parseJwt(id_token);
return {
sub: payload.sub || '',
name: payload.name || '',
username: payload.username || payload.name || '',
email: payload.email || '',
picture: payload.picture || '',
};
}
function saveKoringAuth(tokenRes) {
const user = decodeIdToken(tokenRes.id_token);
const auth = {
user,
access_token: tokenRes.access_token,
refresh_token: tokenRes.refresh_token,
id_token: tokenRes.id_token,
expires_at: Date.now() + tokenRes.expires_in * 1000,
};
// Reuse existing auth file for storage
(0, auth_1.writeAuth)({
username: user.username,
uuid: user.sub,
accessToken: auth.access_token,
refreshToken: auth.refresh_token,
xboxProfile: JSON.stringify(user),
});
return user;
}
function readKoringAuth() {
const auth = (0, auth_1.readAuth)();
if (!auth.username || !auth.refreshToken)
return null;
let user = { sub: '', name: '', username: '', email: '', picture: '' };
try {
user = JSON.parse(auth.xboxProfile || '{}');
}
catch { }
return {
user,
access_token: auth.accessToken,
refresh_token: auth.refreshToken,
id_token: '',
expires_at: 0,
};
}
function deleteKoringAuth() {
(0, auth_1.writeAuth)({ username: '', uuid: '', accessToken: '', refreshToken: '', xboxProfile: '' });
}
-152
View File
@@ -1,152 +0,0 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.launchMinecraft = launchMinecraft;
exports.diagnoseVersion = diagnoseVersion;
const child_process_1 = require("child_process");
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const runningProcesses = new Map();
async function launchMinecraft(options) {
const versionJsonPath = path.join(options.gamePath, 'versions', options.version, `${options.version}.json`);
if (!fs.existsSync(versionJsonPath)) {
throw new Error(`Version JSON not found: ${versionJsonPath}`);
}
const versionJson = JSON.parse(fs.readFileSync(versionJsonPath, 'utf-8'));
const mainClass = versionJson.mainClass;
if (!mainClass) {
throw new Error('Main class not found in version JSON');
}
const args = [];
// Memory
const minMem = options.memory?.min || '512M';
const maxMem = options.memory?.max || '4G';
args.push(`-Xms${minMem}`);
args.push(`-Xmx${maxMem}`);
// JVM args
if (options.jvmArgs) {
args.push(...options.jvmArgs);
}
// Native libraries path
const nativesDir = path.join(options.gamePath, 'versions', options.version, `${options.version}-natives`);
if (fs.existsSync(nativesDir)) {
args.push(`-Djava.library.path=${nativesDir}`);
}
// Classpath
const libraries = versionJson.libraries || [];
const classpath = libraries
.filter((lib) => lib.downloads?.artifact?.path)
.map((lib) => path.join(options.gamePath, 'libraries', lib.downloads.artifact.path));
const clientJar = path.join(options.gamePath, 'versions', options.version, `${options.version}.jar`);
if (fs.existsSync(clientJar)) {
classpath.push(clientJar);
}
args.push('-cp');
args.push(classpath.join(path.delimiter));
args.push(mainClass);
// Game args
args.push(`--username`, options.username);
args.push(`--version`, options.version);
args.push(`--gameDir`, options.gamePath);
args.push(`--assetsDir`, path.join(options.gamePath, 'assets'));
args.push(`--assetIndex`, versionJson.assetIndex?.id || options.version);
args.push(`--uuid`, options.uuid);
if (options.accessToken) {
args.push(`--accessToken`, options.accessToken);
}
if (options.server) {
args.push(`--server`, options.server.ip);
if (options.server.port) {
args.push(`--port`, String(options.server.port));
}
}
if (options.gameArgs) {
args.push(...options.gameArgs);
}
const javaPath = options.javaPath || 'java';
return new Promise((resolve, reject) => {
const child = (0, child_process_1.spawn)(javaPath, args, {
cwd: options.gamePath,
detached: options.detached,
stdio: ['ignore', 'pipe', 'pipe'],
});
const requestId = `mc-${Date.now()}`;
runningProcesses.set(requestId, child);
child.stdout?.on('data', (data) => {
const line = data.toString().trim();
if (line) {
options.onEvent?.({ event: 'stdout', message: line });
}
});
child.stderr?.on('data', (data) => {
const line = data.toString().trim();
if (line) {
options.onEvent?.({ event: 'stderr', message: line });
}
});
child.on('error', (err) => {
runningProcesses.delete(requestId);
options.onEvent?.({ event: 'error', error: String(err) });
reject(err);
});
child.on('exit', (code) => {
runningProcesses.delete(requestId);
options.onEvent?.({ event: 'exit', code });
});
resolve({
pid: child.pid || 0,
version: options.version,
username: options.username,
});
});
}
async function diagnoseVersion(gamePath, version) {
const issues = [];
const versionDir = path.join(gamePath, 'versions', version);
const versionJsonPath = path.join(versionDir, `${version}.json`);
const jarPath = path.join(versionDir, `${version}.jar`);
if (!fs.existsSync(versionJsonPath)) {
issues.push(`Version JSON not found: ${versionJsonPath}`);
}
if (!fs.existsSync(jarPath)) {
issues.push(`Client JAR not found: ${jarPath}`);
}
return {
version,
gamePath,
healthy: issues.length === 0,
issues,
};
}
-163
View File
@@ -1,163 +0,0 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.searchMods = searchMods;
exports.getModDetail = getModDetail;
exports.getModVersions = getModVersions;
exports.installMod = installMod;
const electron_1 = __importDefault(require("electron"));
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const { net } = electron_1.default;
const MODRINTH_API = 'https://api.modrinth.com/v2';
const CURSEFORGE_API = 'https://api.curseforge.com/v1';
async function searchModrinth(query, gameVersion, loader, limit = 20, offset = 0) {
const facets = [];
if (gameVersion)
facets.push([`versions:${gameVersion}`]);
if (loader)
facets.push([`categories:${loader}`]);
const params = new URLSearchParams({
query: query || '',
limit: String(limit),
offset: String(offset),
});
if (facets.length > 0) {
params.set('facets', JSON.stringify(facets));
}
const response = await net.fetch(`${MODRINTH_API}/search?${params.toString()}`);
if (!response.ok)
throw new Error(`Modrinth search failed: ${response.status}`);
const data = await response.json();
return data.hits.map((hit) => ({
id: hit.project_id,
slug: hit.slug,
name: hit.title,
description: hit.description,
downloads: hit.downloads,
iconUrl: hit.icon_url,
categories: hit.categories,
versions: hit.versions,
source: 'modrinth',
}));
}
async function searchCurseForge(query, gameVersion, loader, limit = 20, offset = 0) {
// CurseForge requires API key - return empty for now
return [];
}
async function searchMods(query, gameVersion, loader, limit, offset, source = 'modrinth') {
if (source === 'modrinth') {
return searchModrinth(query, gameVersion, loader, limit, offset);
}
return searchCurseForge(query, gameVersion, loader, limit, offset);
}
async function getModDetail(projectId, source = 'modrinth') {
if (source === 'modrinth') {
const response = await net.fetch(`${MODRINTH_API}/project/${projectId}`);
if (!response.ok)
throw new Error(`Modrinth detail failed: ${response.status}`);
const data = await response.json();
return {
id: data.id,
slug: data.slug,
name: data.title,
description: data.description,
downloads: data.downloads,
iconUrl: data.icon_url,
categories: data.categories,
versions: data.versions,
source: 'modrinth',
};
}
throw new Error(`CurseForge detail not implemented`);
}
async function getModVersions(projectId, gameVersion, loader, source = 'modrinth') {
if (source === 'modrinth') {
const params = new URLSearchParams();
if (gameVersion)
params.set('game_versions', JSON.stringify([gameVersion]));
if (loader)
params.set('loaders', JSON.stringify([loader]));
const response = await net.fetch(`${MODRINTH_API}/project/${projectId}/version?${params.toString()}`);
if (!response.ok)
throw new Error(`Modrinth versions failed: ${response.status}`);
const data = await response.json();
return data.map((v) => ({
id: v.id,
name: v.name,
versionNumber: v.version_number,
gameVersions: v.game_versions,
loaders: v.loaders,
files: v.files.map((f) => ({
filename: f.filename,
url: f.url,
size: f.size,
primary: f.primary,
})),
}));
}
throw new Error(`CurseForge versions not implemented`);
}
async function installMod(projectId, versionId, gamePath, source = 'modrinth') {
const versions = await getModVersions(projectId, undefined, undefined, source);
const targetVersion = versionId
? versions.find((v) => v.id === versionId)
: versions[0];
if (!targetVersion)
throw new Error('No version found');
const primaryFile = targetVersion.files.find((f) => f.primary) || targetVersion.files[0];
if (!primaryFile)
throw new Error('No file found');
// Download the mod file
const modsDir = path.join(gamePath, 'mods');
if (!fs.existsSync(modsDir)) {
fs.mkdirSync(modsDir, { recursive: true });
}
const filePath = path.join(modsDir, primaryFile.filename);
const response = await net.fetch(primaryFile.url);
if (!response.ok)
throw new Error(`Download failed: ${response.status}`);
const buffer = Buffer.from(await response.arrayBuffer());
fs.writeFileSync(filePath, buffer);
return {
projectId,
versionId: targetVersion.id,
filename: primaryFile.filename,
path: filePath,
};
}
+90 -28
View File
@@ -20,6 +20,19 @@ interface ModSearchResult {
source: 'modrinth' | 'curseforge';
}
interface ModSearchResponse {
hits: ModSearchResult[];
total: number;
limit: number;
offset: number;
}
interface ModCategory {
name: string;
label: string;
projectType: string;
}
interface ModVersionResult {
id: string;
name: string;
@@ -38,17 +51,21 @@ async function searchModrinth(
query?: string,
gameVersion?: string,
loader?: string,
category?: string,
projectType: string = 'mod',
limit: number = 20,
offset: number = 0
): Promise<ModSearchResult[]> {
): Promise<ModSearchResponse> {
const facets: string[][] = [];
if (gameVersion) facets.push([`versions:${gameVersion}`]);
if (loader) facets.push([`categories:${loader}`]);
if (category) facets.push([`categories:${category}`]);
const params = new URLSearchParams({
query: query || '',
limit: String(limit),
offset: String(offset),
project_type: projectType,
});
if (facets.length > 0) {
@@ -58,30 +75,38 @@ async function searchModrinth(
const response = await net.fetch(`${MODRINTH_API}/search?${params.toString()}`);
if (!response.ok) throw new Error(`Modrinth search failed: ${response.status}`);
const data = await response.json() as { hits: Array<{
project_id: string;
slug: string;
title: string;
description: string;
downloads: number;
icon_url?: string;
categories?: string[];
versions?: string[];
client_side?: string;
server_side?: string;
}> };
const data = await response.json() as {
total_hits: number;
hits: Array<{
project_id: string;
slug: string;
title: string;
description: string;
downloads: number;
icon_url?: string;
categories?: string[];
versions?: string[];
client_side?: string;
server_side?: string;
}>;
};
return data.hits.map((hit) => ({
id: hit.project_id,
slug: hit.slug,
name: hit.title,
description: hit.description,
downloads: hit.downloads,
iconUrl: hit.icon_url,
categories: hit.categories,
versions: hit.versions,
source: 'modrinth' as const,
}));
return {
hits: data.hits.map((hit) => ({
id: hit.project_id,
slug: hit.slug,
name: hit.title,
description: hit.description,
downloads: hit.downloads,
iconUrl: hit.icon_url,
categories: hit.categories,
versions: hit.versions,
source: 'modrinth' as const,
})),
total: data.total_hits,
limit,
offset,
};
}
async function searchCurseForge(
@@ -90,25 +115,62 @@ async function searchCurseForge(
loader?: string,
limit: number = 20,
offset: number = 0
): Promise<ModSearchResult[]> {
): Promise<ModSearchResponse> {
// CurseForge requires API key - return empty for now
return [];
return { hits: [], total: 0, limit, offset };
}
export async function searchMods(
query?: string,
gameVersion?: string,
loader?: string,
category?: string,
projectType: string = 'mod',
limit?: number,
offset?: number,
source: 'modrinth' | 'curseforge' = 'modrinth'
): Promise<ModSearchResult[]> {
): Promise<ModSearchResponse> {
if (source === 'modrinth') {
return searchModrinth(query, gameVersion, loader, limit, offset);
return searchModrinth(query, gameVersion, loader, category, projectType, limit, offset);
}
return searchCurseForge(query, gameVersion, loader, limit, offset);
}
export async function getCategories(projectType: string = 'mod'): Promise<ModCategory[]> {
const response = await net.fetch(`${MODRINTH_API}/tag/category`);
if (!response.ok) throw new Error(`Modrinth categories failed: ${response.status}`);
const data = await response.json() as Array<{
name: string;
project_type: string;
header: string;
}>;
// 只返回指定类型分类,label 优先取 header(国际化标题),无则用 name
return data
.filter((c) => c.project_type === projectType)
.map((c) => ({ name: c.name, label: c.header || c.name, projectType: c.project_type }));
}
export async function getGameVersions(): Promise<string[]> {
const response = await net.fetch(`${MODRINTH_API}/tag/game_version`);
if (!response.ok) throw new Error(`Modrinth game versions failed: ${response.status}`);
const data = await response.json() as string[];
// 只保留正式版(1.x 且非预览版),并按版本号倒序
return data
.filter((v) => /^\d+\.\d+(\.\d+)?$/.test(v))
.sort((a, b) => {
const pa = a.split('.').map(Number);
const pb = b.split('.').map(Number);
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
const diff = (pb[i] || 0) - (pa[i] || 0);
if (diff !== 0) return diff;
}
return 0;
});
}
export async function getModDetail(
projectId: string,
source: 'modrinth' | 'curseforge' = 'modrinth'
-38
View File
@@ -1,38 +0,0 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.registerAuthHandlers = registerAuthHandlers;
const electron_1 = __importDefault(require("electron"));
const { ipcMain } = electron_1.default;
const auth_1 = require("../auth");
function registerAuthHandlers() {
ipcMain.handle('auth:get', () => {
try {
const auth = (0, auth_1.readAuth)();
return { success: true, data: auth, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('auth:save', (_event, auth) => {
try {
(0, auth_1.writeAuth)(auth);
return { success: true, data: null, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('auth:delete', () => {
try {
(0, auth_1.deleteAuth)();
return { success: true, data: null, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
}
-128
View File
@@ -1,128 +0,0 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.registerBackgroundHandlers = registerBackgroundHandlers;
const electron_1 = __importDefault(require("electron"));
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const config_1 = require("../config");
const { ipcMain } = electron_1.default;
function registerBackgroundHandlers() {
ipcMain.handle('background:set-image', async (_event, payload) => {
try {
const config = (0, config_1.loadConfig)();
config.background.bgType = 'image';
config.background.image = payload.url;
if (payload.blur !== undefined)
config.background.blur = payload.blur;
if (payload.opacity !== undefined)
config.background.opacity = payload.opacity;
(0, config_1.saveConfig)(config);
return { success: true, data: config.background, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('background:set-color', async (_event, payload) => {
try {
const config = (0, config_1.loadConfig)();
config.background.bgType = 'color';
config.background.image = payload.color;
(0, config_1.saveConfig)(config);
return { success: true, data: config.background, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('background:set-blur', async (_event, payload) => {
try {
const config = (0, config_1.loadConfig)();
config.background.blur = payload.blur;
(0, config_1.saveConfig)(config);
return { success: true, data: config.background, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('background:set-opacity', async (_event, payload) => {
try {
const config = (0, config_1.loadConfig)();
config.background.opacity = payload.opacity;
(0, config_1.saveConfig)(config);
return { success: true, data: config.background, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('background:set-animation', async (_event, payload) => {
try {
// Animation config is not persisted in current design, return defaults
return { success: true, data: { bgType: 'image', image: '/background.png', blur: 0, opacity: 100 }, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('background:get', async () => {
try {
const config = (0, config_1.loadConfig)();
return { success: true, data: config.background, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('background:set-theme', async (_event, payload) => {
try {
const config = (0, config_1.loadConfig)();
config.theme.darkMode = payload.theme;
(0, config_1.saveConfig)(config);
return { success: true, data: config.background, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('background:reset', async () => {
try {
const config = (0, config_1.loadConfig)();
config.background = { bgType: 'image', image: '/background.png', blur: 0, opacity: 100 };
(0, config_1.saveConfig)(config);
return { success: true, data: config.background, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
// Copy file to userData and return the destination path
ipcMain.handle('background:copyToUserData', async (_event, srcPath, ext) => {
try {
const userDataPath = electron_1.default.app.getPath('userData');
const destPath = path_1.default.join(userDataPath, `background-custom${ext}`);
fs_1.default.copyFileSync(srcPath, destPath);
return destPath;
}
catch {
return null;
}
});
// Get cached background file path from userData
ipcMain.handle('background:getCachedPath', async () => {
try {
const userDataPath = electron_1.default.app.getPath('userData');
const files = fs_1.default.readdirSync(userDataPath).filter(f => f.startsWith('background-custom'));
if (files.length === 0)
return null;
return path_1.default.join(userDataPath, files[0]);
}
catch {
return null;
}
});
}
-77
View File
@@ -1,77 +0,0 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.registerConfigHandlers = registerConfigHandlers;
const electron_1 = __importDefault(require("electron"));
const { ipcMain, app } = electron_1.default;
const fs = __importStar(require("fs"));
const config_1 = require("../config");
function registerConfigHandlers() {
ipcMain.handle('config:get', () => {
try {
const config = (0, config_1.loadConfig)();
return { success: true, data: config, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('config:save', (_event, config) => {
try {
(0, config_1.saveConfig)(config);
return { success: true, data: null, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('config:reset', () => {
try {
const filePath = (0, config_1.configPath)();
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
}
app.relaunch();
app.exit(0);
return { success: true, data: null, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
}
-206
View File
@@ -1,206 +0,0 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.setupCrashListeners = setupCrashListeners;
exports.registerCrashHandlers = registerCrashHandlers;
exports.testCrashDialog = testCrashDialog;
const electron_1 = __importDefault(require("electron"));
const path_1 = __importDefault(require("path"));
const fs_1 = __importDefault(require("fs"));
const crash_logger_1 = require("../core/crash-logger");
const { app, ipcMain, BrowserWindow } = electron_1.default;
const isDev = !app.isPackaged;
let crashWin = null;
function getIconPath() {
return path_1.default.join(__dirname, '../../build/icon.ico');
}
function createCrashWindow() {
const iconPath = getIconPath();
const win = new BrowserWindow({
width: 600,
height: 460,
minWidth: 500,
maxWidth: 700,
minHeight: 460,
maxHeight: 460,
frame: false,
transparent: false,
resizable: false,
show: false,
icon: iconPath,
webPreferences: {
preload: path_1.default.join(__dirname, '../preload-crash.js'),
nodeIntegration: false,
contextIsolation: true,
sandbox: false,
},
});
if (isDev) {
win.loadURL('http://localhost:1420/crash.html');
}
else {
win.loadFile(path_1.default.join(__dirname, '../../dist/crash.html'));
}
return win;
}
function sendToCrashWindow(data) {
if (!crashWin || crashWin.isDestroyed()) {
crashWin = createCrashWindow();
}
const send = () => {
crashWin?.show();
crashWin?.focus();
crashWin?.webContents.send('crash:show', {
type: data.type,
message: data.message,
timestamp: data.timestamp,
});
};
if (crashWin.webContents.isLoading()) {
crashWin.webContents.once('did-finish-load', send);
}
else {
send();
}
}
function setupCrashListeners(mainWindow) {
mainWindow.webContents.on('render-process-gone', (_event, details) => {
const entry = {
timestamp: new Date().toISOString(),
type: 'renderer-gone',
message: `渲染进程崩溃: ${details.reason} (退出码: ${details.exitCode})`,
details: details,
};
(0, crash_logger_1.writeCrashLog)(entry);
sendToCrashWindow(entry);
});
mainWindow.on('unresponsive', () => {
const entry = {
timestamp: new Date().toISOString(),
type: 'unresponsive',
message: '渲染进程无响应',
};
(0, crash_logger_1.writeCrashLog)(entry);
sendToCrashWindow(entry);
});
// Inject devtools crash tools when devtools opens
if (isDev) {
mainWindow.webContents.on('devtools-opened', () => {
const js = `
(function() {
if (window.__crashToolsLoaded) return;
window.__crashToolsLoaded = true;
console.log('%c[崩溃工具] 已加载', 'color: #f59e0b; font-weight: bold; font-size: 14px;');
console.log('%c可用命令:', 'color: #3b82f6; font-weight: bold;');
console.log('%c crash.simulate() %c— 模拟渲染进程崩溃', 'color: #ef4444; font-weight: bold;', 'color: inherit;');
console.log('%c crash.testDialog() %c— 测试崩溃弹窗', 'color: #ef4444; font-weight: bold;', 'color: inherit;');
console.log('%c crash.readLog() %c— 读取崩溃日志', 'color: #ef4444; font-weight: bold;', 'color: inherit;');
console.log('%c crash.factoryReset()%c— 强还原配置', 'color: #ef4444; font-weight: bold;', 'color: inherit;');
console.log('%c crash.restart() %c— 重启应用', 'color: #ef4444; font-weight: bold;', 'color: inherit;');
console.log('');
window.crash = {
simulate: function() { window.electronAPI?.simulateCrash(); },
testDialog: function() { window.electronAPI?.testCrashDialog(); },
readLog: function() { return window.electronAPI?.invoke('crash:readLog'); },
factoryReset: function() { return window.electronAPI?.invoke('crash:factoryReset'); },
restart: function() { return window.electronAPI?.invoke('crash:restart'); },
};
})();
`;
mainWindow.webContents.executeJavaScript(js);
});
}
}
function registerCrashHandlers() {
// Close crash window
ipcMain.handle('crash:closeWindow', () => {
if (crashWin && !crashWin.isDestroyed()) {
crashWin.close();
crashWin = null;
}
});
// Read crash log
ipcMain.handle('crash:readLog', () => {
return (0, crash_logger_1.readCrashLog)();
});
// Simulate crash — forcefully crash the renderer (devtools only)
ipcMain.handle('crash:simulate', (_event) => {
const senderWebContents = electron_1.default.webContents.getAllWebContents().find((wc) => wc.id === _event.sender.id);
if (senderWebContents) {
senderWebContents.forcefullyCrashRenderer();
}
});
// Test crash dialog — show crash window without actually crashing
ipcMain.handle('crash:testDialog', () => {
testCrashDialog();
});
// Factory reset
ipcMain.handle('crash:factoryReset', () => {
const dataPath = app.isPackaged
? path_1.default.dirname(app.getPath('exe'))
: path_1.default.join(__dirname, '../..');
// Delete config
try {
const configPath = path_1.default.join(dataPath, 'Koring.yml');
if (fs_1.default.existsSync(configPath))
fs_1.default.unlinkSync(configPath);
}
catch { }
// Delete auth
try {
const authPath = path_1.default.join(dataPath, 'koring-auth.json');
if (fs_1.default.existsSync(authPath))
fs_1.default.unlinkSync(authPath);
}
catch { }
// Delete background cache in userData
try {
const bgPath = path_1.default.join(app.getPath('userData'), 'background.png');
if (fs_1.default.existsSync(bgPath))
fs_1.default.unlinkSync(bgPath);
}
catch { }
// Delete crash log
(0, crash_logger_1.clearCrashLog)();
return { success: true };
});
// Restart app
ipcMain.handle('crash:restart', () => {
app.relaunch();
app.exit(0);
});
// Main process error handlers
process.on('uncaughtException', (error) => {
const entry = {
timestamp: new Date().toISOString(),
type: 'uncaught-exception',
message: error.message,
stack: error.stack,
};
(0, crash_logger_1.writeCrashLog)(entry);
sendToCrashWindow(entry);
});
process.on('unhandledRejection', (reason) => {
const entry = {
timestamp: new Date().toISOString(),
type: 'unhandled-rejection',
message: String(reason),
stack: reason instanceof Error ? reason.stack : undefined,
};
(0, crash_logger_1.writeCrashLog)(entry);
sendToCrashWindow(entry);
});
}
// Called from devtools to test crash dialog
function testCrashDialog() {
const entry = {
timestamp: new Date().toISOString(),
type: 'test',
message: '这是一个测试崩溃弹窗',
};
sendToCrashWindow(entry);
}
-74
View File
@@ -1,74 +0,0 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.registerInstallHandlers = registerInstallHandlers;
const electron_1 = __importDefault(require("electron"));
const installer_1 = require("../core/installer");
const { ipcMain } = electron_1.default;
function registerInstallHandlers(win) {
ipcMain.handle('install:version-list', async (_event, payload) => {
try {
const data = await (0, installer_1.getVersionList)(payload.type);
return { success: true, data, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('install:forge-version-list', async (_event, payload) => {
try {
const data = await (0, installer_1.getForgeVersions)(payload.mcVersion);
return { success: true, data, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('install:fabric-version-list', async (_event, payload) => {
try {
const data = await (0, installer_1.getFabricVersions)(payload.mcVersion);
return { success: true, data, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('install:minecraft', async (_event, payload) => {
try {
const requestId = `install-mc-${Date.now()}`;
(0, installer_1.installMinecraft)(payload.version, payload.gamePath, payload.javaPath, payload.downloadThreads, {
onProgress: (progress) => {
win.mainWindow?.webContents.send('install:progress', { requestId, ...progress });
},
}).then((result) => {
win.mainWindow?.webContents.send('install:complete', { requestId, ...result });
}).catch((err) => {
win.mainWindow?.webContents.send('install:error', { requestId, error: String(err) });
});
return { success: true, data: { requestId }, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('install:mod-loader', async (_event, payload) => {
try {
const requestId = `install-loader-${Date.now()}`;
(0, installer_1.installModLoader)(payload.mcVersion, payload.gamePath, payload.loaderType, payload.loaderVersion, payload.javaPath, {
onProgress: (progress) => {
win.mainWindow?.webContents.send('install:progress', { requestId, ...progress });
},
}).then((result) => {
win.mainWindow?.webContents.send('install:complete', { requestId, ...result });
}).catch((err) => {
win.mainWindow?.webContents.send('install:error', { requestId, error: String(err) });
});
return { success: true, data: { requestId }, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
}
-151
View File
@@ -1,151 +0,0 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.registerInstanceHandlers = registerInstanceHandlers;
const electron_1 = __importDefault(require("electron"));
const instance_1 = require("../core/instance");
const { ipcMain } = electron_1.default;
function registerInstanceHandlers(win) {
ipcMain.handle('instance:create', async (_event, payload) => {
try {
const data = await (0, instance_1.createInstance)(payload.name, payload.gamePath, payload.runtime, {
author: payload.author,
description: payload.description,
java: payload.java,
minMemory: payload.minMemory,
maxMemory: payload.maxMemory,
vmOptions: payload.vmOptions,
mcOptions: payload.mcOptions,
});
return { success: true, data, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('instance:list', async (_event, payload) => {
try {
const data = await (0, instance_1.listInstances)(payload.gamePath);
return { success: true, data, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('instance:info', async (_event, payload) => {
try {
const data = await (0, instance_1.getInstanceInfo)(payload.name, payload.gamePath);
return { success: true, data, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('instance:delete', async (_event, payload) => {
try {
const data = await (0, instance_1.deleteInstance)(payload.name, payload.gamePath);
return { success: true, data, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('instance:update', async (_event, payload) => {
try {
const data = await (0, instance_1.updateInstance)(payload.name, payload.gamePath, payload.patch);
return { success: true, data, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('instance:install', async (_event, payload) => {
try {
const requestId = `install-${Date.now()}`;
(0, instance_1.installInstanceGame)(payload.name, payload.gamePath, {
onProgress: (progress) => {
win.mainWindow?.webContents.send('instance:progress', { requestId, ...progress });
},
}).then((data) => {
win.mainWindow?.webContents.send('instance:install-complete', { requestId, data });
}).catch((err) => {
win.mainWindow?.webContents.send('instance:install-error', { requestId, error: String(err) });
});
return { success: true, data: { requestId }, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('instance:launch', async (_event, payload) => {
try {
const requestId = `launch-${Date.now()}`;
(0, instance_1.launchInstance)(payload.name, payload.gamePath, {
username: payload.username,
uuid: payload.uuid,
accessToken: payload.accessToken,
javaPath: payload.javaPath,
server: payload.server,
onEvent: (event) => {
win.mainWindow?.webContents.send('instance:launch-event', { requestId, ...event });
},
}).then((data) => {
win.mainWindow?.webContents.send('instance:launch-complete', { requestId, data });
}).catch((err) => {
win.mainWindow?.webContents.send('instance:launch-error', { requestId, error: String(err) });
});
return { success: true, data: { requestId }, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('instance:diagnose', async (_event, payload) => {
try {
const data = await (0, instance_1.diagnoseInstance)(payload.name, payload.gamePath);
return { success: true, data, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
// Version list APIs
ipcMain.handle('instance:version-list', async (_event, payload) => {
try {
const data = await (0, instance_1.getMinecraftVersionList)(payload.type);
return { success: true, data, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('instance:forge-version-list', async (_event, payload) => {
try {
const data = await (0, instance_1.getForgeVersionList)(payload.mcVersion);
return { success: true, data, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('instance:fabric-version-list', async (_event, payload) => {
try {
const data = await (0, instance_1.getFabricVersionList)(payload.mcVersion);
return { success: true, data, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('instance:quilt-version-list', async (_event, payload) => {
try {
const data = await (0, instance_1.getQuiltVersionList)(payload.mcVersion);
return { success: true, data, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
}
+11
View File
@@ -12,6 +12,7 @@ import {
getForgeVersionList,
getFabricVersionList,
getQuiltVersionList,
scanGameDirectories,
type InstanceRuntime,
type InstanceConfig,
} from '../core/instance';
@@ -194,4 +195,14 @@ export function registerInstanceHandlers(win: WinRef) {
return { success: false, data: null, error: String(e) };
}
});
// 扫描游戏目录中的已安装版本
ipcMain.handle('instance:scan-dir', async (_event, payload: { gamePath: string }) => {
try {
const versions = scanGameDirectories(payload.gamePath);
return { success: true, data: { versions }, error: null };
} catch (e: unknown) {
return { success: false, data: null, error: String(e) };
}
});
}
-113
View File
@@ -1,113 +0,0 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.registerKoringAuthHandlers = registerKoringAuthHandlers;
const electron_1 = __importDefault(require("electron"));
const { ipcMain } = electron_1.default;
const koring_auth_1 = require("../core/koring-auth");
const config_1 = require("../config");
function registerKoringAuthHandlers() {
ipcMain.handle('koring-auth:request-device-code', async () => {
try {
const result = await (0, koring_auth_1.requestDeviceCode)();
return { success: true, data: result, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('koring-auth:poll-token', async (_event, deviceCode) => {
try {
const result = await (0, koring_auth_1.pollForTokenOnce)(deviceCode);
const user = (0, koring_auth_1.saveKoringAuth)(result);
// 同时写入配置文件
try {
const config = (0, config_1.loadConfig)();
config.koringUser = {
sub: user.sub,
name: user.name,
username: user.username,
email: user.email,
picture: user.picture,
accessToken: result.access_token,
refreshToken: result.refresh_token,
};
(0, config_1.saveConfig)(config);
}
catch (e) {
console.error('[koring-auth] failed to save user to config:', e);
}
return { success: true, data: { user }, error: null };
}
catch (e) {
const msg = e instanceof Error ? e.message : String(e);
return { success: false, data: null, error: msg };
}
});
ipcMain.handle('koring-auth:refresh', async () => {
try {
const stored = (0, koring_auth_1.readKoringAuth)();
if (!stored?.refresh_token)
throw new Error('No refresh token');
const result = await (0, koring_auth_1.refreshAccessToken)(stored.refresh_token);
const user = (0, koring_auth_1.saveKoringAuth)(result);
// 同步到配置文件
try {
const config = (0, config_1.loadConfig)();
config.koringUser = {
sub: user.sub,
name: user.name,
username: user.username,
email: user.email,
picture: user.picture,
accessToken: result.access_token,
refreshToken: result.refresh_token,
};
(0, config_1.saveConfig)(config);
}
catch { }
return { success: true, data: { user }, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('koring-auth:get-user', () => {
try {
const stored = (0, koring_auth_1.readKoringAuth)();
// 也从配置文件读取
if (!stored?.user?.sub) {
try {
const config = (0, config_1.loadConfig)();
const ku = config.koringUser;
if (ku?.sub) {
return { success: true, data: { user: ku, access_token: '', refresh_token: '', id_token: '', expires_at: 0 }, error: null };
}
}
catch { }
}
return { success: true, data: stored, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('koring-auth:logout', () => {
try {
(0, koring_auth_1.deleteKoringAuth)();
// 清除配置文件中的用户数据
try {
const config = (0, config_1.loadConfig)();
delete config.koringUser;
(0, config_1.saveConfig)(config);
}
catch { }
return { success: true, data: null, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
}
-45
View File
@@ -1,45 +0,0 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.registerLaunchHandlers = registerLaunchHandlers;
const electron_1 = __importDefault(require("electron"));
const launcher_1 = require("../core/launcher");
const { ipcMain } = electron_1.default;
function registerLaunchHandlers(win) {
ipcMain.handle('launch:launch', async (_event, payload) => {
try {
const requestId = `launch-${Date.now()}`;
const result = await (0, launcher_1.launchMinecraft)({
gamePath: payload.gamePath,
javaPath: payload.javaPath,
version: payload.version,
username: payload.username,
uuid: payload.uuid,
accessToken: payload.accessToken,
memory: payload.memory,
jvmArgs: payload.jvmArgs,
gameArgs: payload.gameArgs,
server: payload.server,
detached: payload.detached,
onEvent: (event) => {
win.mainWindow?.webContents.send('launch:event', { requestId, ...event });
},
});
return { success: true, data: { ...result, requestId }, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('launch:diagnose', async (_event, payload) => {
try {
const data = await (0, launcher_1.diagnoseVersion)(payload.gamePath, payload.version);
return { success: true, data, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
}
-47
View File
@@ -1,47 +0,0 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.registerModsHandlers = registerModsHandlers;
const electron_1 = __importDefault(require("electron"));
const modrinth_1 = require("../core/modrinth");
const { ipcMain } = electron_1.default;
function registerModsHandlers() {
ipcMain.handle('mods:search', async (_event, payload) => {
try {
const data = await (0, modrinth_1.searchMods)(payload.query, payload.gameVersion, payload.loader, payload.limit, payload.offset, payload.source);
return { success: true, data, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('mods:detail', async (_event, payload) => {
try {
const data = await (0, modrinth_1.getModDetail)(payload.projectId, payload.source);
return { success: true, data, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('mods:versions', async (_event, payload) => {
try {
const data = await (0, modrinth_1.getModVersions)(payload.projectId, payload.gameVersion, payload.loader, payload.source);
return { success: true, data, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('mods:install', async (_event, payload) => {
try {
const data = await (0, modrinth_1.installMod)(payload.projectId, payload.versionId, payload.gamePath, payload.source);
return { success: true, data, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
}
+23 -1
View File
@@ -1,5 +1,5 @@
import electron from 'electron';
import { searchMods, getModDetail, getModVersions, installMod } from '../core/modrinth';
import { searchMods, getModDetail, getModVersions, installMod, getCategories, getGameVersions } from '../core/modrinth';
const { ipcMain } = electron;
@@ -8,6 +8,8 @@ export function registerModsHandlers() {
query?: string;
gameVersion?: string;
loader?: string;
category?: string;
projectType?: string;
limit?: number;
offset?: number;
source: string;
@@ -17,6 +19,8 @@ export function registerModsHandlers() {
payload.query,
payload.gameVersion,
payload.loader,
payload.category,
payload.projectType,
payload.limit,
payload.offset,
payload.source as 'modrinth' | 'curseforge'
@@ -27,6 +31,24 @@ export function registerModsHandlers() {
}
});
ipcMain.handle('mods:categories', async (_event, payload: { projectType?: string }) => {
try {
const data = await getCategories(payload.projectType);
return { success: true, data, error: null };
} catch (e: unknown) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('mods:game-versions', async () => {
try {
const data = await getGameVersions();
return { success: true, data, error: null };
} catch (e: unknown) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('mods:detail', async (_event, payload: { projectId: string; source: string }) => {
try {
const data = await getModDetail(
-70
View File
@@ -1,70 +0,0 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.registerSystemHandlers = registerSystemHandlers;
const electron_1 = __importDefault(require("electron"));
const child_process_1 = require("child_process");
const { ipcMain, app } = electron_1.default;
function getBiosId() {
if (process.platform !== 'win32')
return 'N/A (non-Windows)';
try {
const output = (0, child_process_1.execSync)('powershell -NoProfile -Command "(Get-CimInstance Win32_BIOS).SerialNumber"', {
encoding: 'utf-8',
timeout: 5000,
});
return output.trim() || 'Unknown';
}
catch {
return 'Unknown';
}
}
function getOsVersion() {
if (process.platform !== 'win32')
return 'N/A (non-Windows)';
try {
const output = (0, child_process_1.execSync)('powershell -NoProfile -Command "[System.Environment]::OSVersion.VersionString"', {
encoding: 'utf-8',
timeout: 5000,
});
return output.trim() || 'Unknown';
}
catch {
return 'Unknown';
}
}
function getOsName() {
if (process.platform !== 'win32')
return 'N/A (non-Windows)';
try {
const output = (0, child_process_1.execSync)('powershell -NoProfile -Command "(Get-CimInstance Win32_OperatingSystem).Caption"', {
encoding: 'utf-8',
timeout: 5000,
});
return output.trim() || 'Unknown';
}
catch {
return 'Unknown';
}
}
function registerSystemHandlers() {
ipcMain.handle('system:info', () => {
try {
return {
success: true,
data: {
app_version: app.getVersion(),
bios_id: getBiosId(),
os_version: getOsVersion(),
os_name: getOsName(),
},
error: null,
};
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
}
+11 -1
View File
@@ -2,7 +2,7 @@ import electron from 'electron';
import { execSync } from 'child_process';
import * as os from 'os';
const { ipcMain, app } = electron;
const { ipcMain, app, shell } = electron;
function getBiosId(): string {
if (process.platform !== 'win32') return 'N/A (non-Windows)';
@@ -60,4 +60,14 @@ export function registerSystemHandlers() {
return { success: false, data: null, error: String(e) };
}
});
// 在系统文件管理器中打开指定路径(用于"打开游戏目录"等操作)
ipcMain.handle('system:open-path', async (_event, payload: { path: string }) => {
try {
const error = await shell.openPath(payload.path);
return { success: !error, data: { error }, error: error || null };
} catch (e: unknown) {
return { success: false, data: null, error: String(e) };
}
});
}
-61
View File
@@ -1,61 +0,0 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.registerTaskHandlers = registerTaskHandlers;
const electron_1 = __importDefault(require("electron"));
const { ipcMain } = electron_1.default;
const runningTasks = new Map();
function registerTaskHandlers(win) {
ipcMain.handle('task:start', async (_event, payload) => {
try {
const controller = new AbortController();
runningTasks.set(payload.taskId, controller);
win.mainWindow?.webContents.send('task:started', {
taskId: payload.taskId,
xmclPath: payload.executorName,
});
// Simulate task execution
const steps = 20;
for (let i = 0; i <= steps; i++) {
if (controller.signal.aborted) {
win.mainWindow?.webContents.send('task:failed', {
taskId: payload.taskId,
error: 'Task cancelled',
});
runningTasks.delete(payload.taskId);
return { success: true, data: null, error: null };
}
win.mainWindow?.webContents.send('task:progress', {
taskId: payload.taskId,
current: i,
total: steps,
stage: `步骤 ${i}/${steps}`,
});
await new Promise((r) => setTimeout(r, 150));
}
runningTasks.delete(payload.taskId);
win.mainWindow?.webContents.send('task:completed', {
taskId: payload.taskId,
});
return { success: true, data: null, error: null };
}
catch (e) {
runningTasks.delete(payload.taskId);
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('task:cancel', async (_event, payload) => {
try {
const controller = runningTasks.get(payload.taskId);
if (controller) {
controller.abort();
}
return { success: true, data: null, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
}
+55 -20
View File
@@ -1,4 +1,5 @@
import electron from 'electron';
import { createInstance, installInstanceGame, type InstanceRuntime } from '../core/instance';
const { ipcMain } = electron;
@@ -8,6 +9,14 @@ interface WinRef {
mainWindow: electron.BrowserWindow | null;
}
// 在任务中发送进度事件到渲染进程
function emitProgress(win: WinRef, taskId: string, current: number, total: number, stage: string) {
win.mainWindow?.webContents.send('task:progress', { taskId, current, total, stage, event: 'task:progress' });
}
function emitLog(win: WinRef, taskId: string, level: string, message: string) {
win.mainWindow?.webContents.send('task:progress', { taskId, event: 'task:log', level, message });
}
export function registerTaskHandlers(win: WinRef) {
ipcMain.handle('task:start', async (_event, payload: {
taskId: string;
@@ -21,42 +30,68 @@ export function registerTaskHandlers(win: WinRef) {
const controller = new AbortController();
runningTasks.set(payload.taskId, controller);
win.mainWindow?.webContents.send('task:started', {
win.mainWindow?.webContents.send('task:progress', {
taskId: payload.taskId,
event: 'task:started',
xmclPath: payload.executorName,
});
// Simulate task execution
const steps = 20;
for (let i = 0; i <= steps; i++) {
if (controller.signal.aborted) {
win.mainWindow?.webContents.send('task:failed', {
taskId: payload.taskId,
error: 'Task cancelled',
});
runningTasks.delete(payload.taskId);
return { success: true, data: null, error: null };
}
// 根据 executorName 选择处理逻辑
if (payload.executorName === 'install') {
// 使用 @xmcl/core + @xmcl/installer 安装 Minecraft 实例
const params = payload.params ?? {};
const name = String(params.name ?? `mc-${Date.now()}`);
const gamePath = String(params.gamePath ?? '.minecraft');
const runtime = (params.runtime ?? { minecraft: '1.21.4' }) as InstanceRuntime;
const description = String(params.description ?? '');
win.mainWindow?.webContents.send('task:progress', {
taskId: payload.taskId,
current: i,
total: steps,
stage: `步骤 ${i}/${steps}`,
emitProgress(win, payload.taskId, 0, 100, '创建实例目录');
emitLog(win, payload.taskId, 'info', `正在创建实例: ${name}`);
// Step 1: 创建实例(写入 instance.json
await createInstance(name, gamePath, runtime, { description });
emitProgress(win, payload.taskId, 10, 100, '实例目录已创建');
// Step 2: 安装游戏文件(@xmcl/installer
emitLog(win, payload.taskId, 'info', `正在下载 Minecraft ${runtime.minecraft}...`);
await installInstanceGame(name, gamePath, {
onProgress: (progress) => {
if (controller.signal.aborted) return;
const mapped = Math.round(10 + (progress.current / Math.max(progress.total, 1)) * 80);
emitProgress(win, payload.taskId, mapped, 100, progress.stage || '安装中');
if (progress.message) emitLog(win, payload.taskId, 'info', progress.message);
},
});
await new Promise((r) => setTimeout(r, 150));
emitProgress(win, payload.taskId, 100, 100, '安装完成');
emitLog(win, payload.taskId, 'info', `实例「${name}」创建完成`);
} else {
// 默认模拟执行
const steps = 20;
for (let i = 0; i <= steps; i++) {
if (controller.signal.aborted) throw new Error('已取消');
emitProgress(win, payload.taskId, i, steps, `步骤 ${i}/${steps}`);
await new Promise((r) => setTimeout(r, 150));
}
emitLog(win, payload.taskId, 'info', '任务完成');
}
runningTasks.delete(payload.taskId);
win.mainWindow?.webContents.send('task:completed', {
win.mainWindow?.webContents.send('task:progress', {
taskId: payload.taskId,
event: 'task:completed',
});
return { success: true, data: null, error: null };
} catch (e: unknown) {
runningTasks.delete(payload.taskId);
return { success: false, data: null, error: String(e) };
const errMsg = e instanceof Error ? e.message : String(e);
win.mainWindow?.webContents.send('task:progress', {
taskId: payload.taskId,
event: 'task:failed',
error: errMsg,
});
return { success: false, data: null, error: errMsg };
}
});
-95
View File
@@ -1,95 +0,0 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.registerWindowHandlers = registerWindowHandlers;
const electron_1 = __importDefault(require("electron"));
const path_1 = __importDefault(require("path"));
const { ipcMain, dialog, shell } = electron_1.default;
const isDev = !electron_1.default.app.isPackaged;
const MIME_MAP = {
png: 'image/png',
jpg: 'image/jpeg',
jpeg: 'image/jpeg',
webp: 'image/webp',
gif: 'image/gif',
bmp: 'image/bmp',
svg: 'image/svg+xml',
};
function createSplashWindow() {
const splash = new electron_1.default.BrowserWindow({
width: 480,
height: 320,
transparent: true,
frame: false,
resizable: false,
skipTaskbar: true,
alwaysOnTop: true,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
},
});
if (isDev) {
splash.loadURL('http://localhost:1420/splash.html');
}
else {
splash.loadFile(path_1.default.join(__dirname, '../dist/splash.html'));
}
return splash;
}
function registerWindowHandlers(win) {
ipcMain.handle('window:minimize', () => {
win.mainWindow?.minimize();
});
ipcMain.handle('window:maximize', () => {
if (win.mainWindow?.isMaximized()) {
win.mainWindow.unmaximize();
}
else {
win.mainWindow?.maximize();
}
});
ipcMain.handle('window:close', () => {
win.mainWindow?.close();
});
ipcMain.handle('window:isMaximized', () => {
return win.mainWindow?.isMaximized() ?? false;
});
ipcMain.handle('window:getTheme', () => {
return win.mainWindow?.themeSource ?? null;
});
// Splash window management
ipcMain.handle('window:openSplash', () => {
if (win.splashWindow && !win.splashWindow.isDestroyed()) {
win.splashWindow.focus();
return { success: true };
}
win.splashWindow = createSplashWindow();
return { success: true };
});
ipcMain.handle('window:closeSplash', () => {
if (win.splashWindow && !win.splashWindow.isDestroyed()) {
win.splashWindow.close();
win.splashWindow = null;
}
return { success: true };
});
// File dialog — returns source path and extension for preload to handle
ipcMain.handle('dialog:openFile', async (_event, payload) => {
const result = await dialog.showOpenDialog(win.mainWindow, {
properties: ['openFile'],
filters: payload.filters,
});
if (result.canceled || result.filePaths.length === 0)
return null;
const srcPath = result.filePaths[0];
const ext = path_1.default.extname(srcPath).toLowerCase() || '.png';
return { srcPath, ext };
});
// Open external URL in system browser
ipcMain.handle('shell:openExternal', async (_event, url) => {
await shell.openExternal(url);
});
}
+9
View File
@@ -105,4 +105,13 @@ export function registerWindowHandlers(win: WinRef) {
ipcMain.handle('shell:openExternal', async (_event, url: string) => {
await shell.openExternal(url);
});
// Folder dialog — select a directory
ipcMain.handle('dialog:openFolder', async () => {
const result = await dialog.showOpenDialog(win.mainWindow!, {
properties: ['openDirectory'],
});
if (result.canceled || result.filePaths.length === 0) return null;
return { folderPath: result.filePaths[0] };
});
}
+13 -6
View File
@@ -53,10 +53,19 @@ function runStartupChecks(): { isFirstLaunch: boolean; config: ReturnType<typeof
return { isFirstLaunch, config };
}
// 根据调试/运行状态自动选择窗口图标:
// - 开发调试(未打包):直接使用 public/icons/dev/ 下的 dev 图标
// - 打包运行:electron-builder 构建时 switch-icon 已把对应模式图标复制到 build/
function getWindowIconPath(): string | undefined {
if (!app.isPackaged) {
return path.join(__dirname, '../public/icons/dev/icon.ico');
}
const packagedIcon = path.join(__dirname, '../build/icon.ico');
return fs.existsSync(packagedIcon) ? packagedIcon : undefined;
}
function createSplashWindow(): electron.BrowserWindow {
const iconPath = isDev
? path.join(__dirname, '../build/icon.ico')
: path.join(__dirname, '../build/icon.ico');
const iconPath = getWindowIconPath();
const splash = new electron.BrowserWindow({
width: 480,
@@ -83,9 +92,7 @@ function createSplashWindow(): electron.BrowserWindow {
}
function createMainWindow(): electron.BrowserWindow {
const iconPath = isDev
? path.join(__dirname, '../build/icon.ico')
: path.join(__dirname, '../build/icon.ico');
const iconPath = getWindowIconPath();
const main = new electron.BrowserWindow({
width: 1000,