fix: 初步解耦 session到远程

This commit is contained in:
手瓜一十雪
2025-06-16 16:53:12 +08:00
parent 88e9caddfa
commit a2cae1734b
8 changed files with 203 additions and 120 deletions

View File

@@ -41,6 +41,7 @@
"ajv": "^8.13.0",
"async-mutex": "^0.5.0",
"commander": "^13.0.0",
"compressing": "^1.10.1",
"cors": "^2.8.5",
"esbuild": "0.25.5",
"eslint": "^9.14.0",
@@ -52,18 +53,18 @@
"globals": "^16.0.0",
"json5": "^2.2.3",
"multer": "^2.0.1",
"napcat.protobuf": "^1.1.4",
"typescript": "^5.3.3",
"typescript-eslint": "^8.13.0",
"vite": "^6.0.1",
"vite-plugin-cp": "^6.0.0",
"vite-tsconfig-paths": "^5.1.0",
"napcat.protobuf": "^1.1.4",
"winston": "^3.17.0",
"compressing": "^1.10.1"
"superjson": "^2.2.2",
"winston": "^3.17.0"
},
"dependencies": {
"express": "^5.0.0",
"silk-wasm": "^3.6.1",
"ws": "^8.18.0"
}
}
}

View File

@@ -142,7 +142,6 @@ export class NTQQMsgApi {
}
async queryFirstMsgBySender(peer: Peer, SendersUid: string[]) {
console.log(peer, SendersUid);
return await this.context.session.getMsgService().queryMsgsWithFilterEx('0', '0', '0', {
chatInfo: peer,
filterMsgType: [],

View File

@@ -30,7 +30,7 @@ import os from 'node:os';
import { NodeIKernelMsgListener, NodeIKernelProfileListener } from '@/core/listeners';
import { proxiedListenerOf } from '@/common/proxy-handler';
import { NTQQPacketApi } from './apis/packet';
import { createVirtualServiceClient, handleServiceServerOnce } from '@/framework/proxy/service';
import { createVirtualSession } from './virtualsession';
export * from './wrapper';
export * from './types';
export * from './services';
@@ -98,8 +98,10 @@ export class NapCatCore {
constructor(context: InstanceContext, selfInfo: SelfInfo) {
this.selfInfo = selfInfo;
this.context = context;
this.util = this.context.wrapper.NodeQQNTWrapperUtil;
this.eventWrapper = new NTEventWrapper(context.session);
this.context.session = createVirtualSession(this.eventWrapper);
this.configLoader = new NapCatConfigLoader(this, this.context.pathWrapper.configPath, NapcatConfigSchema);
this.apis = {
FileApi: new NTQQFileApi(this.context, this),
@@ -169,13 +171,6 @@ export class NapCatCore {
proxiedListenerOf(msgListener, this.context.logger),
);
let msgServiceClient = createVirtualServiceClient('NodeIKernelMsgService', async (ServiceCommand, ...args) => {
this.context.logger.log(`Client Outing->[${ServiceCommand}]`, ...args);
return handleServiceServerOnce(ServiceCommand, async (listenerCommand: string, ...args: any[]) => {
msgServiceClient.receiverListener(listenerCommand, ...args);
}, this.eventWrapper, ...args);
});
console.log('msgServiceClient', await msgServiceClient.object.fetchFavEmojiList('', 50, true, true));
const profileListener = new NodeIKernelProfileListener();
profileListener.onProfileDetailInfoChanged = (profile) => {
if (profile.uid === this.selfInfo.uid) {
@@ -259,13 +254,13 @@ export async function genSessionConfig(
}
export interface InstanceContext {
readonly workingEnv: NapCatCoreWorkingEnv;
readonly wrapper: WrapperNodeApi;
readonly session: NodeIQQNTWrapperSession;
readonly logger: LogWrapper;
readonly loginService: NodeIKernelLoginService;
readonly basicInfoWrapper: QQBasicInfoWrapper;
readonly pathWrapper: NapCatPathWrapper;
session: NodeIQQNTWrapperSession;
workingEnv: NapCatCoreWorkingEnv;
wrapper: WrapperNodeApi;
logger: LogWrapper;
loginService: NodeIKernelLoginService;
basicInfoWrapper: QQBasicInfoWrapper;
pathWrapper: NapCatPathWrapper;
}
export interface StableNTApiWrapper {

View File

@@ -40,7 +40,6 @@ export class NodeIKernelBuddyListener {
}
onDelBatchBuddyInfos(_arg: unknown): any {
console.log('onDelBatchBuddyInfos not implemented', ...arguments);
}
onDoubtBuddyReqChange(_arg:

View File

@@ -16,6 +16,16 @@ export * from './NodeIKernelDbToolsService';
export * from './NodeIKernelTipOffService';
export * from './NodeIKernelSearchService';
export * from './NodeIKernelCollectionService';
export * from './NodeIKernelAlbumService';
export * from './NodeIKernelECDHService';
export * from './NodeIKernelNodeMiscService';
export * from './NodeIKernelMsgBackupService';
export * from './NodeIKernelTianShuService';
export * from './NodeIKernelUnitedConfigService';
export * from './NodeIkernelTestPerformanceService';
export * from './NodeIKernelUixConvertService';
export * from './NodeIKernelMSFService';
export * from './NodeIKernelRecentContactService';
import type {
NodeIKernelAvatarService,
@@ -37,6 +47,15 @@ import type {
NodeIKernelTipOffService,
} from '.';
import { NodeIKernelAlbumService } from './NodeIKernelAlbumService';
import { NodeIKernelECDHService } from './NodeIKernelECDHService';
import { NodeIKernelNodeMiscService } from './NodeIKernelNodeMiscService';
import { NodeIKernelMsgBackupService } from './NodeIKernelMsgBackupService';
import { NodeIKernelTianShuService } from './NodeIKernelTianShuService';
import { NodeIKernelUnitedConfigService } from './NodeIKernelUnitedConfigService';
import { NodeIkernelTestPerformanceService } from './NodeIkernelTestPerformanceService';
import { NodeIKernelUixConvertService } from './NodeIKernelUixConvertService';
import { NodeIKernelMSFService } from './NodeIKernelMSFService';
import { NodeIKernelRecentContactService } from './NodeIKernelRecentContactService';
export type ServiceNamingMapping = {
NodeIKernelAlbumService: NodeIKernelAlbumService;
@@ -55,6 +74,15 @@ export type ServiceNamingMapping = {
NodeIKernelRichMediaService: NodeIKernelRichMediaService;
NodeIKernelDbToolsService: NodeIKernelDbToolsService;
NodeIKernelTipOffService: NodeIKernelTipOffService;
NodeIKernelSearchService: NodeIKernelSearchService,
NodeIKernelSearchService: NodeIKernelSearchService;
NodeIKernelCollectionService: NodeIKernelCollectionService;
NodeIKernelECDHService: NodeIKernelECDHService;
NodeIKernelNodeMiscService: NodeIKernelNodeMiscService;
NodeIKernelMsgBackupService: NodeIKernelMsgBackupService;
NodeIKernelTianShuService: NodeIKernelTianShuService;
NodeIKernelUnitedConfigService: NodeIKernelUnitedConfigService;
NodeIkernelTestPerformanceService: NodeIkernelTestPerformanceService;
NodeIKernelUixConvertService: NodeIKernelUixConvertService;
NodeIKernelMSFService: NodeIKernelMSFService;
NodeIKernelRecentContactService: NodeIKernelRecentContactService;
};

143
src/core/virtualSession.ts Normal file
View File

@@ -0,0 +1,143 @@
import { NTEventWrapper } from "@/common/event";
import { createVirtualServiceClient } from "@/framework/proxy/service";
import { handleServiceServerOnce } from "@/framework/proxy/service";
import { ServiceMethodCommand } from "@/framework/proxy/service";
import {
NodeIQQNTWrapperSession,
WrapperSessionInitConfig
} from "./wrapper";
import { NodeIKernelSessionListener } from "./listeners/NodeIKernelSessionListener";
import {
NodeIDependsAdapter,
NodeIDispatcherAdapter
} from "./adapters";
import superjson from "superjson";
class VirtualServiceManager {
private services: Map<string, any> = new Map();
private eventWrapper: NTEventWrapper;
constructor(eventWrapper: NTEventWrapper) {
this.eventWrapper = eventWrapper;
}
/**
* 创建虚拟服务实例
*/
private createVirtualService<T extends keyof import("@/core/services").ServiceNamingMapping>(
serviceName: T
): import("@/core/services").ServiceNamingMapping[T] {
if (this.services.has(serviceName)) {
return this.services.get(serviceName);
}
const serviceClient = createVirtualServiceClient(serviceName, async (serviceCommand, ...args) => {
const call_dto = superjson.stringify({ command: serviceCommand, params: args });
const call_data = superjson.parse<{ command: ServiceMethodCommand; params: any[] }>(call_dto);
return handleServiceServerOnce(
call_data.command,
async (listenerCommand: string, ...args: any[]) => {
const listener_dto = superjson.stringify({ command: listenerCommand, params: args });
const listener_data = superjson.parse<{ command: string; params: any[] }>(listener_dto);
serviceClient.receiverListener(listener_data.command, ...listener_data.params);
},
this.eventWrapper,
...call_data.params
);
});
this.services.set(serviceName, serviceClient.object);
return serviceClient.object;
}
/**
* 获取或创建服务实例
*/
getService<T extends keyof import("@/core/services").ServiceNamingMapping>(
serviceName: T
): import("@/core/services").ServiceNamingMapping[T] {
return this.createVirtualService(serviceName);
}
}
/**
*
* NodeIQQNTWrapperSession 的行为
*/
export class VirtualWrapperSession implements NodeIQQNTWrapperSession {
private serviceManager: VirtualServiceManager;
constructor(eventWrapper: NTEventWrapper) {
this.serviceManager = new VirtualServiceManager(eventWrapper);
}
create(): NodeIQQNTWrapperSession {
return new VirtualWrapperSession(this.serviceManager['eventWrapper']);
}
init(
_wrapperSessionInitConfig: WrapperSessionInitConfig,
_nodeIDependsAdapter: NodeIDependsAdapter,
_nodeIDispatcherAdapter: NodeIDispatcherAdapter,
_nodeIKernelSessionListener: NodeIKernelSessionListener,
): void {
}
startNT(_session?: number): void {
}
getBdhUploadService() { return null; }
getECDHService() { return this.serviceManager.getService('NodeIKernelECDHService'); }
getMsgService() { return this.serviceManager.getService('NodeIKernelMsgService'); }
getProfileService() { return this.serviceManager.getService('NodeIKernelProfileService'); }
getProfileLikeService() { return this.serviceManager.getService('NodeIKernelProfileLikeService'); }
getGroupService() { return this.serviceManager.getService('NodeIKernelGroupService'); }
getStorageCleanService() { return this.serviceManager.getService('NodeIKernelStorageCleanService'); }
getBuddyService() { return this.serviceManager.getService('NodeIKernelBuddyService'); }
getRobotService() { return this.serviceManager.getService('NodeIKernelRobotService'); }
getTicketService() { return this.serviceManager.getService('NodeIKernelTicketService'); }
getTipOffService() { return this.serviceManager.getService('NodeIKernelTipOffService'); }
getNodeMiscService() { return this.serviceManager.getService('NodeIKernelNodeMiscService'); }
getRichMediaService() { return this.serviceManager.getService('NodeIKernelRichMediaService'); }
getMsgBackupService() { return this.serviceManager.getService('NodeIKernelMsgBackupService'); }
getAlbumService() { return this.serviceManager.getService('NodeIKernelAlbumService'); }
getTianShuService() { return this.serviceManager.getService('NodeIKernelTianShuService'); }
getUnitedConfigService() { return this.serviceManager.getService('NodeIKernelUnitedConfigService'); }
getSearchService() { return this.serviceManager.getService('NodeIKernelSearchService'); }
getDirectSessionService() { return null; }
getRDeliveryService() { return null; }
getAvatarService() { return this.serviceManager.getService('NodeIKernelAvatarService'); }
getFeedChannelService() { return null; }
getYellowFaceService() { return null; }
getCollectionService() { return this.serviceManager.getService('NodeIKernelCollectionService'); }
getSettingService() { return null; }
getQiDianService() { return null; }
getFileAssistantService() { return this.serviceManager.getService('NodeIKernelFileAssistantService'); }
getGuildService() { return null; }
getSkinService() { return null; }
getTestPerformanceService() { return this.serviceManager.getService('NodeIkernelTestPerformanceService'); }
getQQPlayService() { return null; }
getDbToolsService() { return this.serviceManager.getService('NodeIKernelDbToolsService'); }
getUixConvertService() { return this.serviceManager.getService('NodeIKernelUixConvertService'); }
getOnlineStatusService() { return this.serviceManager.getService('NodeIKernelOnlineStatusService'); }
getRemotingService() { return null; }
getGroupTabService() { return null; }
getGroupSchoolService() { return null; }
getLiteBusinessService() { return null; }
getGuildMsgService() { return null; }
getLockService() { return null; }
getMSFService() { return this.serviceManager.getService('NodeIKernelMSFService'); }
getGuildHotUpdateService() { return null; }
getAVSDKService() { return null; }
getRecentContactService() { return this.serviceManager.getService('NodeIKernelRecentContactService'); }
getConfigMgrService() { return null; }
}
/**
* 创建完全虚拟的QQ NT会话
* @param eventWrapper 事件包装器
* @returns 虚拟会话实例
*/
export function createVirtualSession(eventWrapper: NTEventWrapper): NodeIQQNTWrapperSession {
return new VirtualWrapperSession(eventWrapper) as NodeIQQNTWrapperSession;
}

View File

@@ -1,17 +1,16 @@
import { FuncKeys, NTEventWrapper } from "@/common/event";
import { ServiceNamingMapping } from "@/core";
type ServiceMethodCommand = {
export type ServiceMethodCommand = {
[Service in keyof ServiceNamingMapping]: `${Service}/${FuncKeys<ServiceNamingMapping[Service]>}`
}[keyof ServiceNamingMapping];
export const RegisterListenerCmd: Array<ServiceMethodCommand> = [
'NodeIKernelMsgService/addKernelMsgListener',
'NodeIKernelGroupService/addKernelGroupListener',
'NodeIKernelProfileLikeService/addKernelProfileLikeListener',
'NodeIKernelProfileService/addKernelProfileListener',
'NodeIKernelBuddyService/addKernelBuddyListener',
];
// 使用正则表达式匹配监听器注册命令
const LISTENER_COMMAND_PATTERN = /\/addKernel\w*Listener$/;
function isListenerCommand(command: ServiceMethodCommand): boolean {
return LISTENER_COMMAND_PATTERN.test(command);
}
export function createVirtualServiceServer<T extends keyof ServiceNamingMapping>(
serviceName: T,
@@ -21,7 +20,7 @@ export function createVirtualServiceServer<T extends keyof ServiceNamingMapping>
return new Proxy(() => { }, {
get: (_target: any, functionName: string) => {
const command = `${serviceName}/${functionName}` as ServiceMethodCommand;
if (RegisterListenerCmd.includes(command as ServiceMethodCommand)) {
if (isListenerCommand(command)) {
return async (..._args: any[]) => {
const listener = new Proxy(new class { }(), {
apply: (_target, _thisArg, _arguments) => {
@@ -40,14 +39,14 @@ export function createVirtualServiceServer<T extends keyof ServiceNamingMapping>
// 问题2: 全局状态管理可能导致内存泄漏和状态污染
export const listenerCmdRegisted = new Map<ServiceMethodCommand, boolean>();
export const clientCallback = new Map<string, (command: string, ...args: any[]) => Promise<any>>();
export const clientCallback = new Map<string, (...args: any[]) => Promise<any>>();
export async function handleServiceServerOnce(
command: ServiceMethodCommand,// 服务注册命令
recvListener: (command: string, ...args: any[]) => Promise<any>,//listener监听器
ntevent: NTEventWrapper,// 事件处理器
...args: any[]//实际参数
) {
if (RegisterListenerCmd.includes(command)) {
if (isListenerCommand(command)) {
if (!listenerCmdRegisted.has(command)) {
listenerCmdRegisted.set(command, true);
return (ntevent.callNoListenerEvent as any)(command, new Proxy(new class { }(), {
@@ -61,8 +60,6 @@ export async function handleServiceServerOnce(
}
return 0;
}
console.log('handleServiceServerOnce', command, 'args', args);
console.log('params', args);
return await (ntevent.callNoListenerEvent as (command: ServiceMethodCommand, ...args: any[]) => Promise<any>)(command, ...args);
}
@@ -73,7 +70,7 @@ export function createVirtualServiceClient<T extends keyof ServiceNamingMapping>
const object = new Proxy(() => { }, {
get: (_target: any, functionName: string) => {
const command = `${serviceName}/${functionName}` as ServiceMethodCommand;
if (RegisterListenerCmd.includes(command as ServiceMethodCommand)) {
if (isListenerCommand(command)) {
if (!clientCallback.has(command)) {
return async (listener: Record<string, any>) => {
// 遍历 listener
@@ -94,12 +91,15 @@ export function createVirtualServiceClient<T extends keyof ServiceNamingMapping>
});
const receiverListener = function (command: string, ...args: any[]) {
return clientCallback.get(command)?.(command, ...args);
if (command.indexOf('onRecvMsg') !== - 1 || command.indexOf('onRecvSysMsg') !== -1) {
console.log(`Received command: ${command}, with args: ${JSON.stringify(args)}`);
}
return clientCallback.get(command)?.(...args);
};
return { receiverListener: receiverListener, object: object as ServiceNamingMapping[T] };
}
// 建议添加清理函数
export function clearServiceState() {
listenerCmdRegisted.clear();
clientCallback.clear();

View File

@@ -1,82 +0,0 @@
# QRCode Terminal Edition [![Build Status][travis-ci-img]][travis-ci-url]
> Going where no QRCode has gone before.
![Basic Example][basic-example-img]
# Node Library
## Install
Can be installed with:
$ npm install qrcode-terminal
and used:
var qrcode = require('qrcode-terminal');
## Usage
To display some data to the terminal just call:
qrcode.generate('This will be a QRCode, eh!');
You can even specify the error level (default is 'L'):
qrcode.setErrorLevel('Q');
qrcode.generate('This will be a QRCode with error level Q!');
If you don't want to display to the terminal but just want to string you can provide a callback:
qrcode.generate('http://github.com', function (qrcode) {
console.log(qrcode);
});
If you want to display small output, provide `opts` with `small`:
qrcode.generate('This will be a small QRCode, eh!', {small: true});
qrcode.generate('This will be a small QRCode, eh!', {small: true}, function (qrcode) {
console.log(qrcode)
});
# Command-Line
## Install
$ npm install -g qrcode-terminal
## Usage
$ qrcode-terminal --help
$ qrcode-terminal 'http://github.com'
$ echo 'http://github.com' | qrcode-terminal
# Support
- OS X
- Linux
- Windows
# Server-side
[node-qrcode][node-qrcode-url] is a popular server-side QRCode generator that
renders to a `canvas` object.
# Developing
To setup the development envrionment run `npm install`
To run tests run `npm test`
# Contributers
Gord Tanner <gtanner@gmail.com>
Micheal Brooks <michael@michaelbrooks.ca>
[travis-ci-img]: https://travis-ci.org/gtanner/qrcode-terminal.png
[travis-ci-url]: https://travis-ci.org/gtanner/qrcode-terminal
[basic-example-img]: https://raw.github.com/gtanner/qrcode-terminal/master/example/basic.png
[node-qrcode-url]: https://github.com/soldair/node-qrcode