NapCatQQ source snapshot
This commit is contained in:
commit
f954252a34
1111 files changed
+139867
No files matched your search
@@ -0,0 +1,178 @@
|
||||
const process = require('process');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const { pathToFileURL } = require('url');
|
||||
|
||||
// 保存原始dlopen
|
||||
const dlopenOrig = process.dlopen;
|
||||
|
||||
let wrapperSession;
|
||||
let wrapperNodeApi;
|
||||
let wrapperLoginService;
|
||||
let getWebUiUrlFunc;
|
||||
let wrapperHooked = false;
|
||||
let sessionReady = false;
|
||||
let warnedMissingSession = false;
|
||||
const initCallbacks = new Set();
|
||||
const sessionProxies = new WeakMap();
|
||||
let resolveSession;
|
||||
const sessionPromise = new Promise(resolve => { resolveSession = resolve; });
|
||||
|
||||
// 原生导出可能含只读、不可配置的属性,使用独立 target 避免替换方法时违反 Proxy 约束。
|
||||
// 同时保留导出枚举、prototype 和属性探测,不能把 wrapper 伪装成空对象。
|
||||
function nativeProxy (target, get, construct) {
|
||||
// bound function 没有不可配置的 prototype/caller 属性,可转发原生 class 的属性。
|
||||
// eslint-disable-next-line no-extra-bind
|
||||
const facade = typeof target === 'function' ? function () {}.bind(null) : {};
|
||||
const proxy = new Proxy(facade, {
|
||||
get: (_, key) => get(key),
|
||||
has: (_, key) => Reflect.has(target, key),
|
||||
ownKeys: () => Reflect.ownKeys(target),
|
||||
getOwnPropertyDescriptor: (_, key) => {
|
||||
const descriptor = Reflect.getOwnPropertyDescriptor(target, key);
|
||||
return descriptor ? { ...descriptor, configurable: true } : undefined;
|
||||
},
|
||||
getPrototypeOf: () => Reflect.getPrototypeOf(target),
|
||||
set: (_, key, value) => Reflect.set(target, key, value, target),
|
||||
apply: (_, thisArg, args) => Reflect.apply(target, thisArg, args),
|
||||
construct: (_, args, newTarget) => {
|
||||
const instance = Reflect.construct(target, args, newTarget === proxy ? target : newTarget);
|
||||
return construct ? construct(instance) : instance;
|
||||
},
|
||||
});
|
||||
return proxy;
|
||||
}
|
||||
|
||||
function callInitCallback (callback) {
|
||||
try {
|
||||
callback();
|
||||
} catch (error) {
|
||||
// NapCat 的回调不能中断 QQ 自己的初始化回调。
|
||||
console.log('[NapCat] [Error] 初始化回调失败', error);
|
||||
}
|
||||
}
|
||||
|
||||
function registerInitCallback (callback) {
|
||||
if (sessionReady) callInitCallback(callback);
|
||||
else initCallbacks.add(callback);
|
||||
}
|
||||
|
||||
function markSessionReady (session) {
|
||||
if (session !== wrapperSession || sessionReady) return;
|
||||
sessionReady = true;
|
||||
for (const callback of initCallbacks) callInitCallback(callback);
|
||||
initCallbacks.clear();
|
||||
}
|
||||
|
||||
function wrapSessionListener (listener, session) {
|
||||
const target = listener && typeof listener === 'object' ? listener : {};
|
||||
const methods = new Map();
|
||||
// 缺失第四参/原方法时也提供监听器,不修改 QQ 的原对象(可能已冻结)。
|
||||
const telemetryCallback = (...args) => {
|
||||
const origin = target.onOpentelemetryInit;
|
||||
const ret = typeof origin === 'function' ? Reflect.apply(origin, target, args) : undefined;
|
||||
if (args[0]?.is_init) markSessionReady(session);
|
||||
return ret;
|
||||
};
|
||||
const listenerTarget = Object.create(Object.getPrototypeOf(target), {
|
||||
...Object.getOwnPropertyDescriptors(target),
|
||||
onOpentelemetryInit: { value: telemetryCallback, enumerable: true, configurable: true },
|
||||
});
|
||||
return nativeProxy(listenerTarget, key => {
|
||||
if (key === 'onOpentelemetryInit') return telemetryCallback;
|
||||
const value = Reflect.get(target, key, target);
|
||||
if (typeof value !== 'function' || key === 'constructor') return value;
|
||||
if (!methods.has(value)) methods.set(value, value.bind(target));
|
||||
return methods.get(value);
|
||||
});
|
||||
}
|
||||
|
||||
function captureSession (session) {
|
||||
if (!session || typeof session.init !== 'function') {
|
||||
if (!warnedMissingSession) {
|
||||
warnedMissingSession = true;
|
||||
console.log('[NapCat] [Warn] QQ 尚未返回有效 Session,等待 QQ 后续创建会话');
|
||||
}
|
||||
return session;
|
||||
}
|
||||
if (!wrapperSession) {
|
||||
wrapperSession = session;
|
||||
resolveSession(session);
|
||||
}
|
||||
if (sessionProxies.has(session)) return sessionProxies.get(session);
|
||||
const methods = new Map();
|
||||
const proxy = nativeProxy(session, key => {
|
||||
const value = Reflect.get(session, key, session);
|
||||
if (typeof value !== 'function' || key === 'constructor') return value;
|
||||
if (!methods.has(key)) {
|
||||
methods.set(key, (...args) => {
|
||||
if (key === 'init') args[3] = wrapSessionListener(args[3], session);
|
||||
return Reflect.apply(value, session, args);
|
||||
});
|
||||
}
|
||||
return methods.get(key);
|
||||
});
|
||||
sessionProxies.set(session, proxy);
|
||||
return proxy;
|
||||
}
|
||||
|
||||
function createSessionServiceProxy (service) {
|
||||
const methods = new Map();
|
||||
return nativeProxy(service, key => {
|
||||
const value = Reflect.get(service, key, service);
|
||||
if (typeof value !== 'function' || key === 'constructor') return value;
|
||||
if (!methods.has(key)) {
|
||||
methods.set(key, (...args) => {
|
||||
// 使用原生 class 作为 this;把 QQ 的参数原样传递,不能提前固定调用 nt_1。
|
||||
const ret = Reflect.apply(value, service, args);
|
||||
return key === 'create' || key === 'getNTWrapperSession' ? captureSession(ret) : ret;
|
||||
});
|
||||
}
|
||||
return methods.get(key);
|
||||
}, captureSession);
|
||||
}
|
||||
|
||||
function clearHook () {
|
||||
process.dlopen = dlopenOrig;
|
||||
}
|
||||
|
||||
async function initializeNapCat () {
|
||||
console.log('[NapCat] [Info] 开始初始化NapCat');
|
||||
try {
|
||||
const currentPath = path.dirname(__filename);
|
||||
const { NCoreInitFramework, getWebUiUrl } = await import(pathToFileURL(path.join(currentPath, './napcat.mjs')).href);
|
||||
getWebUiUrlFunc = getWebUiUrl;
|
||||
await NCoreInitFramework(sessionPromise, wrapperLoginService, registerInitCallback);
|
||||
} catch (error) {
|
||||
console.log('[NapCat] [Error] 初始化NapCat', error);
|
||||
}
|
||||
}
|
||||
|
||||
process.dlopen = function (module, filename, flags = os.constants.dlopen.RTLD_LAZY) {
|
||||
const dlopenRet = dlopenOrig(module, filename, flags);
|
||||
if (path.basename(filename) !== 'wrapper.node' || wrapperHooked) return dlopenRet;
|
||||
wrapperHooked = true;
|
||||
clearHook();
|
||||
wrapperNodeApi = module.exports;
|
||||
wrapperLoginService = wrapperNodeApi.NodeIKernelLoginService.get();
|
||||
const sessionService = createSessionServiceProxy(wrapperNodeApi.NodeIQQNTWrapperSession);
|
||||
module.exports = nativeProxy(wrapperNodeApi, key => {
|
||||
if (key === 'NodeIQQNTWrapperSession') return sessionService;
|
||||
return Reflect.get(wrapperNodeApi, key, wrapperNodeApi);
|
||||
});
|
||||
|
||||
// 先安装拦截,再异步加载 Framework。会话由 QQ 创建,不调用已移除的工厂或抢跑初始化。
|
||||
initializeNapCat().catch(error => console.log('[NapCat] [Error] 初始化NapCat', error));
|
||||
|
||||
return dlopenRet;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
NCgetWebUiUrl: async () => {
|
||||
if (!getWebUiUrlFunc) {
|
||||
console.log('[NapCat] [Error] 未初始化完成');
|
||||
return '';
|
||||
}
|
||||
return await getWebUiUrlFunc();
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,454 @@
|
||||
import { NapCatPathWrapper } from 'napcat-common/src/path';
|
||||
import { InitWebUi, WebUiConfig, webUiRuntimePort } from 'napcat-webui-backend/index';
|
||||
import { NapCatAdapterManager } from 'napcat-adapter';
|
||||
import { NativePacketHandler } from 'napcat-core/packet/handler/client';
|
||||
import { Napi2NativeLoader } from 'napcat-core/packet/handler/napi2nativeLoader';
|
||||
import { loadNapcatConfig } from '@/napcat-core/helper/config';
|
||||
import { FFmpegService } from 'napcat-core/helper/ffmpeg/ffmpeg';
|
||||
import { logSubscription, LogWrapper } from 'napcat-core/helper/log';
|
||||
import { QQBasicInfoWrapper } from '@/napcat-core/helper/qq-basic-info';
|
||||
import { InstanceContext, loadQQWrapper, NapCatCore, NapCatCoreWorkingEnv, NodeIKernelLoginListener, NodeIKernelLoginService, NodeIQQNTWrapperSession, SelfInfo, WrapperNodeApi } from '@/napcat-core';
|
||||
import { proxiedListenerOf } from '@/napcat-core/helper/proxy-handler';
|
||||
import { createOfflineRecovery } from '@/napcat-core/helper/offline-recovery';
|
||||
import { statusHelperSubscription } from '@/napcat-core/helper/status';
|
||||
import { applyPendingUpdates } from '@/napcat-webui-backend/src/api/UpdateNapCat';
|
||||
import { WebUiDataRuntime } from '@/napcat-webui-backend/src/helper/Data';
|
||||
import { NapProtoMsg } from 'napcat-protobuf';
|
||||
import { OidbSvcTrpcTcpBase, OidbSvcTrpcTcp0XCDE_2RespBody } from '@/napcat-core/packet/transformer/proto';
|
||||
|
||||
// Framework ES入口文件
|
||||
export async function getWebUiUrl () {
|
||||
const WebUiConfigData = (await WebUiConfig.GetWebUIConfig());
|
||||
return 'http://127.0.0.1:' + webUiRuntimePort + '/webui/?token=' + encodeURIComponent(WebUiConfigData.token);
|
||||
}
|
||||
|
||||
export async function NCoreInitFramework (
|
||||
session: NodeIQQNTWrapperSession | Promise<NodeIQQNTWrapperSession>,
|
||||
loginService: NodeIKernelLoginService,
|
||||
registerInitCallback: (callback: () => void) => void
|
||||
) {
|
||||
// 在进入本层前是否登录未进行判断
|
||||
console.log('NapCat Framework App Loading...');
|
||||
|
||||
process.on('uncaughtException', (err) => {
|
||||
console.log('[NapCat] [Error] Unhandled Exception:', err.message);
|
||||
});
|
||||
|
||||
process.on('unhandledRejection', (reason) => {
|
||||
console.log('[NapCat] [Error] unhandledRejection:', reason);
|
||||
});
|
||||
|
||||
const pathWrapper = new NapCatPathWrapper();
|
||||
|
||||
const logger = new LogWrapper(pathWrapper.logsPath);
|
||||
await applyPendingUpdates(pathWrapper, logger);
|
||||
const basicInfoWrapper = new QQBasicInfoWrapper({ logger });
|
||||
const wrapper = loadQQWrapper(basicInfoWrapper.QQMainPath, basicInfoWrapper.getFullQQVersion());
|
||||
const nativePacketHandler = new NativePacketHandler({ logger }); // 初始化 NativePacketHandler 用于后续使用
|
||||
const napi2nativeLoader = new Napi2NativeLoader({ logger }); // 初始化 Napi2NativeLoader 用于后续使用
|
||||
const napcatConfig = loadNapcatConfig(pathWrapper.configPath);
|
||||
// console.log('[NapCat] [Napi2NativeLoader]', napi2nativeLoader.nativeExports.enableAllBypasses?.());
|
||||
if (process.env['NAPCAT_DISABLE_BYPASS'] !== '1') {
|
||||
const bypassOptions = napcatConfig.bypass ?? {};
|
||||
const bypassEnabled = napi2nativeLoader.nativeExports.enableAllBypasses?.(bypassOptions);
|
||||
if (bypassEnabled) {
|
||||
logger.log('[NapCat] Napi2NativeLoader: 已启用Bypass');
|
||||
}
|
||||
logger.log('[NapCat] Napi2NativeLoader: Framework模式Bypass配置:', bypassOptions);
|
||||
} else {
|
||||
logger.log('[NapCat] Napi2NativeLoader: Bypass已通过环境变量禁用');
|
||||
}
|
||||
// nativePacketHandler.onAll((packet) => {
|
||||
// console.log('[Packet]', packet.uin, packet.cmd, packet.hex_data);
|
||||
// });
|
||||
await nativePacketHandler.init(basicInfoWrapper.getFullQQVersion(), napcatConfig.o3HookMode === 1);
|
||||
// 在 init 之后注册监听器
|
||||
|
||||
// 登录前监听 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('[NapCat] 已启用数据库辅助支持能力');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
logger.logError('[NapCat] [0xCDE_2] 解析失败:', e);
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化 FFmpeg 服务
|
||||
await FFmpegService.init(pathWrapper.binaryPath, logger);
|
||||
|
||||
// 提前启动 WebUI,使其在登录前可用,支持通过 WebUI 操控登录过程
|
||||
WebUiDataRuntime.setWorkingEnv(NapCatCoreWorkingEnv.Framework);
|
||||
InitWebUi(logger, pathWrapper, logSubscription, statusHelperSubscription).then().catch(e => logger.logError(e));
|
||||
|
||||
// 直到登录成功后,执行下一步
|
||||
const selfInfo = await new Promise<SelfInfo>((resolve) => {
|
||||
const loginContext = { isLogined: false };
|
||||
|
||||
const loginListener = new NodeIKernelLoginListener();
|
||||
|
||||
loginListener.onUserLoggedIn = (userid: string) => {
|
||||
const tips = `当前账号(${userid})已登录,无法重复登录`;
|
||||
logger.logError(tips);
|
||||
WebUiDataRuntime.setQQLoginError(tips);
|
||||
};
|
||||
|
||||
loginListener.onQRCodeLoginSucceed = async (loginResult) => {
|
||||
loginContext.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');
|
||||
}
|
||||
await new Promise<void>(resolve => {
|
||||
registerInitCallback(() => resolve());
|
||||
});
|
||||
resolve({
|
||||
uid: loginResult.uid,
|
||||
uin: loginResult.uin,
|
||||
nick: '', // 获取不到
|
||||
online: true,
|
||||
});
|
||||
};
|
||||
|
||||
loginListener.onQRCodeGetPicture = ({ qrcodeUrl }) => {
|
||||
WebUiDataRuntime.setQQLoginQrcodeURL(qrcodeUrl);
|
||||
WebUiDataRuntime.setQQLoginError('');
|
||||
WebUiDataRuntime.setQQLoginPhase('waiting_qrcode');
|
||||
logger.log('[NapCat] [Framework] 二维码已更新, URL:', qrcodeUrl);
|
||||
};
|
||||
|
||||
loginListener.onQRCodeSessionFailed = (errType: number, errCode: number) => {
|
||||
if (!loginContext.isLogined) {
|
||||
logger.logError('[NapCat] [Framework] [Login] QRCode Session Failed, ErrType:', errType, 'ErrCode:', errCode);
|
||||
if (errType === 1 && errCode === 3) {
|
||||
WebUiDataRuntime.setQQLoginError('二维码已过期,请刷新');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loginListener.onLoginFailed = (...args) => {
|
||||
const errInfo = JSON.stringify(args);
|
||||
logger.logError('[NapCat] [Framework] [Login] Login Failed, ErrInfo:', errInfo);
|
||||
WebUiDataRuntime.setQQLoginError(`登录失败: ${errInfo}`);
|
||||
};
|
||||
|
||||
loginListener.onLoginConnected = () => {
|
||||
logger.log('[NapCat] [Framework] 登录服务已连接');
|
||||
// 注册 WebUI 登录操作回调
|
||||
registerWebUiLoginCallbacks(loginService, loginContext, logger);
|
||||
// 获取历史登录列表供 WebUI 使用
|
||||
loginService.getLoginList().then((res) => {
|
||||
const list = res.LocalLoginInfoList.filter((item) => item.isQuickLogin);
|
||||
WebUiDataRuntime.setQQQuickLoginList(list.map((item) => item.uin.toString()));
|
||||
WebUiDataRuntime.setQQNewLoginList(list);
|
||||
}).catch(e => logger.logError('[NapCat] [Framework] 获取登录列表失败:', e));
|
||||
if (!loginContext.isLogined) {
|
||||
const requested = loginService.getQRCodePicture();
|
||||
logger.logWarn('[NapCat] [Framework] 登录服务连接完成,正在获取二维码', { requested, msfStatus: loginService.getMsfStatus() });
|
||||
}
|
||||
};
|
||||
|
||||
loginListener.onQRCodeSessionUserScaned = () => {
|
||||
WebUiDataRuntime.setQQLoginPhase('qrcode_scanned');
|
||||
logger.log('[NapCat] [Framework] 二维码已被扫描,等待确认...');
|
||||
};
|
||||
|
||||
loginListener.onPasswordLoginFailed = (...args) => {
|
||||
logger.logError('[NapCat] [Framework] 密码登录失败:', ...args);
|
||||
};
|
||||
|
||||
loginService.addKernelLoginListener(proxiedListenerOf(loginListener, logger));
|
||||
});
|
||||
// 过早进入会导致addKernelMsgListener等Listener添加失败
|
||||
// await sleep(2500);
|
||||
// 初始化 NapCatFramework
|
||||
// 新版 wrapper 的会话由 QQ 在启动过程中创建,dlopen 时可能尚不存在。
|
||||
const wrapperSession = await session;
|
||||
if (!wrapperSession) throw new Error('QQ 未提供有效的 Framework Session');
|
||||
const loaderObject = new NapCatFramework(wrapper, wrapperSession, logger, selfInfo, basicInfoWrapper, pathWrapper, nativePacketHandler, napi2nativeLoader);
|
||||
// 将捕获的 passphrase 设置到 core 上,供 DatabaseApi 使用
|
||||
if (dbPassphrase) {
|
||||
loaderObject.core.dbPassphrase = dbPassphrase;
|
||||
}
|
||||
await loaderObject.core.initCore();
|
||||
|
||||
// 登录成功后设置数据路径
|
||||
WebUiDataRuntime.setQQDataPath(loaderObject.core.dataPath);
|
||||
|
||||
// // 测试 DatabaseApi
|
||||
// if (dbPassphrase) {
|
||||
// const dbApi = loaderObject.core.apis.DatabaseApi;
|
||||
// const sqliteOk = await dbApi.isSqliteAvailable();
|
||||
// if (sqliteOk) {
|
||||
// try {
|
||||
// const result = dbApi.readDatabase('nt_msg.db');
|
||||
// if (result) {
|
||||
// logger.log('[NapCat] [Database] nt_msg.db:\n' + dbApi.formatResults([result]));
|
||||
// }
|
||||
// } catch (e) {
|
||||
// logger.logError('[NapCat] [Database] 读取失败:', e);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// 使用 NapCatAdapterManager 统一管理协议适配器
|
||||
const adapterManager = new NapCatAdapterManager(loaderObject.core, loaderObject.context, pathWrapper);
|
||||
await adapterManager.initAdapters();
|
||||
// 注册 OneBot 适配器到 WebUiDataRuntime,供调试功能使用
|
||||
const oneBotAdapter = adapterManager.getOneBotAdapter();
|
||||
if (oneBotAdapter) {
|
||||
WebUiDataRuntime.setOneBotContext(oneBotAdapter);
|
||||
if (WebUiDataRuntime.getQQLoginStatus()) {
|
||||
WebUiDataRuntime.setQQLoginPhase('ready');
|
||||
}
|
||||
}
|
||||
|
||||
// 监听下线并同步到 WebUI(兜底保障,防止 WebUI 状态不同步)
|
||||
// KickedOffLine:账号被踢下线;SelfOffline:静默离线(服务端会话语义失效但无通知)。
|
||||
// 两者复用同一条恢复链路:同步 WebUI -> 重启 Worker -> 快速登录。
|
||||
const handleOffline = createOfflineRecovery({
|
||||
isOnline: () => loaderObject.core.selfInfo.online === true,
|
||||
onOffline: (tips) => {
|
||||
WebUiDataRuntime.setQQLoginStatus(false);
|
||||
// 下线后的二维码已失效,绝不能继续交给页面展示或扫码。
|
||||
WebUiDataRuntime.setQQLoginQrcodeURL('');
|
||||
WebUiDataRuntime.setQQLoginError(tips);
|
||||
WebUiDataRuntime.setQQLoginPhase('reconnecting');
|
||||
},
|
||||
onRecovered: () => {
|
||||
WebUiDataRuntime.setQQLoginStatus(true);
|
||||
WebUiDataRuntime.setQQLoginError('');
|
||||
WebUiDataRuntime.setQQLoginPhase('ready');
|
||||
},
|
||||
restart: async (tips) => {
|
||||
try {
|
||||
const restartResult = await WebUiDataRuntime.requestRestartProcess();
|
||||
logger.logWarn('[NapCat] [Framework] 账号已离线,正在重启 Worker 以重新创建 QQ 登录服务', restartResult);
|
||||
if (restartResult.result) return true;
|
||||
WebUiDataRuntime.setQQLoginPhase('offline');
|
||||
WebUiDataRuntime.setQQLoginError(`${tips}\n登录服务重启失败:${restartResult.message}`);
|
||||
} catch (error) {
|
||||
WebUiDataRuntime.setQQLoginPhase('offline');
|
||||
WebUiDataRuntime.setQQLoginError(`${tips}\n登录服务重启失败:${(error as Error).message}`);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
});
|
||||
loaderObject.core.event.on('KickedOffLine', (tips: string) => handleOffline(tips, false));
|
||||
loaderObject.core.event.on('SelfOffline', (tips: string) => handleOffline(tips, true));
|
||||
}
|
||||
|
||||
function registerWebUiLoginCallbacks (
|
||||
loginService: NodeIKernelLoginService,
|
||||
loginContext: { isLogined: boolean; },
|
||||
logger: LogWrapper
|
||||
) {
|
||||
// 刷新二维码
|
||||
WebUiDataRuntime.setRefreshQRCodeCallback(async () => {
|
||||
loginContext.isLogined = false;
|
||||
const msfStatus = loginService.getMsfStatus();
|
||||
if (msfStatus === 2) {
|
||||
WebUiDataRuntime.setQQLoginPhase('reconnecting');
|
||||
const connected = loginService.connect();
|
||||
logger.logWarn('[NapCat] [Framework] 登录服务离线,正在重连以获取二维码', { connected, msfStatus });
|
||||
if (!connected) {
|
||||
throw new Error('QQ 登录服务无法重新连接');
|
||||
}
|
||||
return;
|
||||
}
|
||||
const requested = loginService.getQRCodePicture();
|
||||
logger.logWarn('[NapCat] [Framework] 请求刷新二维码', { requested, msfStatus });
|
||||
if (!requested) {
|
||||
throw new Error('QQ 未接受二维码刷新请求');
|
||||
}
|
||||
});
|
||||
|
||||
// 快速登录
|
||||
WebUiDataRuntime.setQuickLoginCall(async (uin: string) => {
|
||||
return await new Promise((resolve) => {
|
||||
if (!uin) {
|
||||
return resolve({ result: false, message: '快速登录失败' });
|
||||
}
|
||||
logger.log('[NapCat] [Framework] 正在快速登录', uin);
|
||||
loginService.quickLoginWithUin(uin).then(res => {
|
||||
const success = res.result === '0' && !res.loginErrorInfo?.errMsg;
|
||||
if (!success) {
|
||||
const errMsg = res.loginErrorInfo?.errMsg || `快速登录失败,错误码: ${res.result}`;
|
||||
WebUiDataRuntime.setQQLoginError(errMsg);
|
||||
resolve({ result: false, message: errMsg });
|
||||
} else {
|
||||
loginContext.isLogined = true;
|
||||
WebUiDataRuntime.setQQLoginStatus(true);
|
||||
WebUiDataRuntime.setQQLoginError('');
|
||||
resolve({ result: true, message: '' });
|
||||
}
|
||||
}).catch((e) => {
|
||||
logger.logError('[NapCat] [Framework] 快速登录异常:', e);
|
||||
WebUiDataRuntime.setQQLoginError('快速登录发生错误');
|
||||
resolve({ result: false, message: '快速登录发生错误' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 密码登录
|
||||
WebUiDataRuntime.setPasswordLoginCall(async (uin: string, passwordMd5: string) => {
|
||||
return await new Promise((resolve) => {
|
||||
if (!uin || !passwordMd5) {
|
||||
return resolve({ result: false, message: '密码登录失败:参数不完整' });
|
||||
}
|
||||
logger.log('[NapCat] [Framework] 正在密码登录', 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 || '';
|
||||
resolve({ result: false, message: '需要验证码', needCaptcha: true, proofWaterUrl });
|
||||
} else if (res.result === '140022010' || res.result === '140022011') {
|
||||
const jumpUrl = res.loginErrorInfo?.jumpUrl || '';
|
||||
const newDevicePullQrCodeSig = res.loginErrorInfo?.newDevicePullQrCodeSig || '';
|
||||
resolve({ result: false, message: '新设备需要扫码验证', needNewDevice: true, jumpUrl, newDevicePullQrCodeSig });
|
||||
} else if (res.result !== '0') {
|
||||
const errMsg = res.loginErrorInfo?.errMsg || '密码登录失败';
|
||||
WebUiDataRuntime.setQQLoginError(errMsg);
|
||||
resolve({ result: false, message: errMsg });
|
||||
} else {
|
||||
loginContext.isLogined = true;
|
||||
WebUiDataRuntime.setQQLoginStatus(true);
|
||||
WebUiDataRuntime.setQQLoginError('');
|
||||
resolve({ result: true, message: '' });
|
||||
}
|
||||
}).catch((e) => {
|
||||
logger.logError('[NapCat] [Framework] 密码登录异常:', e);
|
||||
WebUiDataRuntime.setQQLoginError('密码登录发生错误');
|
||||
resolve({ result: false, message: '密码登录发生错误' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 验证码登录(密码登录需要验证码时的第二步)
|
||||
WebUiDataRuntime.setCaptchaLoginCall(async (uin: string, passwordMd5: string, ticket: string, randstr: string, sid: string) => {
|
||||
return await new Promise((resolve) => {
|
||||
if (!uin || !passwordMd5 || !ticket) {
|
||||
return resolve({ result: false, message: '验证码登录失败:参数不完整' });
|
||||
}
|
||||
logger.log('[NapCat] [Framework] 正在验证码登录', 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 => {
|
||||
if (res.result === '140022010' || res.result === '140022011') {
|
||||
const jumpUrl = res.loginErrorInfo?.jumpUrl || '';
|
||||
const newDevicePullQrCodeSig = res.loginErrorInfo?.newDevicePullQrCodeSig || '';
|
||||
resolve({ result: false, message: '新设备需要扫码验证', needNewDevice: true, jumpUrl, newDevicePullQrCodeSig });
|
||||
} else if (res.result !== '0') {
|
||||
const errMsg = res.loginErrorInfo?.errMsg || '验证码登录失败';
|
||||
WebUiDataRuntime.setQQLoginError(errMsg);
|
||||
resolve({ result: false, message: errMsg });
|
||||
} else {
|
||||
loginContext.isLogined = true;
|
||||
WebUiDataRuntime.setQQLoginStatus(true);
|
||||
WebUiDataRuntime.setQQLoginError('');
|
||||
resolve({ result: true, message: '' });
|
||||
}
|
||||
}).catch((e) => {
|
||||
logger.logError('[NapCat] [Framework] 验证码登录异常:', e);
|
||||
WebUiDataRuntime.setQQLoginError('验证码登录发生错误');
|
||||
resolve({ result: false, message: '验证码登录发生错误' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 新设备验证登录(密码登录需要新设备验证时的第二步)
|
||||
WebUiDataRuntime.setNewDeviceLoginCall(async (uin: string, passwordMd5: string, newDevicePullQrCodeSig: string) => {
|
||||
return await new Promise((resolve) => {
|
||||
if (!uin || !passwordMd5 || !newDevicePullQrCodeSig) {
|
||||
return resolve({ result: false, message: '新设备验证登录失败:参数不完整' });
|
||||
}
|
||||
logger.log('[NapCat] [Framework] 正在新设备验证登录', 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 sig = res.loginErrorInfo?.newDevicePullQrCodeSig || '';
|
||||
resolve({ result: false, message: '异常设备需要验证', needNewDevice: true, jumpUrl, newDevicePullQrCodeSig: sig });
|
||||
} else if (res.result !== '0') {
|
||||
const errMsg = res.loginErrorInfo?.errMsg || '新设备验证登录失败';
|
||||
WebUiDataRuntime.setQQLoginError(errMsg);
|
||||
resolve({ result: false, message: errMsg });
|
||||
} else {
|
||||
loginContext.isLogined = true;
|
||||
WebUiDataRuntime.setQQLoginStatus(true);
|
||||
WebUiDataRuntime.setQQLoginError('');
|
||||
resolve({ result: true, message: '' });
|
||||
}
|
||||
}).catch((e) => {
|
||||
logger.logError('[NapCat] [Framework] 新设备验证登录异常:', e);
|
||||
WebUiDataRuntime.setQQLoginError('新设备验证登录发生错误');
|
||||
resolve({ result: false, message: '新设备验证登录发生错误' });
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export class NapCatFramework {
|
||||
public core: NapCatCore;
|
||||
context: InstanceContext;
|
||||
|
||||
constructor (
|
||||
wrapper: WrapperNodeApi,
|
||||
session: NodeIQQNTWrapperSession,
|
||||
logger: LogWrapper,
|
||||
selfInfo: SelfInfo,
|
||||
basicInfoWrapper: QQBasicInfoWrapper,
|
||||
pathWrapper: NapCatPathWrapper,
|
||||
packetHandler: NativePacketHandler,
|
||||
napi2nativeLoader: Napi2NativeLoader
|
||||
) {
|
||||
this.context = {
|
||||
packetHandler,
|
||||
napi2nativeLoader,
|
||||
workingEnv: NapCatCoreWorkingEnv.Framework,
|
||||
wrapper,
|
||||
session,
|
||||
logger,
|
||||
basicInfoWrapper,
|
||||
pathWrapper,
|
||||
};
|
||||
this.core = new NapCatCore(this.context, selfInfo);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { pathToFileURL } = require('url');
|
||||
|
||||
async function initializeNapCat (session, loginService, registerCallback) {
|
||||
// const logFile = path.join(currentPath, 'napcat.log');
|
||||
|
||||
console.log('[NapCat] [Info] 开始初始化NapCat');
|
||||
|
||||
// fs.writeFileSync(logFile, '', { flag: 'w' });
|
||||
|
||||
// fs.writeFileSync(logFile, '[NapCat] [Info] NapCat 初始化成功\n', { flag: 'a' });
|
||||
|
||||
try {
|
||||
const currentPath = path.dirname(__filename);
|
||||
const { NCoreInitFramework } = await import(pathToFileURL(path.join(currentPath, './napcat.mjs')).href);
|
||||
await NCoreInitFramework(session, loginService, (callback) => { registerCallback(callback); });
|
||||
} catch (error) {
|
||||
console.log('[NapCat] [Error] 初始化NapCat', error);
|
||||
// fs.writeFileSync(logFile, `[NapCat] [Error] 初始化NapCat失败: ${error.message}\n`, { flag: 'a' });
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
initializeNapCat,
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "napcat-framework",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"typecheck": "tsc --noEmit --skipLibCheck -p tsconfig.json"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./index.ts"
|
||||
},
|
||||
"./*": {
|
||||
"import": "./*"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"napcat-core": "workspace:*",
|
||||
"napcat-common": "workspace:*",
|
||||
"napcat-database": "workspace:*",
|
||||
"napcat-protobuf": "workspace:*",
|
||||
"napcat-adapter": "workspace:*",
|
||||
"napcat-webui-backend": "workspace:*",
|
||||
"napcat-vite": "workspace:*",
|
||||
"napcat-qrcode": "workspace:*",
|
||||
"json5": "^2.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": ".."
|
||||
},
|
||||
"include": [
|
||||
"*.ts",
|
||||
"**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import cp from 'vite-plugin-cp';
|
||||
import { defineConfig, PluginOption, UserConfig } from 'vite';
|
||||
import path, { resolve } from 'path';
|
||||
import nodeResolve from '@rollup/plugin-node-resolve';
|
||||
import { autoIncludeTSPlugin } from 'napcat-vite/vite-auto-include.js';
|
||||
import { builtinModules } from 'module';
|
||||
import react from '@vitejs/plugin-react-swc';
|
||||
import napcatVersion from 'napcat-vite/vite-plugin-version.js';
|
||||
// 依赖排除
|
||||
const external = [
|
||||
'ws',
|
||||
'express',
|
||||
];
|
||||
const nodeModules = [...builtinModules, builtinModules.map((m) => `node:${m}`), 'node:sqlite'].flat();
|
||||
const FrameworkBaseConfigPlugin: PluginOption[] = [
|
||||
autoIncludeTSPlugin({
|
||||
entries: [
|
||||
{ entry: 'napcat.ts', dir: path.resolve(__dirname, '../napcat-core/protocol') },
|
||||
{ entry: 'napcat.ts', dir: path.resolve(__dirname, '../napcat-onebot/action/test') },
|
||||
],
|
||||
}),
|
||||
react({ tsDecorators: true }),
|
||||
cp({
|
||||
targets: [
|
||||
{ src: '../napcat-napi-loader/', dest: 'dist', flatten: true },
|
||||
{ src: '../napcat-native/', dest: 'dist/native', flatten: false },
|
||||
{ src: '../napcat-core/external/napcat.json', dest: 'dist/config/' },
|
||||
{ src: '../napcat-webui-frontend/dist/', dest: 'dist/static/', flatten: false },
|
||||
{ src: './napcat.cjs', dest: 'dist' },
|
||||
{ src: './nativeLoader.cjs', dest: 'dist' },
|
||||
{ src: './preload.cjs', dest: 'dist' },
|
||||
{ src: './renderer.js', dest: 'dist' },
|
||||
{ src: '../../package.json', dest: 'dist' },
|
||||
{ src: '../../logo.png', dest: 'dist' },
|
||||
],
|
||||
}),
|
||||
nodeResolve(),
|
||||
napcatVersion(),
|
||||
];
|
||||
const FrameworkBaseConfig = () =>
|
||||
defineConfig({
|
||||
resolve: {
|
||||
conditions: ['node', 'default'],
|
||||
alias: {
|
||||
'@/napcat-core': resolve(__dirname, '../napcat-core'),
|
||||
'@/napcat-common': resolve(__dirname, '../napcat-common'),
|
||||
'@/napcat-onebot': resolve(__dirname, '../napcat-onebot'),
|
||||
'@/napcat-pty': resolve(__dirname, '../napcat-pty'),
|
||||
'@/napcat-webui-backend': resolve(__dirname, '../napcat-webui-backend'),
|
||||
'@/napcat-image-size': resolve(__dirname, '../napcat-image-size'),
|
||||
'@/napcat-protocol': resolve(__dirname, '../napcat-protocol'),
|
||||
'@/napcat-adapter': resolve(__dirname, '../napcat-adapter'),
|
||||
},
|
||||
},
|
||||
build: {
|
||||
sourcemap: false,
|
||||
target: 'esnext',
|
||||
minify: false,
|
||||
lib: {
|
||||
entry: {
|
||||
napcat: path.resolve(__dirname, 'napcat.ts'),
|
||||
'worker/conoutSocketWorker': path.resolve(__dirname, '../napcat-pty/worker/conoutSocketWorker.ts'),
|
||||
},
|
||||
formats: ['es'],
|
||||
fileName: (_, entryName) => `${entryName}.mjs`,
|
||||
},
|
||||
rollupOptions: {
|
||||
external: [...nodeModules, ...external],
|
||||
},
|
||||
},
|
||||
});
|
||||
export default defineConfig((): UserConfig => {
|
||||
return {
|
||||
...FrameworkBaseConfig(),
|
||||
plugins: [...FrameworkBaseConfigPlugin],
|
||||
};
|
||||
});
|
||||
Reference in new issue
Block a user