import type { SelfInfo } from 'snowcat-core/index'; import { NodeIKernelLoginListener, NodeIKernelSessionListener } from 'snowcat-core/listeners'; import { NodeIDependsAdapter, NodeIDispatcherAdapter, NodeIGlobalAdapter } from 'snowcat-core/adapters'; import { SnowCatPathWrapper } from 'snowcat-common/src/path'; import { genSessionConfig, InstanceContext, loadQQWrapper, SnowCatCore, SnowCatCoreWorkingEnv, NodeIQQNTStartupSessionWrapper, NodeIQQNTWrapperEngine, NodeIQQNTWrapperSession, PlatformType, WrapperNodeApi, WrapperSessionInitConfig, } from 'snowcat-core'; import { hostname, systemVersion } from 'snowcat-common/src/system'; import path from 'path'; import fs from 'fs'; import os from 'os'; import { createHash } from 'node:crypto'; import { LoginListItem, NodeIKernelLoginService } from 'snowcat-core/services'; import qrcode from 'snowcat-qrcode/lib/main'; import { SnowCatAdapterManager } from 'snowcat-adapter'; import { InitWebUi } from 'snowcat-webui-backend/index'; import { WebUiDataRuntime } from 'snowcat-webui-backend/src/helper/Data'; import { snowCatVersion } from 'snowcat-common/src/version'; import { NodeIO3MiscListener } from 'snowcat-core/listeners/NodeIO3MiscListener'; import { sleep } from 'snowcat-common/src/helper'; import { FFmpegService } from '@/snowcat-core/helper/ffmpeg/ffmpeg'; import { NativePacketHandler } from 'snowcat-core/packet/handler/client'; import { Napi2NativeLoader } from 'snowcat-core/packet/handler/napi2nativeLoader'; import { NapProtoMsg } from 'snowcat-protobuf'; import { OidbSvcTrpcTcpBase, OidbSvcTrpcTcp0XCDE_2RespBody } from '@/snowcat-core/packet/transformer/proto'; import { loadSnowCatConfig } from '@/snowcat-core/helper/config'; import { logSubscription, LogWrapper } from '@/snowcat-core/helper/log'; import { proxiedListenerOf } from '@/snowcat-core/helper/proxy-handler'; import { QQBasicInfoWrapper } from '@/snowcat-core/helper/qq-basic-info'; import { statusHelperSubscription } from '@/snowcat-core/helper/status'; import { applyPendingUpdates } from '@/snowcat-webui-backend/src/api/UpdateNapCat'; import { connectToNamedPipe } from './pipe'; // NapCat Shell App ES 入口文件 async function handleUncaughtExceptions (logger: LogWrapper) { process.on('uncaughtException', (err) => { logger.logError('[SnowCat] [Error] Unhandled Exception:', err.message); }); process.on('unhandledRejection', (reason) => { logger.logError('[SnowCat] [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, externalVersion: false, }); } async function handleLogin ( loginService: NodeIKernelLoginService, logger: LogWrapper, pathWrapper: SnowCatPathWrapper, quickLoginUin: string | undefined, historyLoginList: LoginListItem[] ): Promise { const context = { isLogined: false, loginFlowInitialized: false }; let inner_resolve: (value: SelfInfo) => void; const selfInfo: Promise = new Promise((resolve) => { inner_resolve = resolve; }); // 连接服务 const loginListener = new NodeIKernelLoginListener(); loginListener.onUserLoggedIn = (userid: string) => { const tips = `当前账号(${userid})已登录,无法重复登录`; logger.logError(tips); WebUiDataRuntime.setQQLoginError(tips); }; loginListener.onQRCodeLoginSucceed = async (loginResult) => { context.isLogined = true; WebUiDataRuntime.setQQLoginStatus(true); WebUiDataRuntime.setQQLoginError(''); const oneBotContext = WebUiDataRuntime.getOneBotContext(); if (oneBotContext?.core?.selfInfo) { oneBotContext.core.selfInfo.online = true; WebUiDataRuntime.setQQLoginPhase('ready'); } else { WebUiDataRuntime.setQQLoginPhase('initializing'); } inner_resolve({ uid: loginResult.uid, uin: loginResult.uin, nick: '', online: true, }); }; loginListener.onLoginDisConnected = () => { context.isLogined = false; WebUiDataRuntime.setQQLoginStatus(false); WebUiDataRuntime.setQQLoginPhase('offline'); logger.logWarn('[Core] [Login] 登录服务已断开,将允许重新获取二维码'); }; loginListener.onLogoutSucceed = () => { context.isLogined = false; WebUiDataRuntime.setQQLoginStatus(false); WebUiDataRuntime.setQQLoginPhase('offline'); }; loginListener.onLoginConnected = () => { waitForNetworkConnection(loginService, logger).then(async () => { if (!context.loginFlowInitialized) { context.loginFlowInitialized = true; await handleLoginInner(context, logger, loginService, quickLoginUin, historyLoginList); return; } if (!context.isLogined) { WebUiDataRuntime.setQQLoginPhase('generating_qrcode'); const requested = loginService.getQRCodePicture(); logger.logWarn('[Core] [Login] 登录服务重连完成,正在获取二维码', { requested, msfStatus: loginService.getMsfStatus() }); } }).catch(e => logger.logError(e)); }; loginListener.onQRCodeGetPicture = ({ pngBase64QrcodeData, qrcodeUrl }) => { WebUiDataRuntime.setQQLoginQrcodeURL(qrcodeUrl); WebUiDataRuntime.setQQLoginError(''); WebUiDataRuntime.setQQLoginPhase('waiting_qrcode'); 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.onQRCodeSessionUserScaned = () => { WebUiDataRuntime.setQQLoginPhase('qrcode_scanned'); logger.log('[Core] [Login] 二维码已被扫描,等待确认...'); }; loginListener.onQRCodeSessionFailed = (errType: number, errCode: number) => { if (!context.isLogined) { logger.logError('[Core] [Login] Login Error,ErrType: ', errType, ' ErrCode:', errCode); if (errType === 1 && errCode === 3) { // 仅二维码过期时自动请求下一张二维码。账号被踢下线等其他错误 // 需要由用户触发刷新,避免每两秒重复创建原生登录会话。 WebUiDataRuntime.setQQLoginError('二维码已过期,请刷新'); loginService.getQRCodePicture(); } } }; loginListener.onLoginFailed = (...args) => { const errInfo = JSON.stringify(args); logger.logError('[Core] [Login] Login Error , ErrInfo: ', errInfo); WebUiDataRuntime.setQQLoginError(`登录失败: ${errInfo}`); }; loginService.addKernelLoginListener(proxiedListenerOf(loginListener, logger)); loginService.connect(); return await selfInfo; } async function handleLoginInner (context: { isLogined: boolean; loginFlowInitialized: boolean; }, logger: LogWrapper, loginService: NodeIKernelLoginService, quickLoginUin: string | undefined, historyLoginList: LoginListItem[]) { const resolveQuickPasswordMd5 = (): string | undefined => { const quickPasswordMd5 = process.env['NAPCAT_QUICK_PASSWORD_MD5']?.trim(); if (quickPasswordMd5) { if (/^[a-fA-F0-9]{32}$/.test(quickPasswordMd5)) { return quickPasswordMd5.toLowerCase(); } logger.logError('NAPCAT_QUICK_PASSWORD_MD5 格式无效(需为 32 位 MD5)'); } const quickPassword = process.env['NAPCAT_QUICK_PASSWORD']; if (typeof quickPassword === 'string' && quickPassword.length > 0) { logger.log('检测到 NAPCAT_QUICK_PASSWORD,已在内存中计算 MD5 用于回退登录'); return createHash('md5').update(quickPassword, 'utf8').digest('hex'); } return undefined; }; // 注册刷新二维码回调 WebUiDataRuntime.setRefreshQRCodeCallback(async () => { context.isLogined = false; const msfStatus = loginService.getMsfStatus(); if (msfStatus === 2) { // KickedOffLine 会在后台负责重启 Worker。这里不能重启:该回调由 // RefreshQRcode HTTP 请求触发,重启会中断正在等待响应的请求。 WebUiDataRuntime.setQQLoginPhase('reconnecting'); throw new Error('QQ_LOGIN_SERVICE_RESTARTING'); } const requested = loginService.getQRCodePicture(); logger.logWarn('[Core] [Login] 请求刷新二维码', { requested, msfStatus }); if (!requested) { throw new Error('QQ 未接受二维码刷新请求'); } }); WebUiDataRuntime.setQuickLoginCall(async (uin: string) => { return await new Promise((resolve) => { if (uin) { logger.log('正在快速登录 ', uin); loginService.quickLoginWithUin(uin).then(res => { const quickLoginSuccess = res.result === '0' && !res.loginErrorInfo?.errMsg; if (!quickLoginSuccess) { const errMsg = res.loginErrorInfo?.errMsg || `快速登录失败,错误码: ${res.result}`; WebUiDataRuntime.setQQLoginError(errMsg); loginService.getQRCodePicture(); resolve({ result: false, message: errMsg }); } else { WebUiDataRuntime.setQQLoginStatus(true); WebUiDataRuntime.setQQLoginError(''); resolve({ result: true, message: '' }); } }).catch((e) => { logger.logError(e); WebUiDataRuntime.setQQLoginError('快速登录发生错误'); loginService.getQRCodePicture(); resolve({ result: false, message: '快速登录发生错误' }); }); } else { resolve({ result: false, message: '快速登录失败' }); } }); }); // 注册密码登录回调 WebUiDataRuntime.setPasswordLoginCall(async (uin: string, passwordMd5: string) => { return await new Promise((resolve) => { if (uin && passwordMd5) { logger.log('正在密码登录 ', uin); loginService.passwordLogin({ uin, passwordMd5, step: 0, newDeviceLoginSig: new Uint8Array(), proofWaterSig: new Uint8Array(), proofWaterRand: new Uint8Array(), proofWaterSid: new Uint8Array(), unusualDeviceCheckSig: new Uint8Array(), }).then(res => { if (res.result === '140022008') { const proofWaterUrl = res.loginErrorInfo?.proofWaterUrl || ''; logger.log('需要验证码, proofWaterUrl: ', proofWaterUrl); resolve({ result: false, message: '需要验证码', needCaptcha: true, proofWaterUrl, }); } else if (res.result === '140022010') { const jumpUrl = res.loginErrorInfo?.jumpUrl || ''; const newDevicePullQrCodeSig = res.loginErrorInfo?.newDevicePullQrCodeSig || ''; logger.log('新设备需要扫码验证, jumpUrl: ', jumpUrl); resolve({ result: false, message: '新设备需要扫码验证', needNewDevice: true, jumpUrl, newDevicePullQrCodeSig, }); } else if (res.result === '140022011') { const jumpUrl = res.loginErrorInfo?.jumpUrl || ''; const newDevicePullQrCodeSig = res.loginErrorInfo?.newDevicePullQrCodeSig || ''; logger.log('异常设备需要验证, jumpUrl: ', jumpUrl); resolve({ result: false, message: '异常设备需要验证', needNewDevice: true, jumpUrl, newDevicePullQrCodeSig, }); } else if (res.result !== '0') { const errMsg = res.loginErrorInfo?.errMsg || '密码登录失败'; WebUiDataRuntime.setQQLoginError(errMsg); loginService.getQRCodePicture(); resolve({ result: false, message: errMsg }); } else { WebUiDataRuntime.setQQLoginStatus(true); WebUiDataRuntime.setQQLoginError(''); resolve({ result: true, message: '' }); } }).catch((e) => { logger.logError(e); WebUiDataRuntime.setQQLoginError('密码登录发生错误'); loginService.getQRCodePicture(); resolve({ result: false, message: '密码登录发生错误' }); }); } else { resolve({ result: false, message: '密码登录失败:参数不完整' }); } }); }); const tryPasswordFallbackLogin = async (uin: string): Promise<{ success: boolean, attempted: boolean; }> => { const quickPasswordMd5 = resolveQuickPasswordMd5(); if (!quickPasswordMd5) { logger.log(`QQ ${uin} 未配置回退密码环境变量,建议优先使用 ACCOUNT + NAPCAT_QUICK_PASSWORD(NAPCAT_QUICK_PASSWORD_MD5 作为备用),将使用二维码登录方式`); return { success: false, attempted: false }; } logger.log('正在尝试密码回退登录 ', uin); const fallbackResult = await WebUiDataRuntime.requestPasswordLogin(uin, quickPasswordMd5); if (fallbackResult.result) { logger.log('密码回退登录成功 ', uin); return { success: true, attempted: true }; } if (fallbackResult.needCaptcha) { const captchaTip = fallbackResult.proofWaterUrl ? `密码回退需要验证码,请在 WebUi 中继续完成验证:${fallbackResult.proofWaterUrl}` : '密码回退需要验证码,请在 WebUi 中继续完成验证'; logger.logWarn(captchaTip); WebUiDataRuntime.setQQLoginError('密码回退需要验证码,请在 WebUi 中继续完成验证'); return { success: false, attempted: true }; } if (fallbackResult.needNewDevice) { const newDeviceTip = fallbackResult.jumpUrl ? `密码回退需要新设备验证,请在 WebUi 中继续完成验证:${fallbackResult.jumpUrl}` : '密码回退需要新设备验证,请在 WebUi 中继续完成验证'; logger.logWarn(newDeviceTip); WebUiDataRuntime.setQQLoginError('密码回退需要新设备验证,请在 WebUi 中继续完成验证'); return { success: false, attempted: true }; } logger.logError('密码回退登录失败:', fallbackResult.message); return { success: false, attempted: true }; }; // 注册验证码登录回调(密码登录需要验证码时的第二步) WebUiDataRuntime.setCaptchaLoginCall(async (uin: string, passwordMd5: string, ticket: string, randstr: string, sid: string) => { return await new Promise((resolve) => { if (uin && passwordMd5 && ticket) { logger.log('正在验证码登录 ', uin); loginService.passwordLogin({ uin, passwordMd5, step: 1, newDeviceLoginSig: new Uint8Array(), proofWaterSig: new TextEncoder().encode(ticket), proofWaterRand: new TextEncoder().encode(randstr), proofWaterSid: new TextEncoder().encode(sid), unusualDeviceCheckSig: new Uint8Array(), }).then(res => { console.log('验证码登录结果: ', res); if (res.result === '140022010') { const jumpUrl = res.loginErrorInfo?.jumpUrl || ''; const newDevicePullQrCodeSig = res.loginErrorInfo?.newDevicePullQrCodeSig || ''; logger.log('验证码登录后需要新设备验证, jumpUrl: ', jumpUrl); resolve({ result: false, message: '新设备需要扫码验证', needNewDevice: true, jumpUrl, newDevicePullQrCodeSig, }); } else if (res.result === '140022011') { const jumpUrl = res.loginErrorInfo?.jumpUrl || ''; const newDevicePullQrCodeSig = res.loginErrorInfo?.newDevicePullQrCodeSig || ''; logger.log('验证码登录后需要异常设备验证, jumpUrl: ', jumpUrl); resolve({ result: false, message: '异常设备需要验证', needNewDevice: true, jumpUrl, newDevicePullQrCodeSig, }); } else if (res.result !== '0') { const errMsg = res.loginErrorInfo?.errMsg || '验证码登录失败'; WebUiDataRuntime.setQQLoginError(errMsg); loginService.getQRCodePicture(); resolve({ result: false, message: errMsg }); } else { WebUiDataRuntime.setQQLoginStatus(true); WebUiDataRuntime.setQQLoginError(''); resolve({ result: true, message: '' }); } }).catch((e) => { logger.logError(e); WebUiDataRuntime.setQQLoginError('验证码登录发生错误'); loginService.getQRCodePicture(); resolve({ result: false, message: '验证码登录发生错误' }); }); } else { resolve({ result: false, message: '验证码登录失败:参数不完整' }); } }); }); // 注册新设备登录回调(密码登录需要新设备验证时的第二步) WebUiDataRuntime.setNewDeviceLoginCall(async (uin: string, passwordMd5: string, newDevicePullQrCodeSig: string) => { return await new Promise((resolve) => { if (uin && passwordMd5 && newDevicePullQrCodeSig) { logger.log('正在新设备验证登录 ', uin); loginService.passwordLogin({ uin, passwordMd5, step: 2, newDeviceLoginSig: new TextEncoder().encode(newDevicePullQrCodeSig), proofWaterSig: new Uint8Array(), proofWaterRand: new Uint8Array(), proofWaterSid: new Uint8Array(), unusualDeviceCheckSig: new Uint8Array(), }).then(res => { if (res.result === '140022011') { const jumpUrl = res.loginErrorInfo?.jumpUrl || ''; const newDevicePullQrCodeSig = res.loginErrorInfo?.newDevicePullQrCodeSig || ''; logger.log('新设备验证后需要异常设备验证, jumpUrl: ', jumpUrl); resolve({ result: false, message: '异常设备需要验证', needNewDevice: true, jumpUrl, newDevicePullQrCodeSig, }); } else if (res.result !== '0') { const errMsg = res.loginErrorInfo?.errMsg || '新设备验证登录失败'; WebUiDataRuntime.setQQLoginError(errMsg); loginService.getQRCodePicture(); resolve({ result: false, message: errMsg }); } else { WebUiDataRuntime.setQQLoginStatus(true); WebUiDataRuntime.setQQLoginError(''); resolve({ result: true, message: '' }); } }).catch((e) => { logger.logError(e); WebUiDataRuntime.setQQLoginError('新设备验证登录发生错误'); loginService.getQRCodePicture(); 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(async result => { const quickLoginSuccess = result.result === '0' && !result.loginErrorInfo?.errMsg; if (!quickLoginSuccess) { const errMsg = result.loginErrorInfo?.errMsg || `快速登录失败,错误码: ${result.result}`; logger.logError('快速登录错误:', errMsg); WebUiDataRuntime.setQQLoginError(errMsg); const { success, attempted } = await tryPasswordFallbackLogin(quickLoginUin); if (!success && !attempted && !context.isLogined) loginService.getQRCodePicture(); } }) .catch(async (error) => { logger.logError('快速登录异常:', error); WebUiDataRuntime.setQQLoginError('快速登录发生错误'); const { success, attempted } = await tryPasswordFallbackLogin(quickLoginUin); if (!success && !attempted && !context.isLogined) loginService.getQRCodePicture(); }); } else { logger.logError('快速登录失败,未找到该 QQ 历史登录记录,将尝试密码回退登录'); const { success, attempted } = await tryPasswordFallbackLogin(quickLoginUin); if (!success && !attempted && !context.isLogined) loginService.getQRCodePicture(); } } else { logger.log('没有 -q 指令指定快速登录,将使用二维码登录方式'); if (historyLoginList.length > 0) { logger.log(`可用于快速登录 of QQ:\n${historyLoginList .map((u, index) => `${index + 1}. ${u.uin} ${u.nickName}`) .join('\n') }`); } let hasAttemptedFallback = false; try { // 检查是否配置了自动登录账号,并尝试 WebUi 快速/密码回退登录。 // 这可以避免在已经有自动登录配置的情况下过早输出二维码。 const quickLoginList = WebUiDataRuntime.getQQQuickLoginList() || []; const hasAutoLogin = process.env['NAPCAT_QUICK_ACCOUNT'] || (quickLoginList.length > 0); // 假如有历史登录列表 if (hasAutoLogin || process.env['NAPCAT_QUICK_PASSWORD'] || process.env['NAPCAT_QUICK_PASSWORD_MD5']) { hasAttemptedFallback = true; } await WebUiDataRuntime.runWebUiConfigQuickFunction(); } catch (error) { logger.logError('WebUi 快速登录失败 执行失败', error); } if (!hasAttemptedFallback && !context.isLogined) { loginService.getQRCodePicture(); } } 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('SnowCat Shell App Loading...'); const pathWrapper = new SnowCatPathWrapper(); const logger = new LogWrapper(pathWrapper.logsPath); if (process.env['NAPCAT_WORKER_PROCESS'] !== '1' && process.env['NAPCAT_DISABLE_MULTI_PROCESS'] !== '1' && process.env['NAPCAT_DISABLE_MULTIPROCESSING'] !== '1') { logger.setFileLogEnabled(false); } handleUncaughtExceptions(logger); await applyPendingUpdates(pathWrapper, logger); // 提前初始化 Native 模块(在登录前加载) const basicInfoWrapper = new QQBasicInfoWrapper({ logger }); const nativePacketHandler = new NativePacketHandler({ logger }); const napi2nativeLoader = new Napi2NativeLoader({ logger }); // 初始化 FFmpeg 服务 await FFmpegService.init(pathWrapper.binaryPath, logger); if (!(process.env['NAPCAT_DISABLE_PIPE'] === '1' || process.env['NAPCAT_WORKER_PROCESS'] === '1')) { await connectToNamedPipe(logger).catch(e => logger.logError('命名管道连接失败', e)); } const wrapper = loadQQWrapper(basicInfoWrapper.QQMainPath, basicInfoWrapper.getFullQQVersion()); // wrapper.node 加载后再初始化 hook,按 schema 读取配置 const snowcatConfig = loadSnowCatConfig(pathWrapper.configPath); await nativePacketHandler.init(basicInfoWrapper.getFullQQVersion(), snowcatConfig.o3HookMode === 1); // 登录前监听 OidbSvcTrpcTcp.0xcde_2 数据包,获取数据库 passphrase let dbPassphrase: string | undefined; nativePacketHandler.onCmd('OidbSvcTrpcTcp.0xcde_2', ({ type, hex_data }) => { if (type !== 1) return; // 仅处理接收方向 try { const raw = Buffer.from(hex_data, 'hex'); const base = new NapProtoMsg(OidbSvcTrpcTcpBase).decode(raw); if (base.body && base.body.length > 0) { const body = new NapProtoMsg(OidbSvcTrpcTcp0XCDE_2RespBody).decode(base.body); if (body.inner?.value) { dbPassphrase = body.inner.value; logger.log('[SnowCat] 已启用数据库辅助支持能力'); } } } catch (e) { logger.logError('[SnowCat] [0xCDE_2] 解析失败:', e); } }); if (process.env['NAPCAT_ENABLE_VERBOSE_LOG'] === '1') { napi2nativeLoader.nativeExports.setVerbose?.(true); } // wrapper.node 加载后立刻启用 Bypass(可通过环境变量禁用) if (process.env['NAPCAT_DISABLE_BYPASS'] !== '1') { const bypassOptions = snowcatConfig.bypass ?? {}; napi2nativeLoader.nativeExports.enableAllBypasses?.(bypassOptions); } else { logger.log('[SnowCat] Napi2NativeLoader: Bypass已通过环境变量禁用'); } const o3Service = wrapper.NodeIO3MiscService.get(); o3Service.addO3MiscListener(new NodeIO3MiscListener()); logger.log('[SnowCat] [Core] SnowCat.Core Version: ' + snowCatVersion); WebUiDataRuntime.setWorkingEnv(SnowCatCoreWorkingEnv.Shell); InitWebUi(logger, pathWrapper, logSubscription, statusHelperSubscription).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); WebUiDataRuntime.setQQDataPath(dataPath); 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; 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); // 登录成功后通知 Master 进程(用于切换崩溃重试策略) if (typeof process.send === 'function') { process.send({ type: 'login-success' }); logger.log('[SnowCat] 已通知主进程登录成功'); } 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); const shell = new SnowCatShell( wrapper, session, logger, selfInfo, basicInfoWrapper, pathWrapper, nativePacketHandler, napi2nativeLoader ); // 将捕获的 passphrase 设置到 core 上,供 DatabaseApi 使用 if (dbPassphrase) { shell.core.dbPassphrase = dbPassphrase; } await shell.InitNapCat(); } export class SnowCatShell { readonly core: SnowCatCore; readonly context: InstanceContext; constructor ( wrapper: WrapperNodeApi, session: NodeIQQNTWrapperSession, logger: LogWrapper, selfInfo: SelfInfo, basicInfoWrapper: QQBasicInfoWrapper, pathWrapper: SnowCatPathWrapper, packetHandler: NativePacketHandler, napi2nativeLoader: Napi2NativeLoader ) { this.context = { packetHandler, napi2nativeLoader, workingEnv: SnowCatCoreWorkingEnv.Shell, wrapper, session, logger, basicInfoWrapper, pathWrapper, }; this.core = new SnowCatCore(this.context, selfInfo); } async InitNapCat () { await this.core.initCore(); // // 测试 DatabaseApi // if (this.core.dbPassphrase) { // const dbApi = this.core.apis.DatabaseApi; // const sqliteOk = await dbApi.isSqliteAvailable(); // if (sqliteOk) { // try { // const result = dbApi.readDatabase('nt_msg.db'); // if (result) { // this.context.logger.log('[SnowCat] [Database] nt_msg.db:\n' + dbApi.formatResults([result])); // } // } catch (e) { // this.context.logger.logError('[SnowCat] [Database] 读取失败:', e); // } // } // } // 监听下线通知并同步到 WebUI let restartRequested = false; this.core.event.on('KickedOffLine', (tips: string) => { WebUiDataRuntime.setQQLoginStatus(false); // 被踢后的二维码已失效,绝不能继续交给页面展示或扫码。 WebUiDataRuntime.setQQLoginQrcodeURL(''); WebUiDataRuntime.setQQLoginError(tips); WebUiDataRuntime.setQQLoginPhase('reconnecting'); if (restartRequested) return; restartRequested = true; // 不从 RefreshQRcode 请求中重启 Worker;否则请求会在返回前被中断, // 浏览器只能得到 ERR_EMPTY_RESPONSE。KickedOffLine 是独立事件, // 在此异步调度可让前端先观察到“二维码已清空、正在恢复”的状态。 setTimeout(() => { WebUiDataRuntime.requestRestartProcess().then((restartResult) => { this.context.logger.logWarn('[Core] [Login] 账号被踢下线,正在重启 Worker 以重新创建 QQ 登录服务', restartResult); if (!restartResult.result) { restartRequested = false; WebUiDataRuntime.setQQLoginPhase('offline'); WebUiDataRuntime.setQQLoginError(`${tips}\n登录服务重启失败:${restartResult.message}`); } }).catch((error) => { restartRequested = false; WebUiDataRuntime.setQQLoginPhase('offline'); WebUiDataRuntime.setQQLoginError(`${tips}\n登录服务重启失败:${(error as Error).message}`); }); // 常规状态轮询为 3 秒;留出一个轮询周期,确保浏览器先清除旧二维码。 }, 3_500); }); // 使用 SnowCatAdapterManager 统一管理协议适配器 const adapterManager = new SnowCatAdapterManager(this.core, this.context, this.context.pathWrapper); await adapterManager.initAdapters() .catch(e => this.context.logger.logError('初始化协议适配器失败', e)); // 注册 OneBot 适配器到 WebUiDataRuntime,供调试功能使用 const oneBotAdapter = adapterManager.getOneBotAdapter(); if (oneBotAdapter) { WebUiDataRuntime.setOneBotContext(oneBotAdapter); if (WebUiDataRuntime.getQQLoginStatus()) { WebUiDataRuntime.setQQLoginPhase('ready'); } } } }