import type { SelfInfo } from 'napcat-core/types'; import { LogWrapper } from 'napcat-common/src/log'; import { NodeIKernelLoginListener, NodeIKernelSessionListener } from 'napcat-core/listeners'; import { NodeIDependsAdapter, NodeIDispatcherAdapter, NodeIGlobalAdapter } from 'napcat-core/adapters'; import { NapCatPathWrapper } from 'napcat-common/src/path'; import { genSessionConfig, InstanceContext, loadQQWrapper, NapCatCore, NapCatCoreWorkingEnv, NodeIQQNTStartupSessionWrapper, NodeIQQNTWrapperEngine, NodeIQQNTWrapperSession, PlatformType, WrapperNodeApi, WrapperSessionInitConfig, } from 'napcat-core'; import { QQBasicInfoWrapper } from 'napcat-common/src/qq-basic-info'; import { hostname, systemVersion } from 'napcat-common/src/system'; import { proxiedListenerOf } from 'napcat-common/src/proxy-handler'; import path from 'path'; import fs from 'fs'; import os from 'os'; import { LoginListItem, NodeIKernelLoginService } from 'napcat-core/services'; import qrcode from 'napcat-qrcode/lib/main'; import { NapCatOneBot11Adapter } from 'napcat-onebot/index'; import { InitWebUi } from 'napcat-webui-backend/index'; import { WebUiDataRuntime } from 'napcat-webui-backend/src/helper/Data'; import { napCatVersion } from 'napcat-common/src/version'; import { NodeIO3MiscListener } from 'napcat-core/listeners/NodeIO3MiscListener'; import { sleep } from 'napcat-common/src/helper'; import { FFmpegService } from 'napcat-common/src/ffmpeg'; import { connectToNamedPipe } from './pipe'; import { NativePacketHandler } from 'napcat-core/packet/handler/client'; // 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) => { 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> = { win32: PlatformType.KWINDOWS, darwin: PlatformType.KMAC, linux: PlatformType.KLINUX, }; return platformMapping[os.platform()] ?? PlatformType.KWINDOWS; } async function initializeEngine ( engine: NodeIQQNTWrapperEngine, basicInfoWrapper: QQBasicInfoWrapper, dataPathGlobal: string, systemPlatform: PlatformType, systemVersion: string ) { engine.initWithDeskTopConfig( { base_path_prefix: '', platform_type: systemPlatform, app_type: 4, app_version: basicInfoWrapper.getFullQQVersion(), 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.getFullQQVersion(), hostName: hostname, }); } async function handleLogin ( loginService: NodeIKernelLoginService, logger: LogWrapper, pathWrapper: NapCatPathWrapper, quickLoginUin: string | undefined, historyLoginList: LoginListItem[] ): Promise { const context = { isLogined: false }; let inner_resolve: (value: SelfInfo) => void; const selfInfo: Promise = new Promise((resolve) => { inner_resolve = resolve; }); // 连接服务 const loginListener = new NodeIKernelLoginListener(); loginListener.onUserLoggedIn = (userid: string) => { logger.logError(`当前账号(${userid})已登录,无法重复登录`); }; loginListener.onQRCodeLoginSucceed = async (loginResult) => { context.isLogined = true; inner_resolve({ uid: loginResult.uid, uin: loginResult.uin, nick: '', online: true, }); }; loginListener.onLoginConnected = () => { waitForNetworkConnection(loginService, logger).then(() => { handleLoginInner(context, logger, loginService, quickLoginUin, historyLoginList).then().catch(e => logger.logError(e)); loginListener.onLoginConnected = () => { }; }); }; 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) => { if (!context.isLogined) { logger.logError('[Core] [Login] Login Error,ErrType: ', errType, ' ErrCode:', errCode); if (errType === 1 && errCode === 3) { // 二维码过期刷新 } loginService.getQRCodePicture(); } }; loginListener.onLoginFailed = (...args) => { logger.logError('[Core] [Login] Login Error , ErrInfo: ', JSON.stringify(args)); }; loginService.addKernelLoginListener(proxiedListenerOf(loginListener, logger)); loginService.connect(); return await selfInfo; } async function handleLoginInner (context: { isLogined: boolean; }, logger: LogWrapper, loginService: NodeIKernelLoginService, quickLoginUin: string | undefined, historyLoginList: LoginListItem[]) { 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); loginService.quickLoginWithUin(quickLoginUin) .then(result => { if (result.loginErrorInfo.errMsg) { logger.logError('快速登录错误:', result.loginErrorInfo.errMsg); if (!context.isLogined) loginService.getQRCodePicture(); } }) .catch(); } else { logger.logError('快速登录失败,未找到该 QQ 历史登录记录,将使用二维码登录方式'); if (!context.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(); try { await WebUiDataRuntime.runWebUiConfigQuickFunction(); } catch (error) { logger.logError('WebUi 快速登录失败 执行失败', error); } } loginService.getLoginList().then((res) => { // 遍历 res.LocalLoginInfoList[x].isQuickLogin是否可以 res.LocalLoginInfoList[x].uin 转为string 加入string[] 最后遍历完成调用WebUiDataRuntime.setQQQuickLoginList const list = res.LocalLoginInfoList.filter((item) => item.isQuickLogin); WebUiDataRuntime.setQQQuickLoginList(list.map((item) => item.uin.toString())); WebUiDataRuntime.setQQNewLoginList(list); }); } async function initializeSession ( session: NodeIQQNTWrapperSession, sessionConfig: WrapperSessionInitConfig, startupSession: NodeIQQNTStartupSessionWrapper | null ) { return new Promise((resolve, reject) => { const sessionListener = new NodeIKernelSessionListener(); sessionListener.onOpentelemetryInit = (info) => { if (info.is_init) { resolve(); } else { reject(new Error('opentelemetry init failed')); } }; session.init( sessionConfig, new NodeIDependsAdapter(), new NodeIDispatcherAdapter(), sessionListener ); if (startupSession) { startupSession.start(); } else { try { session.startNT(0); } catch { try { session.startNT(); } catch (e: unknown) { reject(new Error('init failed ' + (e as Error).message)); } } } }); } async function handleProxy (session: NodeIQQNTWrapperSession, logger: LogWrapper) { if (process.env['NAPCAT_PROXY_PORT']) { session.getMSFService().setNetworkProxy({ userName: '', userPwd: '', address: process.env['NAPCAT_PROXY_ADDRESS'] || '127.0.0.1', port: +process.env['NAPCAT_PROXY_PORT'], proxyType: 2, domain: '', isSocket: true, }); logger.logWarn('已设置代理', process.env['NAPCAT_PROXY_ADDRESS'], process.env['NAPCAT_PROXY_PORT']); } else if (process.env['NAPCAT_PROXY_CLOSE']) { session.getMSFService().setNetworkProxy({ userName: '', userPwd: '', address: '', port: 0, proxyType: 0, domain: '', isSocket: false, }); } } async function waitForNetworkConnection (loginService: NodeIKernelLoginService, logger: LogWrapper) { let network_ok = false; let _tryCount = 0; while (!network_ok) { network_ok = loginService.getMsfStatus() !== 3;// win 11 0连接 1未连接 logger.log('等待网络连接...'); await sleep(500); _tryCount++; } logger.log('网络已连接'); return network_ok; } export async function NCoreInitShell () { console.log('NapCat Shell App Loading...'); const pathWrapper = new NapCatPathWrapper(); const logger = new LogWrapper(pathWrapper.logsPath); handleUncaughtExceptions(logger); // 初始化 FFmpeg 服务 await FFmpegService.init(pathWrapper.binaryPath, logger); if (process.env['NAPCAT_DISABLE_PIPE'] !== '1') { await connectToNamedPipe(logger).catch(e => logger.logError('命名管道连接失败', e)); } const basicInfoWrapper = new QQBasicInfoWrapper({ logger }); const wrapper = loadQQWrapper(basicInfoWrapper.getFullQQVersion()); const nativePacketHandler = new NativePacketHandler({ logger }); // 初始化 NativePacketHandler 用于后续使用 // nativePacketHandler.onAll((packet) => { // console.log('[Packet]', packet.uin, packet.cmd, packet.hex_data); // }); await nativePacketHandler.init(basicInfoWrapper.getFullQQVersion()); 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(); let session: NodeIQQNTWrapperSession; let startupSession: NodeIQQNTStartupSessionWrapper | null = null; try { startupSession = wrapper.NodeIQQNTStartupSessionWrapper.create(); session = wrapper.NodeIQQNTWrapperSession.getNTWrapperSession('nt_1'); } catch (e: unknown) { try { session = wrapper.NodeIQQNTWrapperSession.create(); } catch (error) { logger.logError('创建 StartupSession 失败', e); logger.logError('创建 Session 失败', error); throw error; } } 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); handleProxy(session, logger); let quickLoginUin: string | undefined = undefined; try { const args = process.argv; const qIndex = args.findIndex(arg => arg === '-q' || arg === '--qq'); if (qIndex !== -1 && qIndex + 1 < args.length) { quickLoginUin = args[qIndex + 1]; } } catch (error) { logger.logWarn('解析命令行参数失败,无法使用快速登录功能', error); } 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.getFullQQVersion(), selfInfo.uin, selfInfo.uid, dataPath ); await initializeSession(session, sessionConfig, startupSession); const accountDataPath = path.resolve(dataPath, './NapCat/data'); // 判断dataPath是否为根目录 或者 D:/ 之类的盘目录 if (dataPath !== '/' && /^[a-zA-Z]:\\$/.test(dataPath) === false) { try { fs.mkdirSync(accountDataPath, { recursive: true }); } catch (error) { logger.logError('创建accountDataPath失败', error); } } logger.logDebug('本账号数据/缓存目录:', accountDataPath); await new NapCatShell( wrapper, session, logger, loginService, selfInfo, basicInfoWrapper, pathWrapper, nativePacketHandler ).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, packetHandler: NativePacketHandler ) { this.context = { packetHandler, 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)); } }