mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2025-12-18 20:30:08 +08:00
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.
292 lines
8.5 KiB
TypeScript
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;
|
|
}
|