import path from 'node:path'; import fs from 'fs'; import os from 'node:os'; import { QQVersionConfigType, QQLevel } from './types'; import { RequestUtil } from './request'; export async function solveProblem any> (func: T, ...args: Parameters): Promise | undefined> { return new Promise | undefined>((resolve) => { try { const result = func(...args); resolve(result); } catch { resolve(undefined); } }); } export async function solveAsyncProblem Promise> (func: T, ...args: Parameters): Promise> | undefined> { return new Promise> | undefined>((resolve) => { func(...args).then((result) => { resolve(result); }).catch(() => { resolve(undefined); }); }); } export function sleep (ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } export function PromiseTimer (promise: Promise, ms: number): Promise { const timeoutPromise = new Promise((_resolve, reject) => setTimeout(() => reject(new Error('PromiseTimer: Operation timed out')), ms) ); return Promise.race([promise, timeoutPromise]); } export async function runAllWithTimeout (tasks: Promise[], timeout: number): Promise { const wrappedTasks = tasks.map((task) => PromiseTimer(task, timeout).then( (result) => ({ status: 'fulfilled', value: result }), (error) => ({ status: 'rejected', reason: error }) ) ); const results = await Promise.all(wrappedTasks); return results .filter((result) => result.status === 'fulfilled') .map((result) => (result as { status: 'fulfilled'; value: T; }).value); } export function isNull (value: any) { return value === undefined || value === null; } export function isNumeric (str: string) { return /^\d+$/.test(str); } export function truncateString (obj: any, maxLength = 500) { if (obj !== null && typeof obj === 'object') { Object.keys(obj).forEach((key) => { if (typeof obj[key] === 'string') { // 如果是字符串且超过指定长度,则截断 if (obj[key].length > maxLength) { obj[key] = obj[key].substring(0, maxLength) + '...'; } } else if (typeof obj[key] === 'object') { // 如果是对象或数组,则递归调用 truncateString(obj[key], maxLength); } }); } return obj; } export function isEqual (obj1: any, obj2: any) { if (obj1 === obj2) return true; if (obj1 == null || obj2 == null) return false; if (typeof obj1 !== 'object' || typeof obj2 !== 'object') return obj1 === obj2; const keys1 = Object.keys(obj1); const keys2 = Object.keys(obj2); if (keys1.length !== keys2.length) return false; for (const key of keys1) { if (!isEqual(obj1[key], obj2[key])) return false; } return true; } export function getDefaultQQVersionConfigInfo (): QQVersionConfigType { if (os.platform() === 'linux') { return { baseVersion: '3.2.12.28060', curVersion: '3.2.12.28060', prevVersion: '', onErrorVersions: [], buildId: '27254', }; } if (os.platform() === 'darwin') { return { baseVersion: '6.9.53.28060', curVersion: '6.9.53.28060', prevVersion: '', onErrorVersions: [], buildId: '28060', }; } return { baseVersion: '9.9.15-28131', curVersion: '9.9.15-28131', prevVersion: '', onErrorVersions: [], buildId: '28131', }; } export function getQQPackageInfoPath (exePath: string = '', version?: string): string { if (process.env['NAPCAT_QQ_PACKAGE_INFO_PATH']) { return process.env['NAPCAT_QQ_PACKAGE_INFO_PATH']; } let packagePath; if (os.platform() === 'darwin') { packagePath = path.join(path.dirname(exePath), '..', 'Resources', 'app', 'package.json'); } else if (os.platform() === 'linux') { packagePath = path.join(path.dirname(exePath), './resources/app/package.json'); } else { packagePath = path.join(path.dirname(exePath), './versions/' + version + '/resources/app/package.json'); } // 下面是老版本兼容 未来去掉 if (!fs.existsSync(packagePath)) { packagePath = path.join(path.dirname(exePath), './resources/app/versions/' + version + '/package.json'); } return packagePath; } export function getQQVersionConfigPath (exePath: string = ''): string | undefined { if (process.env['NAPCAT_QQ_VERSION_CONFIG_PATH']) { return process.env['NAPCAT_QQ_VERSION_CONFIG_PATH']; } let configVersionInfoPath; if (os.platform() === 'win32') { configVersionInfoPath = path.join(path.dirname(exePath), 'versions', 'config.json'); } else if (os.platform() === 'darwin') { const userPath = os.homedir(); const appDataPath = path.resolve(userPath, './Library/Application Support/QQ'); configVersionInfoPath = path.resolve(appDataPath, './versions/config.json'); } else { const userPath = os.homedir(); const appDataPath = path.resolve(userPath, './.config/QQ'); configVersionInfoPath = path.resolve(appDataPath, './versions/config.json'); } if (typeof configVersionInfoPath !== 'string') { return undefined; } // 老版本兼容 未来去掉 if (!fs.existsSync(configVersionInfoPath)) { configVersionInfoPath = path.join(path.dirname(exePath), './resources/app/versions/config.json'); } if (!fs.existsSync(configVersionInfoPath)) { return undefined; } return configVersionInfoPath; } export function calcQQLevel (level?: QQLevel) { if (!level) return 0; // const { penguinNum, crownNum, sunNum, moonNum, starNum } = level; const { crownNum, sunNum, moonNum, starNum } = level; // 没补类型 return crownNum * 64 + sunNum * 16 + moonNum * 4 + starNum; } export function stringifyWithBigInt (obj: any) { return JSON.stringify(obj, (_key, value) => typeof value === 'bigint' ? value.toString() : value ); } export function parseAppidFromMajor (nodeMajor: string): string | undefined { const hexSequence = 'A4 09 00 00 00 35'; const sequenceBytes = Buffer.from(hexSequence.replace(/ /g, ''), 'hex'); const filePath = path.resolve(nodeMajor); const fileContent = fs.readFileSync(filePath); let searchPosition = 0; while (true) { const index = fileContent.indexOf(sequenceBytes, searchPosition); if (index === -1) { break; } const start = index + sequenceBytes.length - 1; const end = fileContent.indexOf(0x00, start); if (end === -1) { break; } const content = fileContent.subarray(start, end); if (!content.every(byte => byte === 0x00)) { try { return content.toString('utf-8'); } catch { break; } } searchPosition = end + 1; } return undefined; } const baseUrl = 'https://github.com/NapNeko/NapCatQQ.git/info/refs?service=git-upload-pack'; const urls = [ 'https://j.1win.ggff.net/' + baseUrl, 'https://git.yylx.win/' + baseUrl, 'https://ghfile.geekertao.top/' + baseUrl, 'https://gh-proxy.net/' + baseUrl, 'https://ghm.078465.xyz/' + baseUrl, 'https://gitproxy.127731.xyz/' + baseUrl, 'https://jiashu.1win.eu.org/' + baseUrl, baseUrl, ]; async function testUrl (url: string): Promise { try { await PromiseTimer(RequestUtil.HttpGetText(url), 5000); return true; } catch { return false; } } async function findAvailableUrl (): Promise { for (const url of urls) { if (await testUrl(url)) { return url; } } return null; } export async function getAllTags (): Promise { const availableUrl = await findAvailableUrl(); if (!availableUrl) { throw new Error('No available URL for fetching tags'); } const raw = await RequestUtil.HttpGetText(availableUrl); return raw .split('\n') .map(line => { const match = line.match(/refs\/tags\/(.+)$/); return match ? match[1] : null; }) .filter(tag => tag !== null && !tag!.endsWith('^{}')) as string[]; } export async function getLatestTag (): Promise { const tags = await getAllTags(); tags.sort((a, b) => compareVersion(a, b)); const latest = tags.at(-1); if (!latest) { throw new Error('No tags found'); } return latest; } function compareVersion (a: string, b: string): number { const normalize = (v: string) => v.replace(/^v/, '') // 去掉开头的 v .split('.') .map(n => parseInt(n) || 0); const pa = normalize(a); const pb = normalize(b); const len = Math.max(pa.length, pb.length); for (let i = 0; i < len; i++) { const na = pa[i] || 0; const nb = pb[i] || 0; if (na !== nb) return na - nb; } return 0; }