Initial clean project import

This commit is contained in:
cat-shark
2026-06-19 18:41:41 +08:00
commit a13b804c7a
1306 changed files with 220568 additions and 0 deletions
+292
View File
@@ -0,0 +1,292 @@
/**
* Electron 防破解模块
* 禁用开发者工具、快捷键、右键菜单
*/
import { BrowserWindow, globalShortcut, Menu } from 'electron';
import { isDev } from './env';
export class AntiDebugManager {
private static instance: AntiDebugManager;
private disabledShortcuts: string[] = [];
private originalOpenDevTools: Map<BrowserWindow, Function> = new Map();
private originalToggleDevTools: Map<BrowserWindow, Function> = new Map();
private constructor() {
console.log('[AntiDebugManager] 初始化');
}
/**
* 获取单例实例
*/
static getInstance(): AntiDebugManager {
if (!AntiDebugManager.instance) {
AntiDebugManager.instance = new AntiDebugManager();
}
return AntiDebugManager.instance;
}
/**
* 禁用开发者工具
* @param window 目标窗口
*/
disableDevTools(window: BrowserWindow): void {
try {
// 💾 保存原始方法,以便后续恢复
this.originalOpenDevTools.set(window, window.webContents.openDevTools.bind(window.webContents));
this.originalToggleDevTools.set(window, window.webContents.toggleDevTools.bind(window.webContents));
// 禁用 openDevTools 方法
window.webContents.openDevTools = () => {
console.warn('[AntiDebugManager] 尝试打开开发者工具被拦截');
};
// 禁用 toggleDevTools 方法
window.webContents.toggleDevTools = () => {
console.warn('[AntiDebugManager] 尝试切换开发者工具被拦截');
};
console.log('[AntiDebugManager] ✅ 已禁用开发者工具');
} catch (error) {
console.error('[AntiDebugManager] 禁用开发者工具失败:', error);
}
}
/**
* 禁用快捷键
* 包括:F12、Ctrl+Shift+I、Ctrl+Shift+C、Ctrl+Shift+J、Ctrl+I 等
*/
disableDebugShortcuts(): void {
try {
const shortcuts = [
'F12', // 开发者工具
'Ctrl+Shift+I', // 开发者工具 (Windows/Linux)
'Cmd+Option+I', // 开发者工具 (macOS)
'Ctrl+Shift+C', // 检查元素
'Cmd+Option+C', // 检查元素 (macOS)
'Ctrl+Shift+J', // 控制台
'Cmd+Option+J', // 控制台 (macOS)
'Ctrl+I', // 开发者工具 (某些浏览器)
'Cmd+Option+U', // 查看源代码 (macOS)
'Ctrl+U', // 查看源代码 (Windows/Linux)
'Ctrl+Shift+K', // 控制台 (某些浏览器)
'Ctrl+Shift+D', // 某些调试快捷键
];
shortcuts.forEach(shortcut => {
try {
globalShortcut.register(shortcut, () => {
console.warn(`[AntiDebugManager] 快捷键 "${shortcut}" 被拦截`);
return true; // 阻止默认行为
});
this.disabledShortcuts.push(shortcut);
} catch (error) {
console.warn(`[AntiDebugManager] 注册快捷键 "${shortcut}" 失败:`, error);
}
});
console.log(`[AntiDebugManager] ✅ 已禁用 ${this.disabledShortcuts.length} 个调试快捷键`);
} catch (error) {
console.error('[AntiDebugManager] 禁用快捷键失败:', error);
}
}
/**
* 禁用右键菜单
* @param window 目标窗口
*/
disableContextMenu(window: BrowserWindow): void {
try {
window.webContents.on('context-menu', (e) => {
e.preventDefault();
console.warn('[AntiDebugManager] 右键菜单被拦截');
});
console.log('[AntiDebugManager] ✅ 已禁用右键菜单');
} catch (error) {
console.error('[AntiDebugManager] 禁用右键菜单失败:', error);
}
}
/**
* 禁用鼠标右键
* @param window 目标窗口
*/
disableMouseRightClick(window: BrowserWindow): void {
try {
window.webContents.on('before-input-event', (event, input) => {
// 检查是否是鼠标右键(某些情况下)
if (input.type === 'mouseUp' && input.button === 2) {
event.preventDefault();
}
});
console.log('[AntiDebugManager] ✅ 已禁用鼠标右键');
} catch (error) {
console.error('[AntiDebugManager] 禁用鼠标右键失败:', error);
}
}
/**
* 拦截快捷键(键盘事件)
* @param window 目标窗口
*/
interceptDebugHotkeys(window: BrowserWindow): void {
try {
window.webContents.on('before-input-event', (event, input) => {
// 拦截 F12
if (input.key.toLowerCase() === 'f12') {
event.preventDefault();
console.warn('[AntiDebugManager] F12 被拦截');
return;
}
// 拦截 Ctrl+Shift+I (Windows/Linux)
if (input.control && input.shift && input.key.toLowerCase() === 'i') {
event.preventDefault();
console.warn('[AntiDebugManager] Ctrl+Shift+I 被拦截');
return;
}
// 拦截 Ctrl+Shift+C (Windows/Linux)
if (input.control && input.shift && input.key.toLowerCase() === 'c') {
event.preventDefault();
console.warn('[AntiDebugManager] Ctrl+Shift+C 被拦截');
return;
}
// 拦截 Ctrl+Shift+J (Windows/Linux)
if (input.control && input.shift && input.key.toLowerCase() === 'j') {
event.preventDefault();
console.warn('[AntiDebugManager] Ctrl+Shift+J 被拦截');
return;
}
// 拦截 Cmd+Option+I (macOS)
if (input.meta && input.alt && input.key.toLowerCase() === 'i') {
event.preventDefault();
console.warn('[AntiDebugManager] Cmd+Option+I 被拦截');
return;
}
// 拦截 Cmd+Option+C (macOS)
if (input.meta && input.alt && input.key.toLowerCase() === 'c') {
event.preventDefault();
console.warn('[AntiDebugManager] Cmd+Option+C 被拦截');
return;
}
// 拦截 Ctrl+U (查看源代码)
if (input.control && input.key.toLowerCase() === 'u') {
event.preventDefault();
console.warn('[AntiDebugManager] Ctrl+U 被拦截');
return;
}
});
console.log('[AntiDebugManager] ✅ 已拦截调试快捷键');
} catch (error) {
console.error('[AntiDebugManager] 快捷键拦截失败:', error);
}
}
/**
* 禁用菜单栏(可选)
* @param window 目标窗口
*/
disableMenuBar(window: BrowserWindow): void {
try {
Menu.setApplicationMenu(null);
console.log('[AntiDebugManager] ✅ 已禁用菜单栏');
} catch (error) {
console.error('[AntiDebugManager] 禁用菜单栏失败:', error);
}
}
/**
* 完整的防破解方案
* @param window 目标窗口
* @param options 选项
*/
enableFullProtection(window: BrowserWindow, options: {
disableDevTools?: boolean;
disableShortcuts?: boolean;
disableContextMenu?: boolean;
disableMenuBar?: boolean;
interceptHotkeys?: boolean;
} = {}): void {
const {
disableDevTools = true,
disableShortcuts = true,
disableContextMenu = true,
disableMenuBar = true,
interceptHotkeys = true
} = options;
console.log('[AntiDebugManager] 启用完整防破解保护...');
console.log('[AntiDebugManager] 环境:', isDev ? '开发' : '生产');
if (disableDevTools) {
this.disableDevTools(window);
}
if (disableShortcuts) {
this.disableDebugShortcuts();
}
if (disableContextMenu) {
this.disableContextMenu(window);
}
if (disableMenuBar) {
this.disableMenuBar(window);
}
if (interceptHotkeys) {
this.interceptDebugHotkeys(window);
}
console.log('[AntiDebugManager] ✅ 防破解保护已启用');
}
/**
* 临时启用开发者工具(用于隐藏调试快捷键)
* @param window 目标窗口
*/
restoreDevTools(window: BrowserWindow): void {
try {
const originalMethod = this.originalOpenDevTools.get(window);
if (originalMethod && typeof originalMethod === 'function') {
console.log('[AntiDebugManager] 使用保存的原始 openDevTools 方法打开 DevTools');
// 调用保存的原始方法
originalMethod({ mode: 'detach', activate: true });
console.log('[AntiDebugManager] ✅ DevTools 已通过原始方法打开');
} else {
console.warn('[AntiDebugManager] 未找到保存的原始方法,尝试直接调用');
// 备选方案:直接调用(可能被拦截,但尽量尝试)
window.webContents.openDevTools({ mode: 'detach', activate: true });
}
} catch (error) {
console.error('[AntiDebugManager] 恢复 DevTools 失败:', error);
}
}
/**
* 卸载防破解(开发时使用)
*/
unload(): void {
try {
// 注销所有快捷键
this.disabledShortcuts.forEach(shortcut => {
globalShortcut.unregister(shortcut);
});
this.disabledShortcuts = [];
console.log('[AntiDebugManager] ✅ 防破解已卸载');
} catch (error) {
console.error('[AntiDebugManager] 卸载防破解失败:', error);
}
}
}
export default AntiDebugManager.getInstance();
+25
View File
@@ -0,0 +1,25 @@
import Apps from "../mapi/app";
export type ResultType<T> = {
// should follow the rules:
// <0 business error
// =0 success
// 10000 error ( network error, server error, etc. )
code: number;
msg: string;
data?: T;
};
export const post = async (url: string, data: any) => {
data = data || {};
const userAgent = Apps.getUserAgent();
data["AppManagerUserAgent"] = userAgent;
return await fetch(url, {
method: "POST",
headers: {
"User-Agent": userAgent,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
};
+268
View File
@@ -0,0 +1,268 @@
/**
* 浏览器路径工具
* 统一管理所有模块的浏览器可执行文件路径
* 支持自定义安装路径和离线使用
*/
import { app } from 'electron';
import path from 'path';
import fs from 'fs';
import { AppEnv } from '../mapi/env';
import { getRuntimeBundlePath } from './resource-path';
/**
* 获取Playwright Chromium浏览器可执行文件路径
* 优先使用打包的浏览器,支持自定义安装路径
*/
function getPlatformChromePaths(): string[] {
if (process.platform === 'win32') {
return ['chrome-win64/chrome.exe', 'chrome-win/chrome.exe'];
} else if (process.platform === 'darwin') {
return [
'chrome-mac/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing',
'chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing',
'chrome-mac/Chromium.app/Contents/MacOS/Chromium',
'chrome-mac/chrome',
];
} else {
return ['chrome-linux/chrome'];
}
}
function getPlatformCacheBase(): string[] {
const runtimeCandidates = [
getRuntimeBundlePath('playwright'),
AppEnv.resourceBundleRoot ? path.join(AppEnv.resourceBundleRoot, 'playwright') : '',
AppEnv.dataRoot ? path.join(AppEnv.dataRoot, 'playwright') : '',
].filter(Boolean);
if (process.platform === 'win32') {
return [
...runtimeCandidates,
path.join(process.env.LOCALAPPDATA || '', 'ms-playwright'),
path.join(process.env.APPDATA || '', 'ms-playwright'),
];
} else if (process.platform === 'darwin') {
return [
...runtimeCandidates,
path.join(process.env.HOME || '', 'Library/Caches/ms-playwright'),
];
} else {
return [
...runtimeCandidates,
path.join(process.env.HOME || '', '.cache/ms-playwright'),
];
}
}
export function getPlaywrightChromiumPath(): string | undefined {
const runtimePath = getRuntimeBundlePath('playwright');
const basePaths = [
runtimePath,
path.join(process.resourcesPath || app.getAppPath(), 'extra/common/playwright'),
path.join(app.getAppPath(), 'resources/extra/common/playwright'),
path.join(app.getAppPath(), 'electron/resources/extra/common/playwright'),
].filter(Boolean);
const chromiumVersions = ['chromium-1200'];
const chromePaths = getPlatformChromePaths();
for (const basePath of basePaths) {
for (const version of chromiumVersions) {
for (const chromePath of chromePaths) {
const browserPath = path.join(basePath, version, chromePath);
if (fs.existsSync(browserPath)) {
console.log('✅ [Browser] 使用打包的浏览器:', browserPath);
return browserPath;
}
}
}
}
const playwrightCacheBase = getPlatformCacheBase();
// 也尝试查找任何可用的 chromium 版本(仅用于开发环境回退)
for (const cacheBase of playwrightCacheBase) {
if (fs.existsSync(cacheBase)) {
const allDirs = fs.readdirSync(cacheBase);
const chromiumDirs = allDirs.filter(d => d.startsWith('chromium-')).sort().reverse();
for (const version of chromiumDirs) {
for (const chromePath of chromePaths) {
const browserPath = path.join(cacheBase, version, chromePath);
if (fs.existsSync(browserPath)) {
console.log('⚠️ [Browser] 使用系统缓存的浏览器:', browserPath);
return browserPath;
}
}
}
}
}
console.log('⚠️ [Browser] 警告:未找到Playwright浏览器');
return undefined;
}
/**
* 获取用户数据目录(用于持久化浏览器数据)
*/
export function getBrowserUserDataDir(contextName: string): string {
return path.join(app.getPath('userData'), 'browser-data', contextName);
}
/**
* 通用的浏览器启动参数
* 参考 aigc-human 项目的反爬和性能优化配置
*/
export const COMMON_BROWSER_ARGS = [
// === 核心反爬与沙箱配置 ===
'--disable-blink-features=AutomationControlled',
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-infobars',
// === 启动速度优化 ===
'--no-first-run',
'--no-default-browser-check',
'--disable-breakpad', // 禁用崩溃报告
'--disable-component-update', // 禁用组件自动更新
'--disable-sync', // 禁用同步
'--disable-default-apps',
'--disable-popup-blocking',
// === 后台性能优化 (防止页面被浏览器通过节流策略降速) ===
'--disable-background-networking',
'--disable-background-timer-throttling',
'--disable-renderer-backgrounding',
'--disable-backgrounding-occluded-windows',
// === 媒体与渲染优化 (针对视频发布场景) ===
'--enable-gpu',
'--enable-webgl',
'--ignore-gpu-blacklist',
'--enable-accelerated-video-decode',
'--enable-gpu-rasterization',
'--enable-proprietary-codecs', // 启用H.264等编解码器
// === 自动播放策略 ===
'--autoplay-policy=no-user-gesture-required',
// === 安全 ===
'--disable-features=IsolateOrigins,site-per-process',
];
/**
* 应该忽略的默认启动参数
* 移除 Chromium 的自动化标识,避免被平台检测
*/
export const BROWSER_IGNORE_DEFAULT_ARGS = [
'--enable-automation',
];
/**
* 反检测注入脚本
* 在每个页面加载前执行,隐藏 Playwright/webdriver 痕迹
*/
export const STEALTH_INIT_SCRIPT = `
// 隐藏 webdriver 标识
Object.defineProperty(navigator, 'webdriver', {
get: () => undefined,
configurable: true
});
// 伪造 chrome.runtime(完善方法)
window.chrome = {
runtime: {
PlatformOs: { MAC: 'mac', WIN: 'win', ANDROID: 'android', CROS: 'cros', LINUX: 'linux', OPENBSD: 'openbsd' },
PlatformArch: { ARM: 'arm', X86_32: 'x86-32', X86_64: 'x86-64', MIPS: 'mips', MIPS64: 'mips64' },
PlatformNaclArch: { ARM: 'arm', X86_32: 'x86-32', X86_64: 'x86-64', MIPS: 'mips', MIPS64: 'mips64' },
RequestUpdateCheckStatus: { THROTTLED: 'throttled', NO_UPDATE: 'no_update', UPDATE_AVAILABLE: 'update_available' },
OnInstalledReason: { INSTALL: 'install', UPDATE: 'update', CHROME_UPDATE: 'chrome_update', SHARED_MODULE_UPDATE: 'shared_module_update' },
OnRestartRequiredReason: { APP_UPDATE: 'app_update', OS_UPDATE: 'os_update', PERIODIC: 'periodic' },
connect: function() { return { onDisconnect: { addListener: function() {} }, onMessage: { addListener: function() {} }, postMessage: function() {}, disconnect: function() {} }; },
sendMessage: function() {},
id: undefined
},
loadTimes: function() { return { firstPaintTime: 0, startLoadTime: 0, commitLoadTime: 0, finishDocumentLoadTime: 0, finishLoadTime: 0, firstPaintAfterLoadTime: 0, navigationType: 'Other', requestTime: Date.now() / 1000 }; },
csi: function() { return { startE: Date.now(), onloadT: Date.now(), pageT: Math.random() * 1000 + 500, tran: 15 }; },
app: { isInstalled: false, InstallState: { DISABLED: 'disabled', INSTALLED: 'installed', NOT_INSTALLED: 'not_installed' }, RunningState: { CANNOT_RUN: 'cannot_run', READY_TO_RUN: 'ready_to_run', RUNNING: 'running' } },
};
// 伪造 navigator.plugins
Object.defineProperty(navigator, 'plugins', {
get: () => {
return [{
name: 'Chrome PDF Plugin',
description: 'Portable Document Format',
filename: 'internal-pdf-viewer',
length: 1,
}, {
name: 'Chrome PDF Viewer',
description: '',
filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai',
length: 1,
}, {
name: 'Native Client',
description: '',
filename: 'internal-nacl-plugin',
length: 2,
}];
},
configurable: true
});
// 伪造 navigator.languages
Object.defineProperty(navigator, 'languages', {
get: () => ['zh-CN', 'zh', 'en'],
configurable: true
});
// 伪造 navigator.hardwareConcurrency
Object.defineProperty(navigator, 'hardwareConcurrency', {
get: () => 8,
configurable: true
});
// 伪造 navigator.connection
if (!navigator.connection) {
Object.defineProperty(navigator, 'connection', {
get: () => ({ effectiveType: '4g', rtt: 50, downlink: 10, saveData: false }),
configurable: true
});
}
// 伪造 navigator.getBattery
if (!navigator.getBattery) {
navigator.getBattery = () => Promise.resolve({
charging: true,
chargingTime: 0,
dischargingTime: Infinity,
level: 1,
addEventListener: function() {},
removeEventListener: function() {},
dispatchEvent: function() { return true; }
});
}
// 清理 Playwright 特征
delete window.__playwright;
delete window.__pw_manual;
// 清理自动化工具特征
delete window._REACT_DEVTOOLS_GLOBAL_HOOK__;
delete window.__selenium_unwrapped;
delete window.__webdriver_evaluate;
delete window.__selenium_evaluate;
delete window.__fxdriver_evaluate;
delete window.__driver_unwrapped;
delete window.__driver_evaluate;
delete window.__phantomas;
delete window.__nightmare;
delete window.callSelenium;
delete window._phantom;
delete window.callPhantom;
delete window.Buffer;
delete window.emit;
delete window.spawn;
delete window.webdriver;
`;
+281
View File
@@ -0,0 +1,281 @@
/**
* Electron 主进程加密模块
* 实现 RSA + AES 混合加密
* 不会被代码混淆破坏
*/
import crypto from 'crypto';
import fs from 'fs';
import path from 'path';
import { app } from 'electron';
export class CryptoManager {
private static instance: CryptoManager;
private publicKeyPath: string;
private privateKeyPath: string;
private publicKey: string | null = null;
private privateKey: string | null = null;
private constructor() {
// 密钥存储路径
const keyDir = path.join(app.getPath('userData'), 'keys');
if (!fs.existsSync(keyDir)) {
fs.mkdirSync(keyDir, { recursive: true });
}
this.publicKeyPath = path.join(keyDir, 'public.pem');
this.privateKeyPath = path.join(keyDir, 'private.pem');
console.log('[CryptoManager] 初始化,密钥路径:', this.publicKeyPath);
}
/**
* 获取单例实例
*/
static getInstance(): CryptoManager {
if (!CryptoManager.instance) {
CryptoManager.instance = new CryptoManager();
}
return CryptoManager.instance;
}
/**
* 初始化 RSA 密钥对
* 如果密钥不存在则生成新的
*/
async initializeKeys(): Promise<void> {
try {
// 检查密钥是否已存在
if (fs.existsSync(this.publicKeyPath) && fs.existsSync(this.privateKeyPath)) {
this.publicKey = fs.readFileSync(this.publicKeyPath, 'utf-8');
this.privateKey = fs.readFileSync(this.privateKeyPath, 'utf-8');
console.log('[CryptoManager] ✅ 已加载现有 RSA 密钥对');
return;
}
// 生成新的 RSA 密钥对
console.log('[CryptoManager] 生成新的 RSA 密钥对...');
const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
modulusLength: 2048,
publicKeyEncoding: {
type: 'spki',
format: 'pem'
},
privateKeyEncoding: {
type: 'pkcs8',
format: 'pem'
}
});
// 保存到文件
fs.writeFileSync(this.publicKeyPath, publicKey, 'utf-8');
fs.writeFileSync(this.privateKeyPath, privateKey, 'utf-8');
this.publicKey = publicKey;
this.privateKey = privateKey;
console.log('[CryptoManager] ✅ RSA 密钥对生成完成');
} catch (error) {
console.error('[CryptoManager] ❌ 密钥初始化失败:', error);
throw error;
}
}
/**
* 获取公钥(用于前端加密)
*/
getPublicKey(): string {
if (!this.publicKey) {
throw new Error('公钥未初始化,请先调用 initializeKeys()');
}
return this.publicKey;
}
/**
* RSA 加密(用公钥)
* @param data 要加密的数据
* @returns Base64 编码的密文
*/
encryptRSA(data: string): string {
try {
if (!this.publicKey) {
throw new Error('公钥未初始化');
}
const encrypted = crypto.publicEncrypt(
{
key: this.publicKey,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING
},
Buffer.from(data, 'utf-8')
);
return encrypted.toString('base64');
} catch (error) {
console.error('[CryptoManager] RSA 加密失败:', error);
throw error;
}
}
/**
* RSA 解密(用私钥)
* @param encryptedData Base64 编码的密文
* @returns 解密后的明文
*/
decryptRSA(encryptedData: string): string {
try {
if (!this.privateKey) {
throw new Error('私钥未初始化');
}
const decrypted = crypto.privateDecrypt(
{
key: this.privateKey,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING
},
Buffer.from(encryptedData, 'base64')
);
return decrypted.toString('utf-8');
} catch (error) {
console.error('[CryptoManager] RSA 解密失败:', error);
throw error;
}
}
/**
* AES-256-GCM 加密(改进版,替代 AES-128-ECB
* @param data 要加密的数据
* @param key 加密密钥(32字节/256位)
* @returns 包含 iv 和密文的 JSON 字符串(Base64 编码)
*/
encryptAES(data: string, key: string): string {
try {
// 生成随机 IV
const iv = crypto.randomBytes(16);
// 确保密钥长度为 32 字节
const keyBuffer = Buffer.alloc(32);
Buffer.from(key, 'utf-8').copy(keyBuffer);
// 创建加密器
const cipher = crypto.createCipheriv('aes-256-gcm', keyBuffer, iv);
// 加密数据
let encrypted = cipher.update(data, 'utf-8', 'hex');
encrypted += cipher.final('hex');
// 获取认证标签
const authTag = cipher.getAuthTag();
// 返回格式:{iv: string, encryptedData: string, authTag: string}
const result = {
iv: iv.toString('base64'),
encryptedData: encrypted,
authTag: authTag.toString('base64')
};
return Buffer.from(JSON.stringify(result)).toString('base64');
} catch (error) {
console.error('[CryptoManager] AES 加密失败:', error);
throw error;
}
}
/**
* AES-256-GCM 解密
* @param encryptedData Base64 编码的加密数据
* @param key 解密密钥(32字节/256位)
* @returns 解密后的明文
*/
decryptAES(encryptedData: string, key: string): string {
try {
// 解析加密数据
const result = JSON.parse(Buffer.from(encryptedData, 'base64').toString('utf-8'));
// 确保密钥长度为 32 字节
const keyBuffer = Buffer.alloc(32);
Buffer.from(key, 'utf-8').copy(keyBuffer);
// 创建解密器
const decipher = crypto.createDecipheriv(
'aes-256-gcm',
keyBuffer,
Buffer.from(result.iv, 'base64')
);
// 设置认证标签
decipher.setAuthTag(Buffer.from(result.authTag, 'base64'));
// 解密数据
let decrypted = decipher.update(result.encryptedData, 'hex', 'utf-8');
decrypted += decipher.final('utf-8');
return decrypted;
} catch (error) {
console.error('[CryptoManager] AES 解密失败:', error);
throw error;
}
}
/**
* SHA256 哈希(安全的哈希替代 MD5)
* @param data 要哈希的数据
* @returns 哈希值
*/
sha256(data: string): string {
return crypto.createHash('sha256').update(data).digest('hex');
}
/**
* 生成随机密钥
* @param length 密钥长度(字节数)
* @returns Base64 编码的随机密钥
*/
generateRandomKey(length: number = 32): string {
return crypto.randomBytes(length).toString('base64');
}
/**
* 签名(用私钥)
* @param data 要签名的数据
* @returns Base64 编码的签名
*/
sign(data: string): string {
try {
if (!this.privateKey) {
throw new Error('私钥未初始化');
}
const sign = crypto.createSign('sha256');
sign.update(data);
return sign.sign(this.privateKey, 'base64');
} catch (error) {
console.error('[CryptoManager] 签名失败:', error);
throw error;
}
}
/**
* 验证签名(用公钥)
* @param data 原始数据
* @param signature Base64 编码的签名
* @returns 签名是否有效
*/
verify(data: string, signature: string): boolean {
try {
if (!this.publicKey) {
throw new Error('公钥未初始化');
}
const verify = crypto.createVerify('sha256');
verify.update(data);
return verify.verify(this.publicKey, signature, 'base64');
} catch (error) {
console.error('[CryptoManager] 签名验证失败:', error);
return false;
}
}
}
export default CryptoManager.getInstance();
+101
View File
@@ -0,0 +1,101 @@
import {BrowserView, BrowserWindow, screen} from "electron";
import {isDev, shouldEnableDevTools} from "./env";
import {WindowConfig} from "../config/window";
export const DevToolsManager = {
enable: true,
rowCount: 4,
colCount: 3,
windows: new Map<BrowserWindow | BrowserView, BrowserWindow>(),
setEnable(enable: boolean) {
DevToolsManager.enable = enable;
},
getWindow(win: BrowserWindow | BrowserView) {
return this.windows.get(win);
},
getOrCreateWindow(name: string, win: BrowserWindow | BrowserView) {
if (this.windows.has(win)) {
return this.windows.get(win);
}
const {x, y, width, height} = this.getDisplayPosition();
// console.log('DevToolsManager', name, {x, y, width, height})
const devtools = new BrowserWindow({
show: true,
x,
y,
width,
height,
title: name,
});
devtools.on("closed", (e) => {
// console.log('DevToolsManager', 'close', name)
this.windows.delete(win);
})
// console.log('DevToolsManager', name, {x, y})
win.webContents.setDevToolsWebContents(devtools.webContents);
win.webContents.on("destroyed", () => {
// console.log('DevToolsManager', 'destroyed', name)
devtools.destroy();
});
devtools.webContents.on("dom-ready", () => {
setTimeout(() => {
if (!devtools.isDestroyed()) {
devtools.setTitle(name);
}
}, 1000);
});
this.windows.set(win, devtools);
return devtools;
},
getLargestDisplay(): Electron.Display {
const displays = screen.getAllDisplays();
return displays.reduce((max, display) => {
const {width, height} = display.size;
const maxResolution = max.size.width * max.size.height;
const currentResolution = width * height;
return currentResolution > maxResolution ? display : max;
});
},
getDisplayPosition(): {
x: number;
y: number;
width: number;
height: number;
} {
const display = this.getLargestDisplay();
const {x, y, width, height} = display.workArea;
// console.log('DevToolsManager', 'getDisplayPosition', {x, y, width, height})
if (width < 1300) {
this.rowCount = 3;
this.colCount = 2;
}
const itemWidth = Math.floor(width / this.rowCount);
const itemHeight = Math.floor(height / this.colCount);
const maxRow = Math.floor(width / itemWidth);
const row = this.windows.size % maxRow;
const col = Math.floor(this.windows.size / maxRow);
return {
x: x + row * itemWidth,
y: y + col * itemHeight,
width: itemWidth,
height: itemHeight,
};
},
register(name: string, win: BrowserWindow | BrowserView) {
if (!shouldEnableDevTools || !DevToolsManager.enable) {
return;
}
this.getOrCreateWindow(name, win);
},
autoShow(win: BrowserWindow | BrowserView) {
if (!shouldEnableDevTools || !DevToolsManager.enable) {
return;
}
if (WindowConfig.alwaysOpenDevTools) {
win.webContents.openDevTools({
mode: "detach",
activate: false,
});
}
},
};
+150
View File
@@ -0,0 +1,150 @@
import url, {fileURLToPath} from "node:url";
import {BrowserView, BrowserWindow} from "electron";
import {isPackaged} from "./env";
import path, {join} from "node:path";
import {Log} from "../mapi/log/main";
import {existsSync} from "node:fs";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
// 🔧 计算应用根目录 - 使用多重降级方案确保兼容 ASAR 打包
function getAppRoot(): string {
try {
// 开发环境:从当前文件目录往上两级
if (!isPackaged) {
const root = path.join(__dirname, "../..");
if (root && typeof root === 'string') {
return root;
}
}
} catch (error) {
console.error('[getAppRoot] 开发环境路径计算失败:', error);
}
// 生产环境或计算失败:尝试使用 process.resourcesPath(最可靠)
try {
if (process.resourcesPath && typeof process.resourcesPath === 'string') {
return process.resourcesPath;
}
} catch (error) {
console.error('[getAppRoot] process.resourcesPath 获取失败:', error);
}
// 最后的降级方案:使用可执行文件目录
try {
const execPath = process.execPath;
if (execPath && typeof execPath === 'string') {
return path.dirname(execPath);
}
} catch (error) {
console.error('[getAppRoot] process.execPath 获取失败:', error);
}
// 如果都失败,返回当前目录(最后防线)
return __dirname;
}
const appRoot = getAppRoot();
process.env.APP_ROOT = appRoot;
console.log('[env-main] APP_ROOT 已设置:', process.env.APP_ROOT);
// 🔧 处理 ASAR 打包:当使用 asar 打包时,应用代码在 app.asar 内
// 根据是否为 asar 打包来调整路径
let MAIN_DIST: string;
if (isPackaged && appRoot === process.resourcesPath) {
// 生产环境且使用了 asar 打包
// preload 和主进程代码在 app.asar 内,但资源在 app.asar.unpacked 或 resources 根目录
try {
const appAsarPath = path.join(appRoot, 'app.asar');
if (existsSync(appAsarPath)) {
MAIN_DIST = path.join(appAsarPath, "dist-electron");
} else {
MAIN_DIST = path.join(appRoot, "dist-electron");
}
} catch (e) {
MAIN_DIST = path.join(appRoot, "dist-electron");
}
} else {
// 开发环境或非 asar 打包
MAIN_DIST = path.join(appRoot, "dist-electron");
}
// 🔧 渲染进程文件也在 app.asar 内
let RENDERER_DIST: string;
if (isPackaged && appRoot === process.resourcesPath) {
try {
const appAsarPath = path.join(appRoot, 'app.asar');
if (existsSync(appAsarPath)) {
RENDERER_DIST = path.join(appAsarPath, "dist");
} else {
RENDERER_DIST = path.join(appRoot, "dist");
}
} catch (e) {
RENDERER_DIST = path.join(appRoot, "dist");
}
} else {
RENDERER_DIST = path.join(appRoot, "dist");
}
export const VITE_DEV_SERVER_URL = process.env.VITE_DEV_SERVER_URL;
process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL ? path.join(appRoot, "public") : RENDERER_DIST;
console.log('[env-main] MAIN_DIST:', MAIN_DIST);
console.log('[env-main] RENDERER_DIST:', RENDERER_DIST);
export { MAIN_DIST, RENDERER_DIST };
export const preloadDefault = path.join(MAIN_DIST, "preload/index.js");
console.log('[env-main] preloadDefault:', preloadDefault);
export const rendererLoadPath = (window: BrowserWindow | BrowserView, fileName: string) => {
if (!isPackaged && process.env.VITE_DEV_SERVER_URL) {
const x = new url.URL(rendererDistPath(fileName));
if (window instanceof BrowserView) {
window.webContents.loadURL(x.toString());
} else {
window.loadURL(x.toString());
}
} else {
if (window instanceof BrowserView) {
window.webContents.loadFile(rendererDistPath(fileName));
} else {
window.loadFile(rendererDistPath(fileName));
}
}
};
export const rendererDistPath = (fileName: string) => {
if (!isPackaged && process.env.VITE_DEV_SERVER_URL) {
return `${process.env.VITE_DEV_SERVER_URL.replace(/\/+$/, "")}/${fileName}`;
}
return join(RENDERER_DIST, fileName);
};
export const rendererIsUrl = (url: string) => {
return url.startsWith("http://") || url.startsWith("https://");
};
export const getGpuInfo = async () => {
const list = [] as {
id: string;
name: string;
size: number;
}[];
try {
// @ts-ignore
const si = await import("systeminformation");
const graphics = await si.graphics();
graphics.controllers.forEach((controller, index) => {
const size = Math.ceil(controller.vram / 1024);
let id = index + "";
const name = controller.model;
list.push({id, name, size});
});
} catch (e) {
Log.error("getGpuInfo", e);
}
return list;
};
+151
View File
@@ -0,0 +1,151 @@
import {execSync} from "child_process";
import {resolve} from "node:path";
import fs from "node:fs";
import os from "os";
import {Log} from "../mapi/log";
import FileIndex from "../mapi/file";
export const isPackaged = ["true"].includes(process.env.IS_PACKAGED);
export const isDev = !isPackaged;
// 🆕 调试模式开关(支持环境变量和特殊标志)
export const isDebugMode =
process.env.ELECTRON_DEBUG_MODE === 'true' ||
process.env.ELECTRON_FORCE_DEVTOOLS === '1';
// 🆕 是否应该启用DevTools(开发环境或调试模式)
export const shouldEnableDevTools = isDev || isDebugMode;
export const isWin = process.platform === "win32";
export const isMac = process.platform === "darwin";
export const isLinux = process.platform === "linux";
export const isMain = process.type === "browser";
export const isRender = process.type === "renderer";
export const platformName = (): "win" | "osx" | "linux" | null => {
if (isWin) return "win";
if (isMac) return "osx";
if (isLinux) return "linux";
return null;
};
export const memoryInfo = () => {
return {
total: os.totalmem(),
free: os.freemem(),
};
};
const tryFirst = (functionList: (() => any)[]) => {
for (const fun of functionList) {
try {
return fun();
} catch (e) {
}
}
return null;
};
let platformVersionCache: string | null = null;
export const platformVersion = () => {
if (null === platformVersionCache) {
const functionList: any[] = [];
if (isWin) {
functionList.push(() => execSync("wmic os get Version").toString().split("\n")[1].trim());
functionList.push(() =>
execSync(
"powershell -command \"(Get-ItemProperty 'HKLM:\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion').ReleaseId\""
)
.toString()
.trim()
);
} else if (isMac) {
functionList.push(() => execSync("sw_vers -productVersion").toString().trim());
} else if (isLinux) {
functionList.push(() =>
execSync("cat /etc/os-release | grep VERSION_ID").toString().split("=")[1].trim().replace(/"/g, "")
);
}
platformVersionCache = tryFirst(functionList);
if (!platformVersionCache) {
Log.error("env.platformVersion.error");
platformVersionCache = "0.0.0";
}
}
return platformVersionCache;
};
export const platformArch = (): "x86" | "arm64" | null => {
switch (os.arch()) {
case "x64":
return "x86";
case "arm64":
return "arm64";
}
return null;
};
let platformUUIDCache: string | null = null;
export const platformUUID = () => {
if (null === platformUUIDCache) {
const functionList: any[] = [];
if (isWin) {
functionList.push(() => execSync("wmic csproduct get UUID").toString().split("\n")[1].trim());
functionList.push(() =>
execSync('powershell -command "(Get-WmiObject Win32_ComputerSystemProduct).UUID"').toString().trim()
);
} else if (isMac) {
functionList.push(() =>
execSync("system_profiler SPHardwareDataType | grep UUID").toString().split(": ")[1].trim()
);
} else if (isLinux) {
functionList.push(() => execSync("cat /var/lib/dbus/machine-id").toString().trim().toUpperCase());
}
platformUUIDCache = tryFirst(functionList);
if (!platformUUIDCache) {
Log.error("env.platformUUID.error");
platformUUIDCache = "000000";
}
}
return platformUUIDCache;
};
export const buildResolve = (value: string): string => {
const basePath = isPackaged ? process.resourcesPath : "electron/resources";
return resolve(basePath, "build", value);
};
export const binResolve = (value: string): string => {
return resolve(process.resourcesPath, "bin", value);
};
export const extraResolve = (filePath: string): string => {
const basePath = isPackaged ? process.resourcesPath : "electron/resources";
return resolve(basePath, "extra", filePath);
};
export const extraResolveBin = (filePath: string): string => {
const originalFilePath = filePath;
if (isWin) {
if (!filePath.endsWith(".exe")) {
filePath += ".exe";
}
}
const dir = [platformName(), platformArch()].join("-");
const p = [dir, filePath].join("/");
const binaryPath = extraResolve(p);
if (!fs.existsSync(binaryPath)) {
// 对于 ffmpeg 和 ffprobe,回退到系统PATH
if (originalFilePath === 'ffmpeg' || originalFilePath === 'ffprobe') {
Log.info(`本地${originalFilePath}未找到,尝试使用系统PATH中的${originalFilePath}`);
return originalFilePath;
}
throw new Error(`错误:未找到${originalFilePath},请先安装${originalFilePath}并添加到系统环境变量`);
}
return binaryPath;
};
+40
View File
@@ -0,0 +1,40 @@
import {BrowserView, BrowserWindow} from "electron";
import {AppsMain} from "../mapi/app/main";
type HookType = never | "Show" | "Hide" | "EnterFullScreen" | "LeaveFullScreen" | "ShowQuitConfirmDialog";
export const executeHooks = async (win: BrowserWindow, hook: HookType, data?: any) => {
const evalJs = `
if(window.__page && window.__page.hooks && typeof window.__page.hooks.on${hook} === 'function' ) {
try {
window.__page.hooks.on${hook}(${JSON.stringify(data)});
} catch(e) {
console.log('executeHooks.on${hook}.error', e);
}
}`;
return win.webContents?.executeJavaScript(evalJs);
};
export const executeDarkMode = async (
view: BrowserWindow | BrowserView,
data: {
isSystem: boolean;
}
) => {
data = Object.assign(
{
isSystem: false,
},
data
);
if (await AppsMain.shouldDarkMode()) {
// body and html
view.webContents.executeJavaScript(`
document.body.setAttribute('data-theme', 'dark');
document.documentElement.setAttribute('data-theme', 'dark');
`);
if (data.isSystem) {
view.webContents.executeJavaScript(`document.body.setAttribute('arco-theme', 'dark');`);
}
}
};
+41
View File
@@ -0,0 +1,41 @@
import * as path from 'path';
import * as os from 'os';
import * as fs from 'fs';
export function normalizePath(filePath: string): string {
if (!filePath) return filePath;
return path.normalize(filePath);
}
export function normalizeOutputPath(filePath: string): string {
if (!filePath) return filePath;
if (os.platform() !== 'win32') return filePath;
const dir = path.dirname(filePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
return path.normalize(filePath);
}
export function normalizePaths(filePaths: string[]): string[] {
return filePaths.map(p => normalizePath(p));
}
export function quotePath(filePath: string): string {
if (filePath.includes(' ') && !filePath.startsWith('"')) {
return `"${filePath}"`;
}
return filePath;
}
export function createSafeTempPath(extension: string): string {
const tempDir = os.tmpdir();
const timestamp = Date.now();
const random = Math.random().toString(36).substring(2, 8);
const filename = `temp_${timestamp}_${random}.${extension}`;
return path.join(tempDir, filename);
}
+41
View File
@@ -0,0 +1,41 @@
import {isMac} from "./env";
let nodeMacPermissions = null;
if (isMac) {
(async () => {
try {
nodeMacPermissions = await import("node-mac-permissions");
nodeMacPermissions = nodeMacPermissions.default;
// console.log('nodeMacPermissions',nodeMacPermissions);
} catch (e) {}
})();
}
export const Permissions = {
async checkAccessibilityAccess(): Promise<boolean> {
return new Promise((resolve, reject) => {
if (isMac) {
const status = nodeMacPermissions.getAuthStatus("accessibility");
resolve(status === "authorized");
} else {
resolve(true);
}
});
},
async askAccessibilityAccess() {
nodeMacPermissions.askForAccessibilityAccess();
},
async checkScreenCaptureAccess(): Promise<boolean> {
return new Promise((resolve, reject) => {
if (isMac) {
const status = nodeMacPermissions.getAuthStatus("screen");
resolve(status === "authorized");
} else {
resolve(true);
}
});
},
async askScreenCaptureAccess() {
nodeMacPermissions.askForScreenCaptureAccess(true);
},
};
+28
View File
@@ -0,0 +1,28 @@
import PinyinMatch from "pinyin-match";
export const PinyinUtil = {
match(input, keywords) {
const index = PinyinMatch.match(input, keywords);
let inputMark = input;
let similarity = 0;
if (index) {
const indexStart = index[0];
const indexEnd = index[1];
inputMark =
input.substring(0, indexStart) +
"<mark>" +
input.substring(indexStart, indexEnd + 1) +
"</mark>" +
input.substring(indexEnd + 1);
similarity = (indexEnd - indexStart + 1) / input.length;
}
return {
matched: !!index,
inputMark,
similarity,
};
},
mark(text) {
return `<mark>${text}</mark>`;
},
};
+131
View File
@@ -0,0 +1,131 @@
import { exec } from "child_process";
import { promisify } from "util";
import path from "node:path";
import Log from "../mapi/log/main";
import { getRuntimeBundleRoot, getRuntimeRoot, isDevelopment } from "./resource-path";
const execAsync = promisify(exec);
export class ProcessCleanupManager {
private static childProcesses: Set<number> = new Set();
static registerChildProcess(pid: number) {
this.childProcesses.add(pid);
Log.info(`[ProcessCleanup] Registered child process: ${pid}`);
}
static unregisterChildProcess(pid: number) {
this.childProcesses.delete(pid);
Log.info(`[ProcessCleanup] Unregistered child process: ${pid}`);
}
static getChildProcesses(): number[] {
return Array.from(this.childProcesses);
}
private static async killProcess(pid: number): Promise<boolean> {
try {
if (process.platform === "win32") {
await execAsync(`taskkill /F /PID ${pid} /T`);
} else {
await execAsync(`kill -9 ${pid}`);
}
Log.info(`[ProcessCleanup] Killed process: ${pid}`);
return true;
} catch (error: any) {
Log.error(`[ProcessCleanup] Failed to kill process ${pid}:`, error.message);
return false;
}
}
private static escapePowerShellString(value: string): string {
return value.replace(/'/g, "''");
}
private static getCleanupRoots(): string[] {
const roots = new Set<string>();
const candidates = [
path.dirname(process.execPath),
getRuntimeRoot(),
getRuntimeBundleRoot(),
];
for (const candidate of candidates) {
if (!candidate) {
continue;
}
try {
roots.add(path.resolve(candidate));
} catch {
}
}
return Array.from(roots);
}
private static async killProcessesUnderAppRoots(): Promise<number> {
if (process.platform !== "win32" || isDevelopment()) {
return 0;
}
const roots = this.getCleanupRoots();
if (roots.length === 0) {
return 0;
}
const rootList = roots.map(root => `'${this.escapePowerShellString(root)}'`).join(", ");
const command =
`powershell -NoProfile -ExecutionPolicy Bypass -Command ` +
`"$roots = @(${rootList}); ` +
`$roots = $roots | Where-Object { $_ } | ForEach-Object { [System.IO.Path]::GetFullPath($_) }; ` +
`Get-Process -ErrorAction SilentlyContinue | ` +
`Where-Object { $_.Id -ne ${process.pid} -and $_.Path } | ` +
`Where-Object { ` +
`try { ` +
`$path = [System.IO.Path]::GetFullPath($_.Path); ` +
`foreach ($root in $roots) { ` +
`if ($path.StartsWith($root, [System.StringComparison]::OrdinalIgnoreCase)) { return $true } ` +
`}; ` +
`$false ` +
`} catch { $false } ` +
`} | ` +
`ForEach-Object { try { Stop-Process -Id $_.Id -Force -ErrorAction Stop; Write-Output $_.Id } catch {} }"`;
try {
const { stdout } = await execAsync(command);
const count = stdout
.split(/\r?\n/)
.map(line => line.trim())
.filter(Boolean).length;
if (count > 0) {
Log.info("[ProcessCleanup] Killed app-owned processes under install roots", { count, roots });
}
return count;
} catch {
return 0;
}
}
static async cleanupAllProcesses(): Promise<boolean> {
Log.info("[ProcessCleanup] Starting cleanup...");
const registeredPids = this.getChildProcesses();
for (const pid of registeredPids) {
await this.killProcess(pid);
this.unregisterChildProcess(pid);
}
await this.killProcessesUnderAppRoots();
await new Promise(resolve => setTimeout(resolve, 2000));
await this.killProcessesUnderAppRoots();
await new Promise(resolve => setTimeout(resolve, 1000));
Log.info("[ProcessCleanup] Cleanup completed");
return true;
}
static getRunningTaskCount(): number {
return this.childProcesses.size;
}
}
+16
View File
@@ -0,0 +1,16 @@
/** 在主进程中获取关键信息存储到环境变量中,从而在预加载脚本中及渲染进程中使用 */
import {app} from "electron";
/** 注意: app.isPackaged 可能被被某些方法改变所以请将该文件放到 main.js 必须位于非依赖项的顶部 */
if (process.platform === "darwin") {
const fixPath = require("fix-path");
fixPath();
}
process.env.IS_PACKAGED = String(app.isPackaged);
process.env.DESKTOP_PATH = app.getPath("desktop");
process.env.CWD = process.cwd();
export const isDummy = false;
+182
View File
@@ -0,0 +1,182 @@
import { app } from 'electron';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { exec, execSync } from 'child_process';
import { getRuntimeBundlePath } from './resource-path';
const SETUP_VERSION = '2';
function getSetupFlagPath(): string {
return path.join(app.getPath('userData'), '.python-setup-done');
}
function getPythonInstallDir(): string {
return getRuntimeBundlePath('python-runtime');
}
function getPythonBinPath(): string {
return path.join(getPythonInstallDir(), 'python', 'bin', 'python3');
}
function getPipBinPath(): string {
return path.join(getPythonInstallDir(), 'python', 'bin', 'pip3');
}
function getLocalResourcePath(filename: string): string | null {
const runtimeBundlePath = path.join(getRuntimeBundlePath('python-runtime'), filename);
if (fs.existsSync(runtimeBundlePath)) {
return runtimeBundlePath;
}
const isDev = __dirname.includes('electron') && !__dirname.includes('dist-electron');
const base = isDev
? path.join(app.getAppPath(), 'electron', 'resources', 'extra', 'osx')
: path.join(process.resourcesPath, 'extra', 'osx');
const fullPath = path.join(base, filename);
return fs.existsSync(fullPath) ? fullPath : null;
}
function execAsync(cmd: string, opts?: any): Promise<{ code: number; stdout: string; stderr: string }> {
return new Promise((resolve) => {
exec(cmd, { timeout: 300000, ...opts }, (err, stdout, stderr) => {
resolve({ code: err ? (err.code as number) || 1 : 0, stdout: stdout || '', stderr: stderr || '' });
});
});
}
export function isSetupComplete(): boolean {
if (process.platform === 'win32') return true;
if (!fs.existsSync(getSetupFlagPath())) return false;
try {
const v = fs.readFileSync(getSetupFlagPath(), 'utf-8').trim();
if (v !== SETUP_VERSION) return false;
return fs.existsSync(getPythonBinPath());
} catch { return false; }
}
function markSetupComplete(): void {
fs.writeFileSync(getSetupFlagPath(), SETUP_VERSION);
}
async function extractPython(): Promise<string> {
const installDir = getPythonInstallDir();
const pythonBin = getPythonBinPath();
if (fs.existsSync(pythonBin)) {
console.log('[PythonSetup] Python 已解压:', pythonBin);
return pythonBin;
}
fs.mkdirSync(installDir, { recursive: true });
const arch = os.arch();
const tarName = arch === 'arm64' ? 'python-mac-arm64.tar.gz' : 'python-mac-x64.tar.gz';
const tarPath = getLocalResourcePath(tarName);
if (!tarPath) {
throw new Error(`未找到 Python 包: ${tarName}`);
}
console.log('[PythonSetup] 解压 Python:', tarPath);
const result = await execAsync(`tar -xzf "${tarPath}" -C "${installDir}"`, { timeout: 120000 });
if (result.code !== 0) {
throw new Error('解压 Python 失败: ' + result.stderr.substring(0, 200));
}
try {
const entries = fs.readdirSync(installDir);
const pythonEntry = entries.find(e => e.startsWith('python'));
if (pythonEntry && pythonEntry !== 'python') {
fs.renameSync(path.join(installDir, pythonEntry), path.join(installDir, 'python'));
}
} catch {}
if (!fs.existsSync(pythonBin)) {
throw new Error('解压后找不到 python3');
}
try { fs.chmodSync(pythonBin, 0o755); } catch {}
console.log('[PythonSetup] ✅ Python 解压完成');
return pythonBin;
}
async function installWheels(pythonBin: string): Promise<void> {
const wheelsDir = getLocalResourcePath('wheels');
if (!wheelsDir) {
throw new Error('未找到 wheels 目录');
}
const wheelFiles = fs.readdirSync(wheelsDir).filter(f => f.endsWith('.whl') || f.endsWith('.tar.gz'));
if (wheelFiles.length === 0) {
throw new Error('wheels 目录为空');
}
console.log(`[PythonSetup] 离线安装 ${wheelFiles.length} 个包...`);
const allFiles = wheelFiles.map(f => `"${path.join(wheelsDir, f)}"`).join(' ');
const result = await execAsync(
`"${pythonBin}" -m pip install --no-index --no-deps ${allFiles}`,
{ timeout: 300000 }
);
if (result.code !== 0) {
console.warn('[PythonSetup] 部分 pip install 失败,尝试逐个安装...');
for (const f of wheelFiles) {
const filePath = path.join(wheelsDir, f);
const r = await execAsync(`"${pythonBin}" -m pip install --no-index "${filePath}"`, { timeout: 120000 });
if (r.code !== 0) {
console.warn(`[PythonSetup] 安装失败: ${f}`, r.stderr.substring(0, 100));
}
}
}
console.log('[PythonSetup] ✅ 依赖安装完成');
}
async function verifyAll(pythonBin: string): Promise<boolean> {
const code = `
import importlib
for mod in ['cv2','numpy','PIL','rembg','onnxruntime','jieba','loguru']:
try: importlib.import_module(mod)
except: exit(1)
print('OK')
`;
const result = await execAsync(`"${pythonBin}" -c '${code.replace(/\n/g, ' ')}'`, { timeout: 30000 });
return result.code === 0 && result.stdout.trim() === 'OK';
}
export async function ensurePythonSetup(): Promise<string | null> {
if (process.platform === 'win32') return null;
const pythonBin = getPythonBinPath();
if (isSetupComplete() && fs.existsSync(pythonBin)) {
return pythonBin;
}
console.log('[PythonSetup] 开始自动初始化(完全离线)...');
try {
const pyBin = await extractPython();
await installWheels(pyBin);
const ok = await verifyAll(pyBin);
if (!ok) {
console.warn('[PythonSetup] ⚠️ 部分依赖验证失败,但继续运行');
}
markSetupComplete();
console.log('[PythonSetup] ✅ Python 环境初始化完成');
return pyBin;
} catch (err) {
console.error('[PythonSetup] ❌ 初始化失败:', err);
return null;
}
}
export function getMacOSPythonPath(): string | null {
if (process.platform === 'win32') return null;
const p = getPythonBinPath();
return fs.existsSync(p) ? p : null;
}
+154
View File
@@ -0,0 +1,154 @@
/**
* Python 进程启动工具
* 统一处理中文路径问题
*/
import { spawn, SpawnOptions } from 'child_process';
import { normalizePath } from './path-util';
import * as path from 'path';
import * as os from 'os';
import logger from '../mapi/log/main';
export interface PythonSpawnOptions extends SpawnOptions {
/**
* 是否转换所有路径参数(默认 true)
*/
normalizePaths?: boolean;
}
/**
* 启动 Python 进程,自动处理中文路径
* @param pythonPath Python 可执行文件路径
* @param args Python 脚本和参数
* @param options spawn 选项
* @returns ChildProcess
*/
export function spawnPython(
pythonPath: string,
args: string[],
options?: PythonSpawnOptions
) {
const platform = os.platform();
const shouldNormalize = options?.normalizePaths !== false && platform === 'win32';
// Windows 平台:转换所有路径
if (shouldNormalize) {
// 转换 Python 可执行文件路径
const normalizedPythonPath = normalizePath(pythonPath);
// 转换参数中的路径
const normalizedArgs = args.map((arg, index) => {
// 检查是否是文件路径(包含路径分隔符或扩展名)
if (arg.includes('/') || arg.includes('\\') ||
(arg.includes('.') && !arg.startsWith('-'))) {
const normalized = normalizePath(arg);
if (normalized !== arg) {
logger.info('[PythonSpawn] 路径转换', {
index,
original: arg.substring(0, 100),
normalized: normalized.substring(0, 100)
});
}
return normalized;
}
return arg;
});
logger.info('[PythonSpawn] 启动 Python 进程', {
pythonPath: normalizedPythonPath.substring(0, 100),
argsCount: normalizedArgs.length,
cwd: options?.cwd
});
return spawn(normalizedPythonPath, normalizedArgs, options);
}
// macOS/Linux:直接启动
return spawn(pythonPath, args, options);
}
/**
* 执行 Python 脚本并返回 Promise
* @param pythonPath Python 可执行文件路径
* @param args Python 脚本和参数
* @param options spawn 选项
* @returns Promise<{stdout: string, stderr: string, exitCode: number}>
*/
export function execPython(
pythonPath: string,
args: string[],
options?: PythonSpawnOptions
): Promise<{stdout: string, stderr: string, exitCode: number}> {
return new Promise((resolve, reject) => {
const child = spawnPython(pythonPath, args, options);
let stdout = '';
let stderr = '';
child.stdout?.on('data', (data) => {
stdout += data.toString();
});
child.stderr?.on('data', (data) => {
stderr += data.toString();
});
child.on('close', (exitCode) => {
resolve({
stdout,
stderr,
exitCode: exitCode || 0
});
});
child.on('error', (error) => {
reject(error);
});
});
}
/**
* 创建临时配置文件(确保在纯英文路径)
* @param config 配置对象
* @param prefix 文件名前缀
* @returns 配置文件路径
*/
export function createTempConfig(config: any, prefix: string = 'config'): string {
const fs = require('fs');
// 使用系统临时目录(通常是纯英文路径)
const tempDir = os.tmpdir();
const timestamp = Date.now();
const random = Math.random().toString(36).substring(2, 8);
const configPath = path.join(tempDir, `${prefix}_${timestamp}_${random}.json`);
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
logger.info('[PythonSpawn] 创建临时配置文件', {
path: configPath,
size: JSON.stringify(config).length
});
return configPath;
}
/**
* 清理临时配置文件
* @param configPath 配置文件路径
*/
export function cleanupTempConfig(configPath: string): void {
try {
const fs = require('fs');
if (fs.existsSync(configPath)) {
fs.unlinkSync(configPath);
logger.info('[PythonSpawn] 清理临时配置文件', { path: configPath });
}
} catch (error) {
logger.warn('[PythonSpawn] 清理临时配置文件失败', {
path: configPath,
error: error instanceof Error ? error.message : String(error)
});
}
}
+89
View File
@@ -0,0 +1,89 @@
/**
* Python工具模块
* 提供Python可执行文件路径查找和验证
*/
import * as fs from 'fs';
import * as path from 'path';
import {spawn} from 'child_process';
import { getPythonExecutablePath, resourceExists, isDevelopment } from './resource-path';
/**
* 获取Python可执行文件路径
* ⚠️ 只使用程序自带的 Python,不依赖系统环境
* 如果找不到自带的 Python,会抛出错误
*/
export function getPythonPath(): string {
const bundledPythonPath = getPythonExecutablePath();
const isDev = isDevelopment();
console.log('[getPythonPath]', {
isDev,
bundledPythonPath,
exists: resourceExists(bundledPythonPath)
});
if (resourceExists(bundledPythonPath)) {
console.log('[getPythonPath] ✅ 找到程序自带的 Python:', bundledPythonPath);
return bundledPythonPath;
}
// ❌ 找不到自带的 Python - 这是严重错误,应该报错
const errorMsg = `❌ 程序自带的 Python 不存在: ${bundledPythonPath}\n` +
`环境: ${isDev ? '开发环境' : '生产环境'}\n` +
`请确认程序是否正确安装,或重新安装程序。`;
console.error('[getPythonPath]', errorMsg);
throw new Error(errorMsg);
}
/**
* 验证Python是否可用
* @returns Promise<boolean>
*/
export async function verifyPython(): Promise<boolean> {
return new Promise((resolve) => {
const pythonPath = getPythonPath();
const process = spawn(pythonPath, ['--version']);
process.on('close', (code) => {
resolve(code === 0);
});
process.on('error', () => {
resolve(false);
});
});
}
/**
* 获取Python版本信息
* @returns Promise<string | null>
*/
export async function getPythonVersion(): Promise<string | null> {
return new Promise((resolve) => {
const pythonPath = getPythonPath();
const process = spawn(pythonPath, ['--version']);
let output = '';
process.stdout.on('data', (data) => {
output += data.toString();
});
process.stderr.on('data', (data) => {
output += data.toString();
});
process.on('close', (code) => {
if (code === 0 && output) {
resolve(output.trim());
} else {
resolve(null);
}
});
process.on('error', () => {
resolve(null);
});
});
}
+237
View File
@@ -0,0 +1,237 @@
/**
* 资源路径工具模块
* 统一管理开发环境和生产环境的资源路径
* ⚠️ 确保所有依赖都能在不同环境下正确识别
*/
import * as fs from 'fs';
import * as path from 'path';
import { app } from 'electron';
import { AppEnv } from '../mapi/env';
/**
* 判断是否为开发环境
*/
export function isDevelopment(): boolean {
const appRoot = process.env.APP_ROOT || process.env.ELECTRON_RESOURCES_PATH || process.cwd();
// 修复:dist-release目录不是开发环境
if (appRoot.includes('dist-release') || appRoot.includes('dist-electron')) {
return false;
}
// 🔧 修复:不硬编码项目文件夹名,改用检查开发目录结构是否存在
// 开发环境特征:项目根目录有 electron/resources/extra 目录
const devResourcePath = path.join(appRoot, 'electron', 'resources', 'extra');
if (fs.existsSync(devResourcePath)) {
return true;
}
// 回退:检查旧的文件夹名(兼容)
return appRoot.includes('aigcpanel-main');
}
/**
* 获取资源根目录
*/
export function getResourceRoot(): string {
const isDev = isDevelopment();
if (isDev) {
// 开发环境:项目根目录/electron/resources/extra
const appRoot = process.env.APP_ROOT || process.cwd();
return path.join(appRoot, 'electron', 'resources', 'extra');
} else {
// 生产环境:使用 process.resourcesPath 获取 resources 目录
// process.resourcesPath 指向 E:\xxx\resources
// 资源文件在 E:\xxx\resources\extra
// 关键修复:直接使用 process.resourcesPath 而不是 APP_ROOT
// 因为 APP_ROOT 被设置为 resources 的父目录,会导致路径错误
const resourcesPath = process.resourcesPath || process.env.ELECTRON_RESOURCES_PATH;
if (resourcesPath) {
return path.join(resourcesPath, 'extra');
}
// 回退:使用 APP_ROOT + resources/extra(仅当 process.resourcesPath 不可用时)
const appRoot = process.env.APP_ROOT || process.cwd();
return path.join(appRoot, 'resources', 'extra');
}
}
/**
* 获取通用资源路径(common 目录下的资源)
* @param subPath 子路径(相对于 extra/common
*/
export function getCommonResourcePath(subPath: string): string {
const resourceRoot = getResourceRoot();
return path.join(resourceRoot, 'common', subPath);
}
export function getRuntimeRoot(...segments: string[]): string {
const baseRoot = AppEnv.installRoot || process.env.APP_ROOT || process.cwd();
return path.join(baseRoot, ...segments);
}
export function getRuntimeDataPath(...segments: string[]): string {
const dataRoot = AppEnv.dataRoot || getRuntimeRoot('data');
return path.join(dataRoot, ...segments);
}
export function getRuntimeBundleRoot(): string {
return AppEnv.resourceBundleRoot || getRuntimeRoot('resources-bundles');
}
export function getRuntimeBundlePath(bundleName: string, ...segments: string[]): string {
return path.join(getRuntimeBundleRoot(), bundleName, ...segments);
}
export function getResourceStatePath(...segments: string[]): string {
const root = AppEnv.resourceStateRoot || getRuntimeDataPath('resource-state');
return path.join(root, ...segments);
}
/**
* 获取平台特定资源路径
* @param subPath 子路径(相对于 extra/{platform}
*/
export function getPlatformResourcePath(subPath: string): string {
const resourceRoot = getResourceRoot();
const platform = getPlatformDir();
return path.join(resourceRoot, platform, subPath);
}
/**
* 获取平台目录名
*/
export function getPlatformDir(): string {
const isWin = process.platform === 'win32';
const isMac = process.platform === 'darwin';
if (isWin) return 'win-x86';
if (isMac) return 'osx';
return 'linux';
}
/**
* 检查资源文件是否存在
* @param resourcePath 资源路径
* @returns 文件是否存在
*/
export function resourceExists(resourcePath: string): boolean {
return fs.existsSync(resourcePath);
}
/**
* 获取并验证资源路径
* @param resourcePath 资源路径
* @param errorMessage 文件不存在时的错误消息
* @returns 验证后的资源路径
* @throws 如果资源不存在则抛出错误
*/
export function getAndVerifyResource(resourcePath: string, errorMessage?: string): string {
if (!fs.existsSync(resourcePath)) {
const error = errorMessage || `资源文件不存在: ${resourcePath}`;
throw new Error(error);
}
return resourcePath;
}
/**
* 快捷方法:获取 Python 可执行文件路径
*/
export function getPythonExecutablePath(): string {
const isWin = process.platform === 'win32';
const pythonFileName = isWin ? 'python.exe' : 'python3';
const runtimePath = getRuntimeBundlePath('python-runtime', pythonFileName);
if (fs.existsSync(runtimePath)) {
return runtimePath;
}
return getCommonResourcePath(path.join('python', pythonFileName));
}
/**
* 快捷方法:获取 FFmpeg 可执行文件路径
*/
export function getFFmpegExecutablePath(): string {
const isWin = process.platform === 'win32';
const isMac = process.platform === 'darwin';
let ffmpegFileName = 'ffmpeg';
if (isWin) ffmpegFileName = 'ffmpeg.exe';
const runtimePath = getRuntimeBundlePath('ffmpeg', ffmpegFileName);
if (fs.existsSync(runtimePath)) {
return runtimePath;
}
return getPlatformResourcePath(ffmpegFileName);
}
/**
* 快捷方法:获取 FFprobe 可执行文件路径
*/
export function getFFprobeExecutablePath(): string {
const isWin = process.platform === 'win32';
const isMac = process.platform === 'darwin';
let ffprobeFileName = 'ffprobe';
if (isWin) ffprobeFileName = 'ffprobe.exe';
const runtimePath = getRuntimeBundlePath('ffmpeg', ffprobeFileName);
if (fs.existsSync(runtimePath)) {
return runtimePath;
}
return getPlatformResourcePath(ffprobeFileName);
}
/**
* 快捷方法:获取模型文件路径
* @param modelFileName 模型文件名(如 'u2net.onnx'
*/
export function getModelPath(modelFileName: string): string {
const runtimePath = getRuntimeBundlePath('models', modelFileName);
if (fs.existsSync(runtimePath)) {
return runtimePath;
}
return getCommonResourcePath(path.join('models', modelFileName));
}
/**
* 快捷方法:获取 Python 脚本路径
* @param scriptFileName Python 脚本文件名
*/
export function getPythonScriptPath(scriptFileName: string): string {
const runtimePath = getRuntimeBundlePath('python-runtime', scriptFileName);
if (fs.existsSync(runtimePath)) {
return runtimePath;
}
const appRoot = process.env.APP_ROOT || process.env.ELECTRON_RESOURCES_PATH || process.cwd();
const isDev = isDevelopment();
if (isDev) {
return path.join(appRoot, 'python', scriptFileName);
} else {
const asarUnpackedPath = path.join(appRoot, 'app.asar.unpacked', 'python', scriptFileName);
if (fs.existsSync(asarUnpackedPath)) {
return asarUnpackedPath;
}
return path.join(appRoot, 'python', scriptFileName);
}
}
/**
* 诊断信息:打印所有资源路径
*/
export function printResourceDiagnostics(): void {
const isDev = isDevelopment();
const resourceRoot = getResourceRoot();
console.log('\n========== 资源路径诊断 ==========');
console.log('环境模式:', isDev ? '开发环境' : '生产环境');
console.log('App Root:', process.env.APP_ROOT || process.cwd());
console.log('资源根目录:', resourceRoot);
console.log('\n关键资源路径:');
console.log(' Python:', getPythonExecutablePath(), '存在:', resourceExists(getPythonExecutablePath()));
console.log(' FFmpeg:', getFFmpegExecutablePath(), '存在:', resourceExists(getFFmpegExecutablePath()));
console.log(' FFprobe:', getFFprobeExecutablePath(), '存在:', resourceExists(getFFprobeExecutablePath()));
console.log(' U2Net模型:', getModelPath('u2net.onnx'), '存在:', resourceExists(getModelPath('u2net.onnx')));
console.log('=====================================\n');
}
+138
View File
@@ -0,0 +1,138 @@
/**
* 🔐 Store Manager - 用于持久化存储激活信息和设备指纹
* 使用 JSON 文件存储,位于用户数据目录
*/
import { app } from 'electron';
import * as fs from 'node:fs';
import * as path from 'node:path';
import Log from '../mapi/log/main';
interface StoreData {
activation?: any;
deviceFingerprint?: string;
}
export class Store {
private static instance: Store | null = null;
private storePath: string;
private data: StoreData = {};
private constructor() {
const userDataPath = app.getPath('userData');
this.storePath = path.join(userDataPath, 'store.json');
this.load();
}
/**
* 获取 Store 单例实例(延迟初始化)
*/
static getInstance(): Store {
if (!Store.instance) {
Store.instance = new Store();
}
return Store.instance;
}
/**
* 从文件加载数据
*/
private load(): void {
try {
if (fs.existsSync(this.storePath)) {
const content = fs.readFileSync(this.storePath, 'utf-8');
this.data = JSON.parse(content);
Log.info('[Store] 已从文件加载持久化数据:', {
hasActivation: !!this.data.activation,
hasDeviceFingerprint: !!this.data.deviceFingerprint
});
} else {
this.data = {};
Log.info('[Store] Store 文件不存在,使用空数据');
}
} catch (error) {
Log.error('[Store] 加载文件失败:', error);
this.data = {};
}
}
/**
* 保存数据到文件
*/
private save(): void {
try {
const dir = path.dirname(this.storePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(this.storePath, JSON.stringify(this.data, null, 2), 'utf-8');
Log.info('[Store] 数据已保存到文件');
} catch (error) {
Log.error('[Store] 保存文件失败:', error);
throw error;
}
}
/**
* 获取激活信息
*/
getActivation(): any {
Log.info('[Store] 获取激活信息:', !!this.data.activation);
return this.data.activation || null;
}
/**
* 保存激活信息
*/
setActivation(data: any): void {
Log.info('[Store] 保存激活信息');
this.data.activation = data;
this.save();
}
/**
* 清除激活信息
*/
clearActivation(): void {
Log.info('[Store] 清除激活信息');
delete this.data.activation;
this.save();
}
/**
* 获取设备指纹
*/
getDeviceFingerprint(): string | null {
Log.info('[Store] 获取设备指纹:', !!this.data.deviceFingerprint);
return this.data.deviceFingerprint || null;
}
/**
* 保存设备指纹
*/
setDeviceFingerprint(fingerprint: string): void {
Log.info('[Store] 保存设备指纹');
this.data.deviceFingerprint = fingerprint;
this.save();
}
/**
* 清除设备指纹
*/
clearDeviceFingerprint(): void {
Log.info('[Store] 清除设备指纹');
delete this.data.deviceFingerprint;
this.save();
}
}
// 延迟初始化:只有在第一次调用时才创建 Store 实例
// 这样确保 AppEnv 已经初始化完成
export default {
getActivation: () => Store.getInstance().getActivation(),
setActivation: (data: any) => Store.getInstance().setActivation(data),
clearActivation: () => Store.getInstance().clearActivation(),
getDeviceFingerprint: () => Store.getInstance().getDeviceFingerprint(),
setDeviceFingerprint: (fingerprint: string) => Store.getInstance().setDeviceFingerprint(fingerprint),
clearDeviceFingerprint: () => Store.getInstance().clearDeviceFingerprint(),
};
+865
View File
@@ -0,0 +1,865 @@
import chardet from "chardet";
import dayjs from "dayjs";
import iconvLite from "iconv-lite";
import {Base64} from "js-base64";
import * as crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import Showdown from "showdown";
// import {Iconv} from "iconv"
import {isMac, isWin} from "./env";
import FileIndex from "../mapi/file";
export const EncodeUtil = {
base32Alphabet: "abcdefghijklmnopqrstuvwxyz234567",
base32Encode(str: string) {
const buffer = Buffer.from(str, "utf8");
let bits = "";
let output = "";
// 将每个字节转为8位二进制
for (let i = 0; i < buffer.length; i++) {
const byte = buffer[i];
bits += byte.toString(2).padStart(8, "0");
}
// 每5位一组,转为 Base32 字符
for (let i = 0; i < bits.length; i += 5) {
const chunk = bits.slice(i, i + 5);
const paddedChunk = chunk.padEnd(5, "0"); // 不足5位补0
const index = parseInt(paddedChunk, 2);
output += EncodeUtil.base32Alphabet[index];
}
return output;
},
base32Decode(str: string) {
const base32Alphabet = "abcdefghijklmnopqrstuvwxyz234567";
let bits = "";
for (let i = 0; i < str.length; i++) {
const char = str[i];
const index = base32Alphabet.indexOf(char);
if (index === -1) {
throw new Error("Invalid Base32 character: " + char);
}
bits += index.toString(2).padStart(5, "0");
}
const bytes: number[] = [];
for (let i = 0; i + 8 <= bits.length; i += 8) {
const byte = bits.slice(i, i + 8);
bytes.push(parseInt(byte, 2));
}
return Buffer.from(bytes).toString("utf8");
},
base64Encode(str: string) {
return Base64.encode(str);
},
base64Decode(str: string) {
return Base64.decode(str);
},
md5(str: string) {
return crypto.createHash("md5").update(str).digest("hex");
},
aesEncode(str: string, key: string) {
const cipher = crypto.createCipheriv("aes-128-ecb", key, "");
let crypted = cipher.update(str, "utf8", "base64");
crypted += cipher.final("base64");
return crypted;
},
aesDecode(str: string, key: string) {
const decipher = crypto.createDecipheriv("aes-128-ecb", key, "");
let dec = decipher.update(str, "base64", "utf8");
dec += decipher.final("utf8");
return dec;
},
async fileXzipEncode(pathname: string): Promise<string> {
if (!fs.existsSync(pathname)) {
throw new Error(`Input file not found: ${pathname}`);
}
// Generate new filepath with .xzip extension
const basePath = pathname.substring(0, pathname.lastIndexOf("."));
const outputPath = basePath + ".xzip";
// Get file info
const fileStats = fs.statSync(pathname);
const fileSize = fileStats.size;
const fileExt = pathname.split(".").pop() || "";
// Generate random 16-character key
const encryptionKey = StrUtil.randomString(16);
// Create metadata
const filemeta = {
version: 1,
format: fileExt,
size: fileSize,
key: encryptionKey,
};
// Convert metadata to JSON and then base64 encode
const metaJson = JSON.stringify(filemeta);
const metaB64 = Buffer.from(metaJson, "utf-8").toString("base64");
const metaLength = metaB64.length;
// Prepare encryption key
const keyBytes = Buffer.from(encryptionKey, "utf-8");
const keyLength = keyBytes.length;
// Stream processing: read, encrypt and write in chunks
const inputStream = fs.createReadStream(pathname);
const outputStream = fs.createWriteStream(outputPath);
// Write metadata length (4 bytes, little-endian)
const metaLengthBuffer = Buffer.allocUnsafe(4);
metaLengthBuffer.writeUInt32LE(metaLength, 0);
outputStream.write(metaLengthBuffer);
// Write base64 encoded metadata
outputStream.write(Buffer.from(metaB64, "utf-8"));
// Stream encrypt the file content
let bytesProcessed = 0;
return new Promise((resolve, reject) => {
inputStream.on("data", (chunk: Buffer) => {
// XOR encrypt the chunk
const encryptedChunk = Buffer.alloc(chunk.length);
for (let i = 0; i < chunk.length; i++) {
encryptedChunk[i] = chunk[i] ^ keyBytes[bytesProcessed % keyLength];
bytesProcessed++;
}
// Write encrypted chunk
outputStream.write(encryptedChunk);
});
inputStream.on("end", () => {
outputStream.end();
resolve(outputPath);
});
inputStream.on("error", error => {
outputStream.destroy();
reject(error);
});
outputStream.on("error", error => {
inputStream.destroy();
reject(error);
});
});
},
async fileXzipDecode(pathname: string): Promise<string> {
if (!fs.existsSync(pathname)) {
throw new Error(`Input file not found: ${pathname}`);
}
if (!pathname.endsWith(".xzip")) {
return pathname; // Not an xzip file, return as is
}
let outputPath = pathname.replace(/\.xzip$/, "");
return new Promise((resolve, reject) => {
const inputStream = fs.createReadStream(pathname);
let metadataRead = false;
let filemeta: any = null;
let keyBytes: Buffer;
let bytesProcessed = 0;
let outputStream: fs.WriteStream;
let remainingMetaBytes = 0;
let metaBuffer = Buffer.alloc(0);
inputStream.on("data", (chunk: Buffer) => {
let chunkOffset = 0;
if (!metadataRead) {
if (remainingMetaBytes === 0) {
// Read metadata length (first 4 bytes)
if (chunk.length < 4) {
reject(new Error("Invalid xzip file: insufficient data for metadata length"));
return;
}
const metaLength = chunk.readUInt32LE(0);
remainingMetaBytes = metaLength;
chunkOffset = 4;
}
// Read metadata
const availableMetaBytes = Math.min(remainingMetaBytes, chunk.length - chunkOffset);
const metaChunk = chunk.subarray(chunkOffset, chunkOffset + availableMetaBytes);
metaBuffer = Buffer.concat([metaBuffer, metaChunk] as readonly Uint8Array[]);
remainingMetaBytes -= availableMetaBytes;
chunkOffset += availableMetaBytes;
if (remainingMetaBytes === 0) {
// Parse metadata
try {
const metaB64 = metaBuffer.toString("utf-8");
const metaJson = Buffer.from(metaB64, "base64").toString("utf-8");
filemeta = JSON.parse(metaJson);
keyBytes = Buffer.from(filemeta.key, "utf-8");
// Create output file with correct extension
const finalOutputPath = outputPath + (filemeta.format ? "." + filemeta.format : "");
outputStream = fs.createWriteStream(finalOutputPath);
metadataRead = true;
// Set the final output path for resolution
outputPath = finalOutputPath;
} catch (error) {
reject(new Error("Invalid xzip file: corrupted metadata"));
return;
}
}
}
if (metadataRead && chunkOffset < chunk.length) {
// Decrypt remaining chunk data
const encryptedChunk = chunk.subarray(chunkOffset);
const decryptedChunk = Buffer.alloc(encryptedChunk.length);
const keyLength = keyBytes.length;
for (let i = 0; i < encryptedChunk.length; i++) {
decryptedChunk[i] = encryptedChunk[i] ^ keyBytes[bytesProcessed % keyLength];
bytesProcessed++;
}
outputStream.write(decryptedChunk);
}
});
inputStream.on("end", () => {
if (outputStream) {
outputStream.end();
resolve(outputPath);
} else {
reject(new Error("Invalid xzip file: incomplete metadata"));
}
});
inputStream.on("error", error => {
if (outputStream) {
outputStream.destroy();
}
reject(error);
});
if (outputStream) {
outputStream.on("error", error => {
inputStream.destroy();
reject(error);
});
}
});
},
};
export const IconvUtil = {
convert(str: string, to?: string, from?: string) {
if (!from) {
from = chardet.detect(Buffer.from(str));
}
to = to || "utf8";
const buffer = iconvLite.encode(str, from);
return iconvLite.decode(buffer, to);
},
bufferToUtf8(buffer: Buffer) {
const encoding = chardet.detect(buffer);
// if ('ISO-2022-CN' === encoding) {
// const iconvInstance = new Iconv('ISO-2022-CN', 'UTF-8//TRANSLIT//IGNORE');
// return iconvInstance.convert(buffer).toString()
// }
return iconvLite.decode(buffer, encoding).toString();
},
detect(buffer: Uint8Array) {
// detect str encoding
return chardet.detect(buffer);
},
};
export const StrUtil = {
randomString(len: number = 32) {
const chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
let result = "";
for (let i = len; i > 0; --i) {
result += chars[Math.floor(Math.random() * chars.length)];
}
return result;
},
uuid() {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
const r = (Math.random() * 16) | 0;
const v = c === "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
},
hashCode(str: string, length: number = 8) {
let hash = 0;
if (str.length === 0) return hash + "";
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash = hash & hash;
}
let result = Math.abs(hash).toString(16);
if (result.length < length) {
result = "0".repeat(length - result.length) + result;
}
return result;
},
hashCodeWithDuplicateCheck(str: string, check: string[], length: number = 8) {
let code = this.hashCode(str, length);
while (check.includes(code)) {
code = this.uuid().substring(0, length);
}
return code;
},
bigIntegerId() {
return [Date.now(), (Math.floor(Math.random() * 1000000) + "").padStart(6, "0")].join("");
},
ucFirst(str: string) {
if (!str) return "";
return str.charAt(0).toUpperCase() + str.slice(1);
},
};
export const TimeUtil = {
timestampInMs() {
return Date.now();
},
timestamp() {
return Math.floor(Date.now() / 1000);
},
format(time: number, format: string = "YYYY-MM-DD HH:mm:ss") {
return dayjs(time).format(format);
},
formatDate(time: number) {
return dayjs(time).format("YYYY-MM-DD");
},
dateString() {
return dayjs().format("YYYYMMDD");
},
datetimeString() {
return dayjs().format("YYYYMMDD_HHmmss_SSS");
},
timestampDayStart(msTimestamp?: number) {
let date = msTimestamp ? new Date(msTimestamp) : new Date();
date.setHours(0, 0, 0, 0);
return Math.floor(date.getTime() / 1000);
},
replacePattern(text: string) {
// @ts-ignore
return text.replaceAll("{year}", dayjs().format("YYYY"))
.replaceAll("{month}", dayjs().format("MM"))
.replaceAll("{day}", dayjs().format("DD"))
.replaceAll("{hour}", dayjs().format("HH"))
.replaceAll("{minute}", dayjs().format("mm"))
.replaceAll("{second}", dayjs().format("ss"));
},
};
export const FileUtil = {
MIME_TYPES: {
html: "text/html",
htm: "text/html",
js: "application/javascript",
css: "text/css",
json: "application/json",
png: "image/png",
jpg: "image/jpeg",
jpeg: "image/jpeg",
gif: "image/gif",
svg: "image/svg+xml",
webp: "image/webp",
woff: "font/woff",
woff2: "font/woff2",
ttf: "font/ttf",
otf: "font/otf",
mp3: "audio/mpeg",
mp4: "video/mp4",
wav: "audio/wav",
wasm: "application/wasm",
eot: "application/vnd.ms-fontobject",
},
getMimeByExt(ext: string, defaultMime: string = ""): string {
ext = ext.toLowerCase();
if (ext.startsWith(".")) {
ext = ext.substring(1);
}
return FileUtil.MIME_TYPES[ext] || defaultMime;
},
getMimeByPath(p: string, defaultMime: string = ""): string {
const extension = p.split(".").pop().toLowerCase();
return FileUtil.getMimeByExt(extension, defaultMime);
},
streamToBase64(stream: NodeJS.ReadableStream): Promise<string> {
return new Promise((resolve, reject) => {
const chunks = [];
stream.on("data", chunk => {
chunks.push(chunk);
});
stream.on("end", () => {
const buffer = Buffer.concat(chunks);
resolve(buffer.toString("base64"));
});
stream.on("error", error => {
reject(error);
});
});
},
bufferToBase64(buffer: Buffer) {
let binary = "";
let bytes = new Uint8Array(buffer);
let len = bytes.byteLength;
for (let i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return EncodeUtil.base64Encode(binary);
},
base64ToBuffer(base64: string): Buffer {
if (base64.startsWith("data:")) {
base64 = base64.split("base64,")[1];
}
return Buffer.from(base64, "base64");
},
formatSize(size: number) {
if (size < 1024) {
return size + "B";
} else if (size < 1024 * 1024) {
return (size / 1024).toFixed(2) + "KB";
} else if (size < 1024 * 1024 * 1024) {
return (size / 1024 / 1024).toFixed(2) + "MB";
} else {
return (size / 1024 / 1024 / 1024).toFixed(2) + "GB";
}
},
async md5(filePath: string) {
return new Promise((resolve, reject) => {
const hash = crypto.createHash("md5");
const stream = fs.createReadStream(filePath);
stream.on("data", data => {
hash.update(data);
});
stream.on("end", () => {
resolve(hash.digest("hex"));
});
stream.on("error", error => {
reject(error);
});
});
},
};
export const JsonUtil = {
stringifyOrdered(obj: any) {
return JSON.stringify(obj, Object.keys(obj).sort(), 4);
},
stringifyValueOrdered(obj: any) {
const sortedData = Object.fromEntries(
Object.entries(obj).sort(([, a], [, b]) => {
// @ts-ignore
return ((a as any) - b) as any;
})
);
return JSON.stringify(sortedData, null, 4);
},
};
export const ImportUtil = {
async loadCommonJs(cjsPath: string, forceReload: boolean = true) {
let tempPath = cjsPath;
if (forceReload) {
const md5 = await FileUtil.md5(cjsPath);
tempPath = path.join(
await FileIndex.tempDir('commonJs'),
`${md5}.cjs`,
)
if (!fs.existsSync(tempPath)) {
fs.copyFileSync(cjsPath, tempPath);
}
}
const backend = await import(/* @vite-ignore */ `file://${tempPath}`);
// console.log('loadCommonJs', `${cjsPath}?t=${md5}`)
return backend.default;
},
};
export const MemoryCacheUtil = {
pool: {} as {
[key: string]: {
value: any;
expire: number;
};
},
_gc() {
const now = TimeUtil.timestamp();
for (const key in this.pool) {
if (this.pool[key].expire < now) {
delete this.pool[key];
}
}
},
async remember(key: string, callback: () => Promise<any>, ttl: number = 60) {
if (this.pool[key] && this.pool[key].expire > TimeUtil.timestamp()) {
return this.pool[key].value;
}
const value = await callback();
this.pool[key] = {
value,
expire: TimeUtil.timestamp() + ttl,
};
this._gc();
return value;
},
get(key: string) {
if (this.pool[key] && this.pool[key].expire > TimeUtil.timestamp()) {
return this.pool[key].value;
}
this._gc();
return null;
},
set(key: string, value: any, ttl: number = 86400) {
this.pool[key] = {
value,
expire: TimeUtil.timestamp() + ttl,
};
this._gc();
},
forget(key: string) {
delete this.pool[key];
},
};
export const MemoryMapCacheUtil = {
pool: {} as {
[group: string]: {
[key: string]: {
value: any;
expire: number;
};
};
},
_gc() {
const now = TimeUtil.timestamp();
for (const group in this.pool) {
for (const key in this.pool[group]) {
if (this.pool[group][key].expire < now) {
delete this.pool[group][key];
}
}
}
},
get(group: string, key: string) {
if (this.pool[group] && this.pool[group][key] && this.pool[group][key].expire > TimeUtil.timestamp()) {
return this.pool[group][key].value;
}
this._gc();
return null;
},
set(group: string, key: string, value: any, ttl: number = 86400) {
if (!this.pool[group]) {
this.pool[group] = {};
}
this.pool[group][key] = {
value,
expire: TimeUtil.timestamp() + ttl,
};
this._gc();
},
forget(group: string, key: string) {
if (this.pool[group]) {
delete this.pool[group][key];
}
},
};
export const ShellUtil = {
quotaPath(p: string) {
return `"${p}"`;
},
parseCommandArgs(command: string) {
let args = [];
let arg = "";
let quote = "";
let escape = false;
for (let i = 0; i < command.length; i++) {
const c = command[i];
if (escape) {
arg += c;
escape = false;
continue;
}
if ("\\" === c) {
escape = true;
arg += c;
continue;
}
if ("" === quote && (" " === c || "\t" === c)) {
if (arg) {
args.push(arg);
arg = "";
}
continue;
}
if ("" === quote && ('"' === c || "'" === c)) {
quote = c;
arg += c;
continue;
}
if ('"' === quote && '"' === c) {
quote = "";
arg += c;
continue;
}
if ("'" === quote && "'" === c) {
quote = "";
arg += c;
continue;
}
arg += c;
}
if (arg) {
args.push(arg);
}
return args;
},
};
export const VersionUtil = {
/**
* 检测版本是否匹配
* @param v string
* @param match string 如 * 或 >=1.0.0 或 >1.0.0 或 <1.0.0 或 <=1.0.0 或 1.0.0
*/
match(v: string, match: string) {
if (match === "*") {
return true;
}
if (match.startsWith(">=")) {
if (this.ge(v, match.substring(2))) {
return true;
}
} else if (match.startsWith("<=")) {
if (this.le(v, match.substring(2))) {
return true;
}
} else if (match.startsWith(">")) {
if (this.gt(v, match.substring(1))) {
return true;
}
} else if (match.startsWith("<")) {
if (this.lt(v, match.substring(1))) {
return true;
}
} else {
return this.eq(v, match);
}
return false;
},
compare(v1: string, v2: string) {
const v1Arr = v1.split(".");
const v2Arr = v2.split(".");
for (let i = 0; i < v1Arr.length; i++) {
const v1Num = parseInt(v1Arr[i]);
const v2Num = parseInt(v2Arr[i]);
if (v1Num > v2Num) {
return 1;
} else if (v1Num < v2Num) {
return -1;
}
}
return 0;
},
gt(v1: string, v2: string) {
return VersionUtil.compare(v1, v2) > 0;
},
ge(v1: string, v2: string) {
return VersionUtil.compare(v1, v2) >= 0;
},
lt(v1: string, v2: string) {
return VersionUtil.compare(v1, v2) < 0;
},
le: (v1: string, v2: string) => {
return VersionUtil.compare(v1, v2) <= 0;
},
eq: (v1: string, v2: string) => {
return VersionUtil.compare(v1, v2) === 0;
},
};
export const UIUtil = {
sizeToPx(size: string, sizeFull: number) {
if (/^\d+$/.test(size)) {
// 纯数字
return parseInt(size);
} else if (size.endsWith("%")) {
// 百分比
let result = Math.floor((sizeFull * parseInt(size)) / 100);
result = Math.min(result, sizeFull);
return result;
} else {
throw "UnsupportSizeString";
}
},
};
export const ReUtil = {
match(regex: string, text: string) {
if ("" === regex || null === regex) {
return false;
}
if (regex.startsWith("/")) {
const index = regex.lastIndexOf("/");
const source = regex.slice(1, index);
const flags = regex.slice(index + 1);
return new RegExp(source, flags).test(text);
}
return new RegExp(regex).test(text);
},
};
const converter = new Showdown.Converter({
tables: true,
});
export const MarkdownUtil = {
toHtml(markdown: string): string {
return converter.makeHtml(markdown);
},
};
type HotkeyModifierType = "Control" | "Option" | "Command" | "Ctrl" | "Alt" | "Win" | "Meta" | "Shift";
type HotkeyType = { key: string; modifiers: HotkeyModifierType[] };
export const HotKeyUtil = {
orderModifiers(modifiers: HotkeyModifierType[]) {
const order = ["Control", "Ctrl", "Command", "Meta", "Win", "Option", "Alt", "Shift"];
return modifiers.sort((a, b) => {
return order.indexOf(a) - order.indexOf(b);
});
},
unifyObject(hotkey: HotkeyType) {
return {
key: hotkey.key.toUpperCase(),
modifiers: this.orderModifiers(hotkey.modifiers.map(modifier => StrUtil.ucFirst(modifier))),
};
},
unifyString(hotkey: string): HotkeyType {
const parts = hotkey.split("+");
const key = parts.pop() || "";
const modifiers: any[] = [];
parts.forEach(part => {
modifiers.push(StrUtil.ucFirst(part.trim()));
});
return this.unifyObject({key, modifiers});
},
unify(hotkeys: string | string[] | HotkeyType | HotkeyType[]): HotkeyType[] {
if (typeof hotkeys === "string") {
return [this.unifyString(hotkeys)];
} else if (Array.isArray(hotkeys)) {
return hotkeys.map(hotkey => {
if (typeof hotkey === "string") {
return this.unifyString(hotkey);
} else {
return this.unifyObject(hotkey);
}
});
} else {
return [this.unifyObject(hotkeys)];
}
},
getFromEvent(event: any): HotkeyType | null {
const valid = [
"A",
"B",
"C",
"D",
"E",
"F",
"G",
"H",
"I",
"J",
"K",
"L",
"M",
"N",
"O",
"P",
"Q",
"R",
"S",
"T",
"U",
"V",
"W",
"X",
"Y",
"Z",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"0",
"Space",
];
const key = (event.key || "").toUpperCase();
if (!event || !event.key || !valid.includes(key)) {
return null;
}
const modifiers: HotkeyModifierType[] = [];
if (isWin) {
if (event.ctrlKey || event.control) {
modifiers.push("Ctrl");
}
if (event.altKey || event.alt) {
modifiers.push("Alt");
}
if (event.metaKey || event.meta) {
modifiers.push("Win");
}
} else if (isMac) {
if (event.ctrlKey || event.control) {
modifiers.push("Control");
}
if (event.altKey || event.alt) {
modifiers.push("Option");
}
if (event.metaKey || event.meta) {
modifiers.push("Command");
}
} else {
if (event.ctrlKey || event.control) {
modifiers.push("Ctrl");
}
if (event.altKey || event.alt) {
modifiers.push("Alt");
}
if (event.metaKey || event.meta) {
modifiers.push("Meta");
}
}
if (event.shiftKey || event.shift) {
modifiers.push("Shift");
}
return this.unifyObject({key, modifiers});
},
match(hotkeysForMatch: HotkeyType[], hotkey: HotkeyType): boolean {
if (!hotkeysForMatch || !hotkey) {
return false;
}
const hotKeyStr = hotkey.modifiers.join("+") + "+" + hotkey.key;
for (const key of hotkeysForMatch) {
const keyStr = key.modifiers.join("+") + "+" + key.key;
if (keyStr === hotKeyStr) {
return true;
}
}
return false;
},
};