mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2025-07-19 12:03:37 +00:00
2
.env.universal
Normal file
2
.env.universal
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
VITE_BUILD_TYPE = Production
|
||||||
|
VITE_BUILD_PLATFORM = Universal
|
@@ -4,9 +4,11 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "4.2.7",
|
"version": "4.2.7",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"build:universal": "npm run build:webui && vite build --mode universal || exit 1",
|
||||||
"build:framework": "npm run build:webui && vite build --mode framework || exit 1",
|
"build:framework": "npm run build:webui && vite build --mode framework || exit 1",
|
||||||
"build:shell": "npm run build:webui && vite build --mode shell || exit 1",
|
"build:shell": "npm run build:webui && vite build --mode shell || exit 1",
|
||||||
"build:webui": "cd napcat.webui && vite build",
|
"build:webui": "cd napcat.webui && vite build",
|
||||||
|
"dev:universal": "vite build --mode universal",
|
||||||
"dev:framework": "vite build --mode framework",
|
"dev:framework": "vite build --mode framework",
|
||||||
"dev:shell": "vite build --mode shell",
|
"dev:shell": "vite build --mode shell",
|
||||||
"dev:webui": "cd napcat.webui && npm run webui:dev",
|
"dev:webui": "cd napcat.webui && npm run webui:dev",
|
||||||
|
366
src/shell/base.ts
Normal file
366
src/shell/base.ts
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
import type { SelfInfo } from '@/core/types';
|
||||||
|
|
||||||
|
import { LogWrapper } from '@/common/log';
|
||||||
|
import { NodeIKernelLoginListener, NodeIKernelSessionListener } from '@/core/listeners';
|
||||||
|
import { NodeIDependsAdapter, NodeIDispatcherAdapter, NodeIGlobalAdapter } from '@/core/adapters';
|
||||||
|
import { NapCatPathWrapper } from '@/common/path';
|
||||||
|
import {
|
||||||
|
genSessionConfig,
|
||||||
|
InstanceContext,
|
||||||
|
loadQQWrapper,
|
||||||
|
NapCatCore,
|
||||||
|
NapCatCoreWorkingEnv,
|
||||||
|
NodeIQQNTWrapperSession,
|
||||||
|
PlatformType,
|
||||||
|
WrapperNodeApi,
|
||||||
|
} from '@/core';
|
||||||
|
import { QQBasicInfoWrapper } from '@/common/qq-basic-info';
|
||||||
|
import { hostname, systemVersion } from '@/common/system';
|
||||||
|
import { proxiedListenerOf } from '@/common/proxy-handler';
|
||||||
|
|
||||||
|
import path from 'path';
|
||||||
|
import fs from 'fs';
|
||||||
|
import os from 'os';
|
||||||
|
import { NodeIKernelLoginService } from '@/core/services';
|
||||||
|
import { program } from 'commander';
|
||||||
|
import qrcode from 'qrcode-terminal';
|
||||||
|
import { NapCatOneBot11Adapter } from '@/onebot';
|
||||||
|
import { InitWebUi } from '@/webui';
|
||||||
|
import { WebUiDataRuntime } from '@/webui/src/helper/Data';
|
||||||
|
import { napCatVersion } from '@/common/version';
|
||||||
|
import { NodeIO3MiscListener } from '@/core/listeners/NodeIO3MiscListener';
|
||||||
|
// NapCat Shell App ES 入口文件
|
||||||
|
async function handleUncaughtExceptions(logger: LogWrapper) {
|
||||||
|
process.on('uncaughtException', (err) => {
|
||||||
|
logger.logError('[NapCat] [Error] Unhandled Exception:', err.message);
|
||||||
|
});
|
||||||
|
process.on('unhandledRejection', (reason, promise) => {
|
||||||
|
logger.logError('[NapCat] [Error] unhandledRejection:', reason);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDataPaths(wrapper: WrapperNodeApi): [string, string] {
|
||||||
|
if (os.platform() === 'darwin') {
|
||||||
|
const userPath = os.homedir();
|
||||||
|
const appDataPath = path.resolve(userPath, './Library/Application Support/QQ');
|
||||||
|
return [appDataPath, path.join(appDataPath, 'global')];
|
||||||
|
}
|
||||||
|
let dataPath = wrapper.NodeQQNTWrapperUtil.getNTUserDataInfoConfig();
|
||||||
|
if (!dataPath) {
|
||||||
|
dataPath = path.resolve(os.homedir(), './.config/QQ');
|
||||||
|
fs.mkdirSync(dataPath, { recursive: true });
|
||||||
|
}
|
||||||
|
const dataPathGlobal = path.resolve(dataPath, './nt_qq/global');
|
||||||
|
return [dataPath, dataPathGlobal];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPlatformType(): PlatformType {
|
||||||
|
const platformMapping: Partial<Record<NodeJS.Platform, PlatformType>> = {
|
||||||
|
win32: PlatformType.KWINDOWS,
|
||||||
|
darwin: PlatformType.KMAC,
|
||||||
|
linux: PlatformType.KLINUX,
|
||||||
|
};
|
||||||
|
return platformMapping[os.platform()] ?? PlatformType.KWINDOWS;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initializeEngine(
|
||||||
|
engine: any,
|
||||||
|
basicInfoWrapper: QQBasicInfoWrapper,
|
||||||
|
dataPathGlobal: string,
|
||||||
|
systemPlatform: PlatformType,
|
||||||
|
systemVersion: string
|
||||||
|
) {
|
||||||
|
engine.initWithDeskTopConfig(
|
||||||
|
{
|
||||||
|
base_path_prefix: '',
|
||||||
|
platform_type: systemPlatform,
|
||||||
|
app_type: 4,
|
||||||
|
app_version: basicInfoWrapper.getFullQQVesion(),
|
||||||
|
os_version: systemVersion,
|
||||||
|
use_xlog: false,
|
||||||
|
qua: basicInfoWrapper.QQVersionQua,
|
||||||
|
global_path_config: {
|
||||||
|
desktopGlobalPath: dataPathGlobal,
|
||||||
|
},
|
||||||
|
thumb_config: { maxSide: 324, minSide: 48, longLimit: 6, density: 2 },
|
||||||
|
},
|
||||||
|
new NodeIGlobalAdapter(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initializeLoginService(
|
||||||
|
loginService: NodeIKernelLoginService,
|
||||||
|
basicInfoWrapper: QQBasicInfoWrapper,
|
||||||
|
dataPathGlobal: string,
|
||||||
|
systemVersion: string,
|
||||||
|
hostname: string
|
||||||
|
) {
|
||||||
|
loginService.initConfig({
|
||||||
|
machineId: '',
|
||||||
|
appid: basicInfoWrapper.QQVersionAppid ?? '',
|
||||||
|
platVer: systemVersion,
|
||||||
|
commonPath: dataPathGlobal,
|
||||||
|
clientVer: basicInfoWrapper.getFullQQVesion(),
|
||||||
|
hostName: hostname,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleLogin(
|
||||||
|
loginService: NodeIKernelLoginService,
|
||||||
|
logger: LogWrapper,
|
||||||
|
pathWrapper: NapCatPathWrapper,
|
||||||
|
quickLoginUin: string | undefined,
|
||||||
|
historyLoginList: any[]
|
||||||
|
): Promise<SelfInfo> {
|
||||||
|
return new Promise<SelfInfo>((resolve) => {
|
||||||
|
const loginListener = new NodeIKernelLoginListener();
|
||||||
|
let isLogined = false;
|
||||||
|
|
||||||
|
loginListener.onUserLoggedIn = (userid: string) => {
|
||||||
|
logger.logError(`当前账号(${userid})已登录,无法重复登录`);
|
||||||
|
};
|
||||||
|
|
||||||
|
loginListener.onQRCodeLoginSucceed = async (loginResult) => {
|
||||||
|
isLogined = true;
|
||||||
|
resolve({
|
||||||
|
uid: loginResult.uid,
|
||||||
|
uin: loginResult.uin,
|
||||||
|
nick: '',
|
||||||
|
online: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
loginListener.onQRCodeGetPicture = ({ pngBase64QrcodeData, qrcodeUrl }) => {
|
||||||
|
WebUiDataRuntime.setQQLoginQrcodeURL(qrcodeUrl);
|
||||||
|
|
||||||
|
const realBase64 = pngBase64QrcodeData.replace(/^data:image\/\w+;base64,/, '');
|
||||||
|
const buffer = Buffer.from(realBase64, 'base64');
|
||||||
|
logger.logWarn('请扫描下面的二维码,然后在手Q上授权登录:');
|
||||||
|
const qrcodePath = path.join(pathWrapper.cachePath, 'qrcode.png');
|
||||||
|
qrcode.generate(qrcodeUrl, { small: true }, (res) => {
|
||||||
|
logger.logWarn([
|
||||||
|
'\n',
|
||||||
|
res,
|
||||||
|
'二维码解码URL: ' + qrcodeUrl,
|
||||||
|
'如果控制台二维码无法扫码,可以复制解码url到二维码生成网站生成二维码再扫码,也可以打开下方的二维码路径图片进行扫码。',
|
||||||
|
].join('\n'));
|
||||||
|
fs.writeFile(qrcodePath, buffer, {}, () => {
|
||||||
|
logger.logWarn('二维码已保存到', qrcodePath);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
loginListener.onQRCodeSessionFailed = (errType: number, errCode: number, errMsg: string) => {
|
||||||
|
if (!isLogined) {
|
||||||
|
logger.logError('[Core] [Login] Login Error,ErrCode: ', errCode, ' ErrMsg:', errMsg);
|
||||||
|
if (errType == 1 && errCode == 3) {
|
||||||
|
// 二维码过期刷新
|
||||||
|
}
|
||||||
|
loginService.getQRCodePicture();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loginListener.onLoginFailed = (args) => {
|
||||||
|
logger.logError('[Core] [Login] Login Error , ErrInfo: ', args);
|
||||||
|
};
|
||||||
|
|
||||||
|
loginService.addKernelLoginListener(proxiedListenerOf(loginListener, logger));
|
||||||
|
const isConnect = loginService.connect();
|
||||||
|
if (!isConnect) {
|
||||||
|
logger.logError('核心登录服务连接失败!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log('核心登录服务连接成功!');
|
||||||
|
|
||||||
|
loginService.getLoginList().then((res) => {
|
||||||
|
// 遍历 res.LocalLoginInfoList[x].isQuickLogin是否可以 res.LocalLoginInfoList[x].uin 转为string 加入string[] 最后遍历完成调用WebUiDataRuntime.setQQQuickLoginList
|
||||||
|
WebUiDataRuntime.setQQQuickLoginList(res.LocalLoginInfoList.filter((item) => item.isQuickLogin).map((item) => item.uin.toString()));
|
||||||
|
});
|
||||||
|
|
||||||
|
WebUiDataRuntime.setQuickLoginCall(async (uin: string) => {
|
||||||
|
return await new Promise((resolve) => {
|
||||||
|
if (uin) {
|
||||||
|
logger.log('正在快速登录 ', uin);
|
||||||
|
loginService.quickLoginWithUin(uin).then(res => {
|
||||||
|
if (res.loginErrorInfo.errMsg) {
|
||||||
|
resolve({ result: false, message: res.loginErrorInfo.errMsg });
|
||||||
|
}
|
||||||
|
resolve({ result: true, message: '' });
|
||||||
|
}).catch((e) => {
|
||||||
|
logger.logError(e);
|
||||||
|
resolve({ result: false, message: '快速登录发生错误' });
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve({ result: false, message: '快速登录失败' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (quickLoginUin) {
|
||||||
|
if (historyLoginList.some(u => u.uin === quickLoginUin)) {
|
||||||
|
logger.log('正在快速登录 ', quickLoginUin);
|
||||||
|
setTimeout(() => {
|
||||||
|
loginService.quickLoginWithUin(quickLoginUin)
|
||||||
|
.then(result => {
|
||||||
|
if (result.loginErrorInfo.errMsg) {
|
||||||
|
logger.logError('快速登录错误:', result.loginErrorInfo.errMsg);
|
||||||
|
if (!isLogined) loginService.getQRCodePicture();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch();
|
||||||
|
}, 1000);
|
||||||
|
} else {
|
||||||
|
logger.logError('快速登录失败,未找到该 QQ 历史登录记录,将使用二维码登录方式');
|
||||||
|
if (!isLogined) loginService.getQRCodePicture();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.log('没有 -q 指令指定快速登录,将使用二维码登录方式');
|
||||||
|
if (historyLoginList.length > 0) {
|
||||||
|
logger.log(`可用于快速登录的 QQ:\n${historyLoginList
|
||||||
|
.map((u, index) => `${index + 1}. ${u.uin} ${u.nickName}`)
|
||||||
|
.join('\n')
|
||||||
|
}`);
|
||||||
|
}
|
||||||
|
loginService.getQRCodePicture();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initializeSession(
|
||||||
|
session: NodeIQQNTWrapperSession,
|
||||||
|
sessionConfig: any,
|
||||||
|
logger: LogWrapper
|
||||||
|
) {
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
const sessionListener = new NodeIKernelSessionListener();
|
||||||
|
sessionListener.onSessionInitComplete = (r: unknown) => {
|
||||||
|
if (r === 0) {
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
reject(new Error('登录异常' + r?.toString()));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
session.init(
|
||||||
|
sessionConfig,
|
||||||
|
new NodeIDependsAdapter(),
|
||||||
|
new NodeIDispatcherAdapter(),
|
||||||
|
sessionListener,
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
session.startNT(0);
|
||||||
|
} catch (_) {
|
||||||
|
try {
|
||||||
|
session.startNT();
|
||||||
|
} catch (e: unknown) {
|
||||||
|
reject(new Error('init failed ' + (e as Error).message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function NCoreInitShell() {
|
||||||
|
console.log('NapCat Shell App Loading...');
|
||||||
|
const pathWrapper = new NapCatPathWrapper();
|
||||||
|
const logger = new LogWrapper(pathWrapper.logsPath);
|
||||||
|
handleUncaughtExceptions(logger);
|
||||||
|
|
||||||
|
const basicInfoWrapper = new QQBasicInfoWrapper({ logger });
|
||||||
|
const wrapper = loadQQWrapper(basicInfoWrapper.getFullQQVesion());
|
||||||
|
|
||||||
|
const o3Service = wrapper.NodeIO3MiscService.get();
|
||||||
|
o3Service.addO3MiscListener(new NodeIO3MiscListener());
|
||||||
|
|
||||||
|
logger.log(`[NapCat] [Core] NapCat.Core Version: ` + napCatVersion);
|
||||||
|
InitWebUi(logger, pathWrapper).then().catch(e => logger.logError(e));
|
||||||
|
|
||||||
|
const engine = wrapper.NodeIQQNTWrapperEngine.get();
|
||||||
|
const loginService = wrapper.NodeIKernelLoginService.get();
|
||||||
|
const session = wrapper.NodeIQQNTWrapperSession.create();
|
||||||
|
|
||||||
|
const [dataPath, dataPathGlobal] = getDataPaths(wrapper);
|
||||||
|
const systemPlatform = getPlatformType();
|
||||||
|
|
||||||
|
if (!basicInfoWrapper.QQVersionAppid || !basicInfoWrapper.QQVersionQua) throw new Error('QQVersionAppid or QQVersionQua is not defined');
|
||||||
|
|
||||||
|
await initializeEngine(engine, basicInfoWrapper, dataPathGlobal, systemPlatform, systemVersion);
|
||||||
|
await initializeLoginService(loginService, basicInfoWrapper, dataPathGlobal, systemVersion, hostname);
|
||||||
|
|
||||||
|
program.option('-q, --qq [number]', 'QQ号').parse(process.argv);
|
||||||
|
const cmdOptions = program.opts();
|
||||||
|
const quickLoginUin = cmdOptions.qq;
|
||||||
|
const historyLoginList = (await loginService.getLoginList()).LocalLoginInfoList;
|
||||||
|
|
||||||
|
const dataTimestape = new Date().getTime().toString();
|
||||||
|
o3Service.reportAmgomWeather('login', 'a1', [dataTimestape, '0', '0']);
|
||||||
|
|
||||||
|
const selfInfo = await handleLogin(loginService, logger, pathWrapper, quickLoginUin, historyLoginList);
|
||||||
|
|
||||||
|
const amgomDataPiece = 'eb1fd6ac257461580dc7438eb099f23aae04ca679f4d88f53072dc56e3bb1129';
|
||||||
|
o3Service.setAmgomDataPiece(basicInfoWrapper.QQVersionAppid, new Uint8Array(Buffer.from(amgomDataPiece, 'hex')));
|
||||||
|
|
||||||
|
let guid = loginService.getMachineGuid();
|
||||||
|
guid = guid.slice(0, 8) + '-' + guid.slice(8, 12) + '-' + guid.slice(12, 16) + '-' + guid.slice(16, 20) + '-' + guid.slice(20);
|
||||||
|
o3Service.reportAmgomWeather('login', 'a6', [dataTimestape, '184', '329']);
|
||||||
|
|
||||||
|
const sessionConfig = await genSessionConfig(
|
||||||
|
guid,
|
||||||
|
basicInfoWrapper.QQVersionAppid,
|
||||||
|
basicInfoWrapper.getFullQQVesion(),
|
||||||
|
selfInfo.uin,
|
||||||
|
selfInfo.uid,
|
||||||
|
dataPath,
|
||||||
|
);
|
||||||
|
|
||||||
|
await initializeSession(session, sessionConfig, logger);
|
||||||
|
|
||||||
|
const accountDataPath = path.resolve(dataPath, './NapCat/data');
|
||||||
|
fs.mkdirSync(dataPath, { recursive: true });
|
||||||
|
logger.logDebug('本账号数据/缓存目录:', accountDataPath);
|
||||||
|
|
||||||
|
await new NapCatShell(
|
||||||
|
wrapper,
|
||||||
|
session,
|
||||||
|
logger,
|
||||||
|
loginService,
|
||||||
|
selfInfo,
|
||||||
|
basicInfoWrapper,
|
||||||
|
pathWrapper,
|
||||||
|
).InitNapCat();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export class NapCatShell {
|
||||||
|
readonly core: NapCatCore;
|
||||||
|
readonly context: InstanceContext;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
wrapper: WrapperNodeApi,
|
||||||
|
session: NodeIQQNTWrapperSession,
|
||||||
|
logger: LogWrapper,
|
||||||
|
loginService: NodeIKernelLoginService,
|
||||||
|
selfInfo: SelfInfo,
|
||||||
|
basicInfoWrapper: QQBasicInfoWrapper,
|
||||||
|
pathWrapper: NapCatPathWrapper,
|
||||||
|
) {
|
||||||
|
this.context = {
|
||||||
|
workingEnv: NapCatCoreWorkingEnv.Shell,
|
||||||
|
wrapper,
|
||||||
|
session,
|
||||||
|
logger,
|
||||||
|
loginService,
|
||||||
|
basicInfoWrapper,
|
||||||
|
pathWrapper,
|
||||||
|
};
|
||||||
|
this.core = new NapCatCore(this.context, selfInfo);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
async InitNapCat() {
|
||||||
|
await this.core.initCore();
|
||||||
|
new NapCatOneBot11Adapter(this.core, this.context, this.context.pathWrapper).InitOneBot()
|
||||||
|
.catch(e => this.context.logger.logError('初始化OneBot失败', e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@@ -1,369 +1,3 @@
|
|||||||
import type { SelfInfo } from '@/core/types';
|
import { NCoreInitShell } from "./base";
|
||||||
|
|
||||||
import { LogWrapper } from '@/common/log';
|
|
||||||
import { NodeIKernelLoginListener, NodeIKernelSessionListener } from '@/core/listeners';
|
|
||||||
import { NodeIDependsAdapter, NodeIDispatcherAdapter, NodeIGlobalAdapter } from '@/core/adapters';
|
|
||||||
import { NapCatPathWrapper } from '@/common/path';
|
|
||||||
import {
|
|
||||||
genSessionConfig,
|
|
||||||
InstanceContext,
|
|
||||||
loadQQWrapper,
|
|
||||||
NapCatCore,
|
|
||||||
NapCatCoreWorkingEnv,
|
|
||||||
NodeIQQNTWrapperSession,
|
|
||||||
PlatformType,
|
|
||||||
WrapperNodeApi,
|
|
||||||
} from '@/core';
|
|
||||||
import { QQBasicInfoWrapper } from '@/common/qq-basic-info';
|
|
||||||
import { hostname, systemVersion } from '@/common/system';
|
|
||||||
import { proxiedListenerOf } from '@/common/proxy-handler';
|
|
||||||
|
|
||||||
import path from 'path';
|
|
||||||
import fs from 'fs';
|
|
||||||
import os from 'os';
|
|
||||||
import { NodeIKernelLoginService } from '@/core/services';
|
|
||||||
import { program } from 'commander';
|
|
||||||
import qrcode from 'qrcode-terminal';
|
|
||||||
import { NapCatOneBot11Adapter } from '@/onebot';
|
|
||||||
import { InitWebUi } from '@/webui';
|
|
||||||
import { WebUiDataRuntime } from '@/webui/src/helper/Data';
|
|
||||||
import { napCatVersion } from '@/common/version';
|
|
||||||
import { NodeIO3MiscListener } from '@/core/listeners/NodeIO3MiscListener';
|
|
||||||
|
|
||||||
program.option('-q, --qq [number]', 'QQ号').parse(process.argv);
|
|
||||||
const cmdOptions = program.opts();
|
|
||||||
|
|
||||||
// NapCat Shell App ES 入口文件
|
|
||||||
async function handleUncaughtExceptions(logger: LogWrapper) {
|
|
||||||
process.on('uncaughtException', (err) => {
|
|
||||||
logger.logError('[NapCat] [Error] Unhandled Exception:', err.message);
|
|
||||||
});
|
|
||||||
process.on('unhandledRejection', (reason, promise) => {
|
|
||||||
logger.logError('[NapCat] [Error] unhandledRejection:', reason);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDataPaths(wrapper: WrapperNodeApi): [string, string] {
|
|
||||||
if (os.platform() === 'darwin') {
|
|
||||||
const userPath = os.homedir();
|
|
||||||
const appDataPath = path.resolve(userPath, './Library/Application Support/QQ');
|
|
||||||
return [appDataPath, path.join(appDataPath, 'global')];
|
|
||||||
}
|
|
||||||
let dataPath = wrapper.NodeQQNTWrapperUtil.getNTUserDataInfoConfig();
|
|
||||||
if (!dataPath) {
|
|
||||||
dataPath = path.resolve(os.homedir(), './.config/QQ');
|
|
||||||
fs.mkdirSync(dataPath, { recursive: true });
|
|
||||||
}
|
|
||||||
const dataPathGlobal = path.resolve(dataPath, './nt_qq/global');
|
|
||||||
return [dataPath, dataPathGlobal];
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPlatformType(): PlatformType {
|
|
||||||
const platformMapping: Partial<Record<NodeJS.Platform, PlatformType>> = {
|
|
||||||
win32: PlatformType.KWINDOWS,
|
|
||||||
darwin: PlatformType.KMAC,
|
|
||||||
linux: PlatformType.KLINUX,
|
|
||||||
};
|
|
||||||
return platformMapping[os.platform()] ?? PlatformType.KWINDOWS;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function initializeEngine(
|
|
||||||
engine: any,
|
|
||||||
basicInfoWrapper: QQBasicInfoWrapper,
|
|
||||||
dataPathGlobal: string,
|
|
||||||
systemPlatform: PlatformType,
|
|
||||||
systemVersion: string
|
|
||||||
) {
|
|
||||||
engine.initWithDeskTopConfig(
|
|
||||||
{
|
|
||||||
base_path_prefix: '',
|
|
||||||
platform_type: systemPlatform,
|
|
||||||
app_type: 4,
|
|
||||||
app_version: basicInfoWrapper.getFullQQVesion(),
|
|
||||||
os_version: systemVersion,
|
|
||||||
use_xlog: false,
|
|
||||||
qua: basicInfoWrapper.QQVersionQua,
|
|
||||||
global_path_config: {
|
|
||||||
desktopGlobalPath: dataPathGlobal,
|
|
||||||
},
|
|
||||||
thumb_config: { maxSide: 324, minSide: 48, longLimit: 6, density: 2 },
|
|
||||||
},
|
|
||||||
new NodeIGlobalAdapter(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function initializeLoginService(
|
|
||||||
loginService: NodeIKernelLoginService,
|
|
||||||
basicInfoWrapper: QQBasicInfoWrapper,
|
|
||||||
dataPathGlobal: string,
|
|
||||||
systemVersion: string,
|
|
||||||
hostname: string
|
|
||||||
) {
|
|
||||||
loginService.initConfig({
|
|
||||||
machineId: '',
|
|
||||||
appid: basicInfoWrapper.QQVersionAppid ?? '',
|
|
||||||
platVer: systemVersion,
|
|
||||||
commonPath: dataPathGlobal,
|
|
||||||
clientVer: basicInfoWrapper.getFullQQVesion(),
|
|
||||||
hostName: hostname,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleLogin(
|
|
||||||
loginService: NodeIKernelLoginService,
|
|
||||||
logger: LogWrapper,
|
|
||||||
pathWrapper: NapCatPathWrapper,
|
|
||||||
quickLoginUin: string | undefined,
|
|
||||||
historyLoginList: any[]
|
|
||||||
): Promise<SelfInfo> {
|
|
||||||
return new Promise<SelfInfo>((resolve) => {
|
|
||||||
const loginListener = new NodeIKernelLoginListener();
|
|
||||||
let isLogined = false;
|
|
||||||
|
|
||||||
loginListener.onUserLoggedIn = (userid: string) => {
|
|
||||||
logger.logError(`当前账号(${userid})已登录,无法重复登录`);
|
|
||||||
};
|
|
||||||
|
|
||||||
loginListener.onQRCodeLoginSucceed = async (loginResult) => {
|
|
||||||
isLogined = true;
|
|
||||||
resolve({
|
|
||||||
uid: loginResult.uid,
|
|
||||||
uin: loginResult.uin,
|
|
||||||
nick: '',
|
|
||||||
online: true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
loginListener.onQRCodeGetPicture = ({ pngBase64QrcodeData, qrcodeUrl }) => {
|
|
||||||
WebUiDataRuntime.setQQLoginQrcodeURL(qrcodeUrl);
|
|
||||||
|
|
||||||
const realBase64 = pngBase64QrcodeData.replace(/^data:image\/\w+;base64,/, '');
|
|
||||||
const buffer = Buffer.from(realBase64, 'base64');
|
|
||||||
logger.logWarn('请扫描下面的二维码,然后在手Q上授权登录:');
|
|
||||||
const qrcodePath = path.join(pathWrapper.cachePath, 'qrcode.png');
|
|
||||||
qrcode.generate(qrcodeUrl, { small: true }, (res) => {
|
|
||||||
logger.logWarn([
|
|
||||||
'\n',
|
|
||||||
res,
|
|
||||||
'二维码解码URL: ' + qrcodeUrl,
|
|
||||||
'如果控制台二维码无法扫码,可以复制解码url到二维码生成网站生成二维码再扫码,也可以打开下方的二维码路径图片进行扫码。',
|
|
||||||
].join('\n'));
|
|
||||||
fs.writeFile(qrcodePath, buffer, {}, () => {
|
|
||||||
logger.logWarn('二维码已保存到', qrcodePath);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
loginListener.onQRCodeSessionFailed = (errType: number, errCode: number, errMsg: string) => {
|
|
||||||
if (!isLogined) {
|
|
||||||
logger.logError('[Core] [Login] Login Error,ErrCode: ', errCode, ' ErrMsg:', errMsg);
|
|
||||||
if (errType == 1 && errCode == 3) {
|
|
||||||
// 二维码过期刷新
|
|
||||||
}
|
|
||||||
loginService.getQRCodePicture();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
loginListener.onLoginFailed = (args) => {
|
|
||||||
logger.logError('[Core] [Login] Login Error , ErrInfo: ', args);
|
|
||||||
};
|
|
||||||
|
|
||||||
loginService.addKernelLoginListener(proxiedListenerOf(loginListener, logger));
|
|
||||||
const isConnect = loginService.connect();
|
|
||||||
if (!isConnect) {
|
|
||||||
logger.logError('核心登录服务连接失败!');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.log('核心登录服务连接成功!');
|
|
||||||
|
|
||||||
loginService.getLoginList().then((res) => {
|
|
||||||
// 遍历 res.LocalLoginInfoList[x].isQuickLogin是否可以 res.LocalLoginInfoList[x].uin 转为string 加入string[] 最后遍历完成调用WebUiDataRuntime.setQQQuickLoginList
|
|
||||||
WebUiDataRuntime.setQQQuickLoginList(res.LocalLoginInfoList.filter((item) => item.isQuickLogin).map((item) => item.uin.toString()));
|
|
||||||
});
|
|
||||||
|
|
||||||
WebUiDataRuntime.setQuickLoginCall(async (uin: string) => {
|
|
||||||
return await new Promise((resolve) => {
|
|
||||||
if (uin) {
|
|
||||||
logger.log('正在快速登录 ', uin);
|
|
||||||
loginService.quickLoginWithUin(uin).then(res => {
|
|
||||||
if (res.loginErrorInfo.errMsg) {
|
|
||||||
resolve({ result: false, message: res.loginErrorInfo.errMsg });
|
|
||||||
}
|
|
||||||
resolve({ result: true, message: '' });
|
|
||||||
}).catch((e) => {
|
|
||||||
logger.logError(e);
|
|
||||||
resolve({ result: false, message: '快速登录发生错误' });
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
resolve({ result: false, message: '快速登录失败' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (quickLoginUin) {
|
|
||||||
if (historyLoginList.some(u => u.uin === quickLoginUin)) {
|
|
||||||
logger.log('正在快速登录 ', quickLoginUin);
|
|
||||||
setTimeout(() => {
|
|
||||||
loginService.quickLoginWithUin(quickLoginUin)
|
|
||||||
.then(result => {
|
|
||||||
if (result.loginErrorInfo.errMsg) {
|
|
||||||
logger.logError('快速登录错误:', result.loginErrorInfo.errMsg);
|
|
||||||
if (!isLogined) loginService.getQRCodePicture();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch();
|
|
||||||
}, 1000);
|
|
||||||
} else {
|
|
||||||
logger.logError('快速登录失败,未找到该 QQ 历史登录记录,将使用二维码登录方式');
|
|
||||||
if (!isLogined) loginService.getQRCodePicture();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logger.log('没有 -q 指令指定快速登录,将使用二维码登录方式');
|
|
||||||
if (historyLoginList.length > 0) {
|
|
||||||
logger.log(`可用于快速登录的 QQ:\n${historyLoginList
|
|
||||||
.map((u, index) => `${index + 1}. ${u.uin} ${u.nickName}`)
|
|
||||||
.join('\n')
|
|
||||||
}`);
|
|
||||||
}
|
|
||||||
loginService.getQRCodePicture();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function initializeSession(
|
|
||||||
session: NodeIQQNTWrapperSession,
|
|
||||||
sessionConfig: any,
|
|
||||||
logger: LogWrapper
|
|
||||||
) {
|
|
||||||
return new Promise<void>((resolve, reject) => {
|
|
||||||
const sessionListener = new NodeIKernelSessionListener();
|
|
||||||
sessionListener.onSessionInitComplete = (r: unknown) => {
|
|
||||||
if (r === 0) {
|
|
||||||
resolve();
|
|
||||||
} else {
|
|
||||||
reject(new Error('登录异常' + r?.toString()));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
session.init(
|
|
||||||
sessionConfig,
|
|
||||||
new NodeIDependsAdapter(),
|
|
||||||
new NodeIDispatcherAdapter(),
|
|
||||||
sessionListener,
|
|
||||||
);
|
|
||||||
try {
|
|
||||||
session.startNT(0);
|
|
||||||
} catch (_) {
|
|
||||||
try {
|
|
||||||
session.startNT();
|
|
||||||
} catch (e: unknown) {
|
|
||||||
reject(new Error('init failed ' + (e as Error).message));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function NCoreInitShell() {
|
|
||||||
console.log('NapCat Shell App Loading...');
|
|
||||||
const pathWrapper = new NapCatPathWrapper();
|
|
||||||
const logger = new LogWrapper(pathWrapper.logsPath);
|
|
||||||
handleUncaughtExceptions(logger);
|
|
||||||
|
|
||||||
const basicInfoWrapper = new QQBasicInfoWrapper({ logger });
|
|
||||||
const wrapper = loadQQWrapper(basicInfoWrapper.getFullQQVesion());
|
|
||||||
|
|
||||||
const o3Service = wrapper.NodeIO3MiscService.get();
|
|
||||||
o3Service.addO3MiscListener(new NodeIO3MiscListener());
|
|
||||||
|
|
||||||
logger.log(`[NapCat] [Core] NapCat.Core Version: ` + napCatVersion);
|
|
||||||
InitWebUi(logger, pathWrapper).then().catch(e => logger.logError(e));
|
|
||||||
|
|
||||||
const engine = wrapper.NodeIQQNTWrapperEngine.get();
|
|
||||||
const loginService = wrapper.NodeIKernelLoginService.get();
|
|
||||||
const session = wrapper.NodeIQQNTWrapperSession.create();
|
|
||||||
|
|
||||||
const [dataPath, dataPathGlobal] = getDataPaths(wrapper);
|
|
||||||
const systemPlatform = getPlatformType();
|
|
||||||
|
|
||||||
if (!basicInfoWrapper.QQVersionAppid || !basicInfoWrapper.QQVersionQua) throw new Error('QQVersionAppid or QQVersionQua is not defined');
|
|
||||||
|
|
||||||
await initializeEngine(engine, basicInfoWrapper, dataPathGlobal, systemPlatform, systemVersion);
|
|
||||||
await initializeLoginService(loginService, basicInfoWrapper, dataPathGlobal, systemVersion, hostname);
|
|
||||||
|
|
||||||
const quickLoginUin = cmdOptions.qq;
|
|
||||||
const historyLoginList = (await loginService.getLoginList()).LocalLoginInfoList;
|
|
||||||
|
|
||||||
const dataTimestape = new Date().getTime().toString();
|
|
||||||
o3Service.reportAmgomWeather('login', 'a1', [dataTimestape, '0', '0']);
|
|
||||||
|
|
||||||
const selfInfo = await handleLogin(loginService, logger, pathWrapper, quickLoginUin, historyLoginList);
|
|
||||||
|
|
||||||
const amgomDataPiece = 'eb1fd6ac257461580dc7438eb099f23aae04ca679f4d88f53072dc56e3bb1129';
|
|
||||||
o3Service.setAmgomDataPiece(basicInfoWrapper.QQVersionAppid, new Uint8Array(Buffer.from(amgomDataPiece, 'hex')));
|
|
||||||
|
|
||||||
let guid = loginService.getMachineGuid();
|
|
||||||
guid = guid.slice(0, 8) + '-' + guid.slice(8, 12) + '-' + guid.slice(12, 16) + '-' + guid.slice(16, 20) + '-' + guid.slice(20);
|
|
||||||
o3Service.reportAmgomWeather('login', 'a6', [dataTimestape, '184', '329']);
|
|
||||||
|
|
||||||
const sessionConfig = await genSessionConfig(
|
|
||||||
guid,
|
|
||||||
basicInfoWrapper.QQVersionAppid,
|
|
||||||
basicInfoWrapper.getFullQQVesion(),
|
|
||||||
selfInfo.uin,
|
|
||||||
selfInfo.uid,
|
|
||||||
dataPath,
|
|
||||||
);
|
|
||||||
|
|
||||||
await initializeSession(session, sessionConfig, logger);
|
|
||||||
|
|
||||||
const accountDataPath = path.resolve(dataPath, './NapCat/data');
|
|
||||||
fs.mkdirSync(dataPath, { recursive: true });
|
|
||||||
logger.logDebug('本账号数据/缓存目录:', accountDataPath);
|
|
||||||
|
|
||||||
await new NapCatShell(
|
|
||||||
wrapper,
|
|
||||||
session,
|
|
||||||
logger,
|
|
||||||
loginService,
|
|
||||||
selfInfo,
|
|
||||||
basicInfoWrapper,
|
|
||||||
pathWrapper,
|
|
||||||
).InitNapCat();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export class NapCatShell {
|
|
||||||
readonly core: NapCatCore;
|
|
||||||
readonly context: InstanceContext;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
wrapper: WrapperNodeApi,
|
|
||||||
session: NodeIQQNTWrapperSession,
|
|
||||||
logger: LogWrapper,
|
|
||||||
loginService: NodeIKernelLoginService,
|
|
||||||
selfInfo: SelfInfo,
|
|
||||||
basicInfoWrapper: QQBasicInfoWrapper,
|
|
||||||
pathWrapper: NapCatPathWrapper,
|
|
||||||
) {
|
|
||||||
this.context = {
|
|
||||||
workingEnv: NapCatCoreWorkingEnv.Shell,
|
|
||||||
wrapper,
|
|
||||||
session,
|
|
||||||
logger,
|
|
||||||
loginService,
|
|
||||||
basicInfoWrapper,
|
|
||||||
pathWrapper,
|
|
||||||
};
|
|
||||||
this.core = new NapCatCore(this.context, selfInfo);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
async InitNapCat() {
|
|
||||||
await this.core.initCore();
|
|
||||||
new NapCatOneBot11Adapter(this.core, this.context, this.context.pathWrapper).InitOneBot()
|
|
||||||
.catch(e => this.context.logger.logError('初始化OneBot失败', e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NCoreInitShell();
|
NCoreInitShell();
|
||||||
|
7
src/universal/napcat.ts
Normal file
7
src/universal/napcat.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { NCoreInitShell } from "@/shell/base";
|
||||||
|
|
||||||
|
export * from "@/framework/napcat";
|
||||||
|
export * from "@/shell/base";
|
||||||
|
if ((global as any).LiteLoader == undefined) {
|
||||||
|
NCoreInitShell();
|
||||||
|
}
|
@@ -6,9 +6,7 @@ import { builtinModules } from 'module';
|
|||||||
//依赖排除
|
//依赖排除
|
||||||
const external = ['silk-wasm', 'ws', 'express', 'qrcode-terminal', 'fluent-ffmpeg', 'piscina'];
|
const external = ['silk-wasm', 'ws', 'express', 'qrcode-terminal', 'fluent-ffmpeg', 'piscina'];
|
||||||
const nodeModules = [...builtinModules, builtinModules.map((m) => `node:${m}`)].flat();
|
const nodeModules = [...builtinModules, builtinModules.map((m) => `node:${m}`)].flat();
|
||||||
function genCpModule(module: string) {
|
|
||||||
return { src: `./node_modules/${module}`, dest: `dist/node_modules/${module}`, flatten: false };
|
|
||||||
}
|
|
||||||
let startScripts: string[] | undefined = undefined;
|
let startScripts: string[] | undefined = undefined;
|
||||||
if (process.env.NAPCAT_BUILDSYS == 'linux') {
|
if (process.env.NAPCAT_BUILDSYS == 'linux') {
|
||||||
startScripts = [];
|
startScripts = [];
|
||||||
@@ -17,6 +15,29 @@ if (process.env.NAPCAT_BUILDSYS == 'linux') {
|
|||||||
} else {
|
} else {
|
||||||
startScripts = ['./script/KillQQ.bat'];
|
startScripts = ['./script/KillQQ.bat'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const UniversalBaseConfigPlugin: PluginOption[] = [
|
||||||
|
cp({
|
||||||
|
targets: [
|
||||||
|
{ src: './manifest.json', dest: 'dist' },
|
||||||
|
{ src: './src/core/external/napcat.json', dest: 'dist/config/' },
|
||||||
|
{ src: './src/native/packet', dest: 'dist/moehoo', flatten: false },
|
||||||
|
{ src: './napcat.webui/dist/', dest: 'dist/static/', flatten: false },
|
||||||
|
{ src: './src/framework/liteloader.cjs', dest: 'dist' },
|
||||||
|
{ src: './src/framework/napcat.cjs', dest: 'dist' },
|
||||||
|
{ src: './src/framework/preload.cjs', dest: 'dist' },
|
||||||
|
{ src: './src/framework/renderer.js', dest: 'dist' },
|
||||||
|
{ src: './package.json', dest: 'dist' },
|
||||||
|
{ src: './logo.png', dest: 'dist' },
|
||||||
|
{ src: './launcher/', dest: 'dist', flatten: true },
|
||||||
|
...startScripts.map((startScript) => {
|
||||||
|
return { src: startScript, dest: 'dist' };
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
nodeResolve(),
|
||||||
|
];
|
||||||
|
|
||||||
const FrameworkBaseConfigPlugin: PluginOption[] = [
|
const FrameworkBaseConfigPlugin: PluginOption[] = [
|
||||||
cp({
|
cp({
|
||||||
targets: [
|
targets: [
|
||||||
@@ -34,6 +55,8 @@ const FrameworkBaseConfigPlugin: PluginOption[] = [
|
|||||||
}),
|
}),
|
||||||
nodeResolve(),
|
nodeResolve(),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
const ShellBaseConfigPlugin: PluginOption[] = [
|
const ShellBaseConfigPlugin: PluginOption[] = [
|
||||||
cp({
|
cp({
|
||||||
targets: [
|
targets: [
|
||||||
@@ -49,6 +72,35 @@ const ShellBaseConfigPlugin: PluginOption[] = [
|
|||||||
}),
|
}),
|
||||||
nodeResolve(),
|
nodeResolve(),
|
||||||
];
|
];
|
||||||
|
const UniversalBaseConfig = () =>
|
||||||
|
defineConfig({
|
||||||
|
resolve: {
|
||||||
|
conditions: ['node', 'default'],
|
||||||
|
alias: {
|
||||||
|
'@/core': resolve(__dirname, './src/core'),
|
||||||
|
'@': resolve(__dirname, './src'),
|
||||||
|
'./lib-cov/fluent-ffmpeg': './lib/fluent-ffmpeg',
|
||||||
|
'@webapi': resolve(__dirname, './src/webui/src'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
sourcemap: false,
|
||||||
|
target: 'esnext',
|
||||||
|
minify: false,
|
||||||
|
lib: {
|
||||||
|
entry: {
|
||||||
|
napcat: 'src/universal/napcat.ts',
|
||||||
|
'audio-worker': 'src/common/audio-worker.ts',
|
||||||
|
},
|
||||||
|
formats: ['es'],
|
||||||
|
fileName: (_, entryName) => `${entryName}.mjs`,
|
||||||
|
},
|
||||||
|
rollupOptions: {
|
||||||
|
external: [...nodeModules, ...external],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
const ShellBaseConfig = () =>
|
const ShellBaseConfig = () =>
|
||||||
defineConfig({
|
defineConfig({
|
||||||
@@ -114,6 +166,11 @@ export default defineConfig(({ mode }): UserConfig => {
|
|||||||
...ShellBaseConfig(),
|
...ShellBaseConfig(),
|
||||||
plugins: [...ShellBaseConfigPlugin],
|
plugins: [...ShellBaseConfigPlugin],
|
||||||
};
|
};
|
||||||
|
} else if (mode == 'universal') {
|
||||||
|
return {
|
||||||
|
...UniversalBaseConfig(),
|
||||||
|
plugins: [...UniversalBaseConfigPlugin],
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
...FrameworkBaseConfig(),
|
...FrameworkBaseConfig(),
|
||||||
|
Reference in New Issue
Block a user