NapCatQQ/packages/napcat-common/src/helper.ts
手瓜一十雪 173a165c4b Add latest version check and update prompt to UI
Introduces backend and frontend logic to fetch the latest NapCat version tag from multiple sources, exposes a new API endpoint, and adds a UI prompt to notify users of new versions with an update button. Also includes minor code style improvements in dialog context.
2025-11-22 13:52:49 +08:00

292 lines
8.5 KiB
TypeScript

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<T extends (...arg: any[]) => any> (func: T, ...args: Parameters<T>): Promise<ReturnType<T> | undefined> {
return new Promise<ReturnType<T> | undefined>((resolve) => {
try {
const result = func(...args);
resolve(result);
} catch {
resolve(undefined);
}
});
}
export async function solveAsyncProblem<T extends (...args: any[]) => Promise<any>> (func: T, ...args: Parameters<T>): Promise<Awaited<ReturnType<T>> | undefined> {
return new Promise<Awaited<ReturnType<T>> | undefined>((resolve) => {
func(...args).then((result) => {
resolve(result);
}).catch(() => {
resolve(undefined);
});
});
}
export function sleep (ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export function PromiseTimer<T> (promise: Promise<T>, ms: number): Promise<T> {
const timeoutPromise = new Promise<T>((_resolve, reject) =>
setTimeout(() => reject(new Error('PromiseTimer: Operation timed out')), ms)
);
return Promise.race([promise, timeoutPromise]);
}
export async function runAllWithTimeout<T> (tasks: Promise<T>[], timeout: number): Promise<T[]> {
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<boolean> {
try {
await PromiseTimer(RequestUtil.HttpGetText(url), 5000);
return true;
} catch {
return false;
}
}
async function findAvailableUrl (): Promise<string | null> {
for (const url of urls) {
if (await testUrl(url)) {
return url;
}
}
return null;
}
export async function getAllTags (): Promise<string[]> {
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<string> {
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;
}