mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2025-07-19 12:03:37 +00:00
Compare commits
13 Commits
v4.7.81
...
base-rpc-s
Author | SHA1 | Date | |
---|---|---|---|
![]() |
8243abaf0c | ||
![]() |
25be976fc9 | ||
![]() |
a8d8a94309 | ||
![]() |
eb9209cffb | ||
![]() |
06bc761dd3 | ||
![]() |
8994d3af14 | ||
![]() |
5eefd3dbe8 | ||
![]() |
2be014a9f2 | ||
![]() |
ee4c9e95ad | ||
![]() |
3e25172450 | ||
![]() |
ac51c50046 | ||
![]() |
a2cae1734b | ||
![]() |
88e9caddfa |
@@ -41,6 +41,7 @@
|
|||||||
"ajv": "^8.13.0",
|
"ajv": "^8.13.0",
|
||||||
"async-mutex": "^0.5.0",
|
"async-mutex": "^0.5.0",
|
||||||
"commander": "^13.0.0",
|
"commander": "^13.0.0",
|
||||||
|
"compressing": "^1.10.1",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"esbuild": "0.25.5",
|
"esbuild": "0.25.5",
|
||||||
"eslint": "^9.14.0",
|
"eslint": "^9.14.0",
|
||||||
@@ -52,18 +53,17 @@
|
|||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
"json5": "^2.2.3",
|
"json5": "^2.2.3",
|
||||||
"multer": "^2.0.1",
|
"multer": "^2.0.1",
|
||||||
|
"napcat.protobuf": "^1.1.4",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
"typescript-eslint": "^8.13.0",
|
"typescript-eslint": "^8.13.0",
|
||||||
"vite": "^6.0.1",
|
"vite": "^6.0.1",
|
||||||
"vite-plugin-cp": "^6.0.0",
|
"vite-plugin-cp": "^6.0.0",
|
||||||
"vite-tsconfig-paths": "^5.1.0",
|
"vite-tsconfig-paths": "^5.1.0",
|
||||||
"napcat.protobuf": "^1.1.4",
|
"winston": "^3.17.0"
|
||||||
"winston": "^3.17.0",
|
|
||||||
"compressing": "^1.10.1"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^5.0.0",
|
"express": "^5.0.0",
|
||||||
"silk-wasm": "^3.6.1",
|
"silk-wasm": "^3.6.1",
|
||||||
"ws": "^8.18.0"
|
"ws": "^8.18.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -10,9 +10,9 @@ interface InternalMapKey {
|
|||||||
checker: ((...args: any[]) => boolean) | undefined;
|
checker: ((...args: any[]) => boolean) | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
type EnsureFunc<T> = T extends (...args: any) => any ? T : never;
|
export type EnsureFunc<T> = T extends (...args: any) => any ? T : never;
|
||||||
|
|
||||||
type FuncKeys<T> = Extract<
|
export type FuncKeys<T> = Extract<
|
||||||
{
|
{
|
||||||
[K in keyof T]: EnsureFunc<T[K]> extends never ? never : K;
|
[K in keyof T]: EnsureFunc<T[K]> extends never ? never : K;
|
||||||
}[keyof T],
|
}[keyof T],
|
||||||
|
@@ -20,3 +20,23 @@ export function proxyHandlerOf(logger: LogWrapper) {
|
|||||||
export function proxiedListenerOf<T extends object>(listener: T, logger: LogWrapper) {
|
export function proxiedListenerOf<T extends object>(listener: T, logger: LogWrapper) {
|
||||||
return new Proxy<T>(listener, proxyHandlerOf(logger));
|
return new Proxy<T>(listener, proxyHandlerOf(logger));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function proxyHandlerOfWithoutLogger() {
|
||||||
|
return {
|
||||||
|
get(target: any, prop: any, receiver: any) {
|
||||||
|
if (typeof target[prop] === 'undefined') {
|
||||||
|
// 如果方法不存在,返回一个函数,这个函数调用existentMethod
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
return (..._args: unknown[]) => {
|
||||||
|
console.log(`${target.constructor.name} has no method ${prop}`);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// 如果方法存在,正常返回
|
||||||
|
return Reflect.get(target, prop, receiver);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function proxiedListenerOfWithoutLogger<T extends object>(listener: T) {
|
||||||
|
return new Proxy<T>(listener, proxyHandlerOfWithoutLogger());
|
||||||
|
}
|
@@ -142,7 +142,6 @@ export class NTQQMsgApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async queryFirstMsgBySender(peer: Peer, SendersUid: string[]) {
|
async queryFirstMsgBySender(peer: Peer, SendersUid: string[]) {
|
||||||
console.log(peer, SendersUid);
|
|
||||||
return await this.context.session.getMsgService().queryMsgsWithFilterEx('0', '0', '0', {
|
return await this.context.session.getMsgService().queryMsgsWithFilterEx('0', '0', '0', {
|
||||||
chatInfo: peer,
|
chatInfo: peer,
|
||||||
filterMsgType: [],
|
filterMsgType: [],
|
||||||
|
@@ -30,6 +30,10 @@ import os from 'node:os';
|
|||||||
import { NodeIKernelMsgListener, NodeIKernelProfileListener } from '@/core/listeners';
|
import { NodeIKernelMsgListener, NodeIKernelProfileListener } from '@/core/listeners';
|
||||||
import { proxiedListenerOf } from '@/common/proxy-handler';
|
import { proxiedListenerOf } from '@/common/proxy-handler';
|
||||||
import { NTQQPacketApi } from './apis/packet';
|
import { NTQQPacketApi } from './apis/packet';
|
||||||
|
import { handleServiceServerOnce, receiverServiceListener, ServiceMethodCommand } from '@/remote/service';
|
||||||
|
import { rpc_decode, rpc_encode } from '@/remote/serialize';
|
||||||
|
import { PipeClient, PipeServer } from '@/remote/pipe';
|
||||||
|
import { RemoteWrapperSession } from '@/remote/remoteSession';
|
||||||
export * from './wrapper';
|
export * from './wrapper';
|
||||||
export * from './types';
|
export * from './types';
|
||||||
export * from './services';
|
export * from './services';
|
||||||
@@ -97,9 +101,63 @@ export class NapCatCore {
|
|||||||
constructor(context: InstanceContext, selfInfo: SelfInfo) {
|
constructor(context: InstanceContext, selfInfo: SelfInfo) {
|
||||||
this.selfInfo = selfInfo;
|
this.selfInfo = selfInfo;
|
||||||
this.context = context;
|
this.context = context;
|
||||||
|
|
||||||
this.util = this.context.wrapper.NodeQQNTWrapperUtil;
|
this.util = this.context.wrapper.NodeQQNTWrapperUtil;
|
||||||
this.eventWrapper = new NTEventWrapper(context.session);
|
this.eventWrapper = new NTEventWrapper(context.session);
|
||||||
this.configLoader = new NapCatConfigLoader(this, this.context.pathWrapper.configPath,NapcatConfigSchema);
|
|
||||||
|
// 管道服务端测试
|
||||||
|
let pipe_server = new PipeServer('//./pipe/napcat');
|
||||||
|
pipe_server.registerHandler(async (packet, helper) => {
|
||||||
|
if (packet.type !== 'event_request') {
|
||||||
|
return helper.error('Invalid packet type');
|
||||||
|
}
|
||||||
|
let event_rpc_data = rpc_decode<{ params: any[] }>(JSON.parse(packet.data));
|
||||||
|
let event_rpc_trace = packet.trace;
|
||||||
|
let event_rpc_command = packet.command as ServiceMethodCommand;
|
||||||
|
let event_rpc_result = await handleServiceServerOnce(event_rpc_command,
|
||||||
|
async (listenerCommand: string, ...args: any[]) => {
|
||||||
|
let listener_data = rpc_encode<{ params: any[] }>({ params: args });
|
||||||
|
helper.sendListenerCallback(listenerCommand, JSON.stringify(rpc_encode(listener_data)));
|
||||||
|
},
|
||||||
|
this.eventWrapper,
|
||||||
|
...event_rpc_data.params
|
||||||
|
);
|
||||||
|
return helper.sendEventResponse(event_rpc_trace, JSON.stringify(rpc_encode(event_rpc_result)));
|
||||||
|
});
|
||||||
|
pipe_server.start().then(() => {
|
||||||
|
this.context.logger.log('Pipe server started successfully');
|
||||||
|
let pipe_client = new PipeClient('//./pipe/napcat');
|
||||||
|
let trace_callback_map = new Map<string, (trace: string, data: any) => void>();
|
||||||
|
pipe_client.registerHandler(async (packet, _helper) => {
|
||||||
|
if (packet.type == 'event_response') {
|
||||||
|
let event_rpc_data = rpc_decode<Array<any>>(JSON.parse(packet.data));
|
||||||
|
trace_callback_map.get(packet.trace)?.(packet.trace, event_rpc_data);
|
||||||
|
} else if (packet.type == 'listener_callback') {
|
||||||
|
let event_rpc_data = rpc_decode<Array<any>>(JSON.parse(packet.data));
|
||||||
|
await receiverServiceListener(packet.command, ...event_rpc_data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.context.session = new RemoteWrapperSession(async (_serviceClient, serviceCommand, ...args) => {
|
||||||
|
let trace = crypto.randomUUID();
|
||||||
|
return await new Promise((resolve, _reject) => {
|
||||||
|
trace_callback_map.set(trace, (_trace, data) => {
|
||||||
|
//console.log('Received response for trace:', _trace, 'with data:', data);
|
||||||
|
resolve(data);
|
||||||
|
});
|
||||||
|
pipe_client.sendRequest(serviceCommand, JSON.stringify(rpc_encode({ params: args })), trace);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
pipe_client.connect().then(() => {
|
||||||
|
this.context.logger.log('Pipe client connected successfully');
|
||||||
|
}).catch((e) => {
|
||||||
|
this.context.logger.logError('Pipe client connection failed: ' + e.message);
|
||||||
|
});
|
||||||
|
}).catch((e) => {
|
||||||
|
this.context.logger.logError('Pipe server start failed: ' + e.message);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
this.configLoader = new NapCatConfigLoader(this, this.context.pathWrapper.configPath, NapcatConfigSchema);
|
||||||
this.apis = {
|
this.apis = {
|
||||||
FileApi: new NTQQFileApi(this.context, this),
|
FileApi: new NTQQFileApi(this.context, this),
|
||||||
SystemApi: new NTQQSystemApi(this.context, this),
|
SystemApi: new NTQQSystemApi(this.context, this),
|
||||||
@@ -251,13 +309,13 @@ export async function genSessionConfig(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface InstanceContext {
|
export interface InstanceContext {
|
||||||
readonly workingEnv: NapCatCoreWorkingEnv;
|
session: NodeIQQNTWrapperSession;
|
||||||
readonly wrapper: WrapperNodeApi;
|
workingEnv: NapCatCoreWorkingEnv;
|
||||||
readonly session: NodeIQQNTWrapperSession;
|
wrapper: WrapperNodeApi;
|
||||||
readonly logger: LogWrapper;
|
logger: LogWrapper;
|
||||||
readonly loginService: NodeIKernelLoginService;
|
loginService: NodeIKernelLoginService;
|
||||||
readonly basicInfoWrapper: QQBasicInfoWrapper;
|
basicInfoWrapper: QQBasicInfoWrapper;
|
||||||
readonly pathWrapper: NapCatPathWrapper;
|
pathWrapper: NapCatPathWrapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StableNTApiWrapper {
|
export interface StableNTApiWrapper {
|
||||||
|
@@ -40,7 +40,6 @@ export class NodeIKernelBuddyListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onDelBatchBuddyInfos(_arg: unknown): any {
|
onDelBatchBuddyInfos(_arg: unknown): any {
|
||||||
console.log('onDelBatchBuddyInfos not implemented', ...arguments);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onDoubtBuddyReqChange(_arg:
|
onDoubtBuddyReqChange(_arg:
|
||||||
|
@@ -1,71 +1,71 @@
|
|||||||
import { User, UserDetailInfoListenerArg } from '@/core/types';
|
import { SelfStatusInfo, User, UserDetailInfoListenerArg } from '@/core/types';
|
||||||
|
|
||||||
export class NodeIKernelProfileListener {
|
export class NodeIKernelProfileListener {
|
||||||
onUserDetailInfoChanged(arg: UserDetailInfoListenerArg): void {
|
onUserDetailInfoChanged(_arg: UserDetailInfoListenerArg): void {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onProfileSimpleChanged(...args: unknown[]): any {
|
onProfileSimpleChanged(..._args: unknown[]): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onProfileDetailInfoChanged(profile: User): any {
|
onProfileDetailInfoChanged(_profile: User): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onStatusUpdate(...args: unknown[]): any {
|
onStatusUpdate(..._args: unknown[]): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onSelfStatusChanged(...args: unknown[]): any {
|
onSelfStatusChanged(_info: SelfStatusInfo): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onStrangerRemarkChanged(...args: unknown[]): any {
|
onStrangerRemarkChanged(..._args: unknown[]): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMemberListChange(...args: unknown[]): any {
|
onMemberListChange(..._args: unknown[]): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMemberInfoChange(...args: unknown[]): any {
|
onMemberInfoChange(..._args: unknown[]): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onGroupListUpdate(...args: unknown[]): any {
|
onGroupListUpdate(..._args: unknown[]): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onGroupAllInfoChange(...args: unknown[]): any {
|
onGroupAllInfoChange(..._args: unknown[]): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onGroupDetailInfoChange(...args: unknown[]): any {
|
onGroupDetailInfoChange(..._args: unknown[]): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onGroupConfMemberChange(...args: unknown[]): any {
|
onGroupConfMemberChange(..._args: unknown[]): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onGroupExtListUpdate(...args: unknown[]): any {
|
onGroupExtListUpdate(..._args: unknown[]): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onGroupNotifiesUpdated(...args: unknown[]): any {
|
onGroupNotifiesUpdated(..._args: unknown[]): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onGroupNotifiesUnreadCountUpdated(...args: unknown[]): any {
|
onGroupNotifiesUnreadCountUpdated(..._args: unknown[]): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onGroupMemberLevelInfoChange(...args: unknown[]): any {
|
onGroupMemberLevelInfoChange(..._args: unknown[]): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onGroupBulletinChange(...args: unknown[]): any {
|
onGroupBulletinChange(..._args: unknown[]): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -16,6 +16,16 @@ export * from './NodeIKernelDbToolsService';
|
|||||||
export * from './NodeIKernelTipOffService';
|
export * from './NodeIKernelTipOffService';
|
||||||
export * from './NodeIKernelSearchService';
|
export * from './NodeIKernelSearchService';
|
||||||
export * from './NodeIKernelCollectionService';
|
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 {
|
import type {
|
||||||
NodeIKernelAvatarService,
|
NodeIKernelAvatarService,
|
||||||
@@ -36,8 +46,19 @@ import type {
|
|||||||
NodeIKernelTicketService,
|
NodeIKernelTicketService,
|
||||||
NodeIKernelTipOffService,
|
NodeIKernelTipOffService,
|
||||||
} from '.';
|
} 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 = {
|
export type ServiceNamingMapping = {
|
||||||
|
NodeIKernelAlbumService: NodeIKernelAlbumService;
|
||||||
NodeIKernelAvatarService: NodeIKernelAvatarService;
|
NodeIKernelAvatarService: NodeIKernelAvatarService;
|
||||||
NodeIKernelBuddyService: NodeIKernelBuddyService;
|
NodeIKernelBuddyService: NodeIKernelBuddyService;
|
||||||
NodeIKernelFileAssistantService: NodeIKernelFileAssistantService;
|
NodeIKernelFileAssistantService: NodeIKernelFileAssistantService;
|
||||||
@@ -53,6 +74,15 @@ export type ServiceNamingMapping = {
|
|||||||
NodeIKernelRichMediaService: NodeIKernelRichMediaService;
|
NodeIKernelRichMediaService: NodeIKernelRichMediaService;
|
||||||
NodeIKernelDbToolsService: NodeIKernelDbToolsService;
|
NodeIKernelDbToolsService: NodeIKernelDbToolsService;
|
||||||
NodeIKernelTipOffService: NodeIKernelTipOffService;
|
NodeIKernelTipOffService: NodeIKernelTipOffService;
|
||||||
NodeIKernelSearchService: NodeIKernelSearchService,
|
NodeIKernelSearchService: NodeIKernelSearchService;
|
||||||
NodeIKernelCollectionService: NodeIKernelCollectionService;
|
NodeIKernelCollectionService: NodeIKernelCollectionService;
|
||||||
|
NodeIKernelECDHService: NodeIKernelECDHService;
|
||||||
|
NodeIKernelNodeMiscService: NodeIKernelNodeMiscService;
|
||||||
|
NodeIKernelMsgBackupService: NodeIKernelMsgBackupService;
|
||||||
|
NodeIKernelTianShuService: NodeIKernelTianShuService;
|
||||||
|
NodeIKernelUnitedConfigService: NodeIKernelUnitedConfigService;
|
||||||
|
NodeIkernelTestPerformanceService: NodeIkernelTestPerformanceService;
|
||||||
|
NodeIKernelUixConvertService: NodeIKernelUixConvertService;
|
||||||
|
NodeIKernelMSFService: NodeIKernelMSFService;
|
||||||
|
NodeIKernelRecentContactService: NodeIKernelRecentContactService;
|
||||||
};
|
};
|
||||||
|
@@ -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
|
|
||||||
|
|
536
src/remote/pipe.ts
Normal file
536
src/remote/pipe.ts
Normal file
@@ -0,0 +1,536 @@
|
|||||||
|
import * as net from 'net';
|
||||||
|
import { randomUUID } from 'crypto';
|
||||||
|
import { EventEmitter } from 'events';
|
||||||
|
|
||||||
|
export interface Packet<T = any> {
|
||||||
|
command: string;
|
||||||
|
trace: string;
|
||||||
|
data: T;
|
||||||
|
type: 'listener_callback' | 'event_response' | 'event_request' | 'default';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 协议常量
|
||||||
|
const PROTOCOL_MAGIC = 0x4E415043; // 'NAPC'
|
||||||
|
const PROTOCOL_VERSION = 0x01;
|
||||||
|
const HEADER_SIZE = 12;
|
||||||
|
const MAX_PACKET_SIZE = 16 * 1024 * 1024; // 降低到16MB
|
||||||
|
const BUFFER_HIGH_WATER_MARK = 2 * 1024 * 1024; // 2MB背压阈值
|
||||||
|
const BUFFER_LOW_WATER_MARK = 512 * 1024; // 512KB恢复阈值
|
||||||
|
|
||||||
|
// 高效缓冲区管理器
|
||||||
|
class BufferManager {
|
||||||
|
private buffers: Buffer[] = [];
|
||||||
|
private totalSize: number = 0;
|
||||||
|
private readOffset: number = 0;
|
||||||
|
private isHighWaterMark: boolean = false;
|
||||||
|
|
||||||
|
// 添加数据
|
||||||
|
append(data: Buffer): void {
|
||||||
|
this.buffers.push(data);
|
||||||
|
this.totalSize += data.length;
|
||||||
|
|
||||||
|
// 检查背压
|
||||||
|
if (!this.isHighWaterMark && this.totalSize > BUFFER_HIGH_WATER_MARK) {
|
||||||
|
this.isHighWaterMark = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 消费数据
|
||||||
|
consume(length: number): Buffer {
|
||||||
|
if (length > this.available) {
|
||||||
|
throw new Error('消费长度超过可用数据');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = Buffer.allocUnsafe(length);
|
||||||
|
let resultOffset = 0;
|
||||||
|
let remaining = length;
|
||||||
|
|
||||||
|
while (remaining > 0 && this.buffers.length > 0) {
|
||||||
|
const currentBuffer = this.buffers[0];
|
||||||
|
if (!currentBuffer?.[0]) continue;
|
||||||
|
const availableInCurrent = currentBuffer.length - this.readOffset;
|
||||||
|
const toCopy = Math.min(remaining, availableInCurrent);
|
||||||
|
|
||||||
|
currentBuffer.copy(result, resultOffset, this.readOffset, this.readOffset + toCopy);
|
||||||
|
resultOffset += toCopy;
|
||||||
|
remaining -= toCopy;
|
||||||
|
this.readOffset += toCopy;
|
||||||
|
|
||||||
|
// 如果当前buffer用完了,移除它
|
||||||
|
if (this.readOffset >= currentBuffer.length) {
|
||||||
|
this.buffers.shift();
|
||||||
|
this.readOffset = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.totalSize -= length;
|
||||||
|
|
||||||
|
// 检查是否可以恢复读取
|
||||||
|
if (this.isHighWaterMark && this.totalSize < BUFFER_LOW_WATER_MARK) {
|
||||||
|
this.isHighWaterMark = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 预览数据(不消费)
|
||||||
|
peek(length: number): Buffer | null {
|
||||||
|
if (length > this.available) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = Buffer.allocUnsafe(length);
|
||||||
|
let resultOffset = 0;
|
||||||
|
let remaining = length;
|
||||||
|
let bufferIndex = 0;
|
||||||
|
let currentReadOffset = this.readOffset;
|
||||||
|
|
||||||
|
while (remaining > 0 && bufferIndex < this.buffers.length) {
|
||||||
|
const currentBuffer = this.buffers[bufferIndex];
|
||||||
|
if (!currentBuffer) continue;
|
||||||
|
const availableInCurrent = currentBuffer.length - currentReadOffset;
|
||||||
|
const toCopy = Math.min(remaining, availableInCurrent);
|
||||||
|
|
||||||
|
currentBuffer.copy(result, resultOffset, currentReadOffset, currentReadOffset + toCopy);
|
||||||
|
resultOffset += toCopy;
|
||||||
|
remaining -= toCopy;
|
||||||
|
|
||||||
|
if (currentReadOffset + toCopy >= currentBuffer.length) {
|
||||||
|
bufferIndex++;
|
||||||
|
currentReadOffset = 0;
|
||||||
|
} else {
|
||||||
|
currentReadOffset += toCopy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
get available(): number {
|
||||||
|
return this.totalSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
get shouldPause(): boolean {
|
||||||
|
return this.isHighWaterMark;
|
||||||
|
}
|
||||||
|
|
||||||
|
reset(): void {
|
||||||
|
this.buffers = [];
|
||||||
|
this.totalSize = 0;
|
||||||
|
this.readOffset = 0;
|
||||||
|
this.isHighWaterMark = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 简化的数据包管理器
|
||||||
|
class PacketManager {
|
||||||
|
static pack(packet: Packet): Buffer {
|
||||||
|
const jsonStr = JSON.stringify(packet);
|
||||||
|
const jsonBuffer = Buffer.from(jsonStr, 'utf8');
|
||||||
|
|
||||||
|
if (jsonBuffer.length > MAX_PACKET_SIZE - HEADER_SIZE) {
|
||||||
|
throw new Error(`数据包过大: ${jsonBuffer.length}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const buffer = Buffer.allocUnsafe(HEADER_SIZE + jsonBuffer.length);
|
||||||
|
|
||||||
|
buffer.writeUInt32BE(PROTOCOL_MAGIC, 0);
|
||||||
|
buffer.writeUInt32BE(jsonBuffer.length, 4);
|
||||||
|
buffer.writeUInt32BE(PROTOCOL_VERSION, 8);
|
||||||
|
jsonBuffer.copy(buffer, HEADER_SIZE);
|
||||||
|
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
static unpack(bufferManager: BufferManager): Packet[] {
|
||||||
|
const packets: Packet[] = [];
|
||||||
|
|
||||||
|
while (bufferManager.available >= HEADER_SIZE) {
|
||||||
|
// 检查魔数
|
||||||
|
const header = bufferManager.peek(HEADER_SIZE);
|
||||||
|
if (!header) break;
|
||||||
|
|
||||||
|
const magic = header.readUInt32BE(0);
|
||||||
|
if (magic !== PROTOCOL_MAGIC) {
|
||||||
|
// 简单的同步恢复:跳过一个字节
|
||||||
|
bufferManager.consume(1);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataLength = header.readUInt32BE(4);
|
||||||
|
//const version = header.readUInt32BE(8);
|
||||||
|
|
||||||
|
// 基本验证
|
||||||
|
if (dataLength <= 0 || dataLength > MAX_PACKET_SIZE - HEADER_SIZE) {
|
||||||
|
bufferManager.consume(1);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查完整包
|
||||||
|
const totalSize = HEADER_SIZE + dataLength;
|
||||||
|
if (bufferManager.available < totalSize) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 消费完整包
|
||||||
|
bufferManager.consume(HEADER_SIZE);
|
||||||
|
const jsonBuffer = bufferManager.consume(dataLength);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const packet = JSON.parse(jsonBuffer.toString('utf8')) as Packet;
|
||||||
|
if (this.isValidPacket(packet)) {
|
||||||
|
packets.push(packet);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('JSON解析失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return packets;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static isValidPacket(packet: any): packet is Packet {
|
||||||
|
return packet &&
|
||||||
|
typeof packet.command === 'string' &&
|
||||||
|
typeof packet.trace === 'string' &&
|
||||||
|
packet.data !== undefined &&
|
||||||
|
['listener_callback', 'event_response', 'event_request', 'default'].includes(packet.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
static createRequest<T = any>(command: string, data: T, trace?: string): Packet<T> {
|
||||||
|
return {
|
||||||
|
command,
|
||||||
|
trace: trace || randomUUID(),
|
||||||
|
data,
|
||||||
|
type: 'event_request'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static createResponse<T = any>(trace: string, data: T, command = ''): Packet<T> {
|
||||||
|
return {
|
||||||
|
command,
|
||||||
|
trace,
|
||||||
|
data,
|
||||||
|
type: 'event_response'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static createCallback<T = any>(command: string, data: T, trace?: string): Packet<T> {
|
||||||
|
return {
|
||||||
|
command,
|
||||||
|
trace: trace || randomUUID(),
|
||||||
|
data,
|
||||||
|
type: 'listener_callback'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应助手类
|
||||||
|
class ResponseHelper {
|
||||||
|
private responseSent = false;
|
||||||
|
|
||||||
|
constructor(private socket: net.Socket, private trace: string, private command: string = '') { }
|
||||||
|
|
||||||
|
success<T = any>(data: T): void {
|
||||||
|
if (this.responseSent) return;
|
||||||
|
|
||||||
|
const response = PacketManager.createResponse(this.trace, data, this.command);
|
||||||
|
this.writePacket(response);
|
||||||
|
this.responseSent = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
error(message: string, code = 500): void {
|
||||||
|
if (this.responseSent) return;
|
||||||
|
|
||||||
|
const response = PacketManager.createResponse(this.trace, { error: message, code }, this.command);
|
||||||
|
this.writePacket(response);
|
||||||
|
this.responseSent = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendEventResponse<T = any>(trace: string, data: T): void {
|
||||||
|
const response = PacketManager.createResponse(trace, data, this.command);
|
||||||
|
this.writePacket(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
sendListenerCallback<T = any>(command: string, data: T): void {
|
||||||
|
const callback = PacketManager.createCallback(command, data);
|
||||||
|
this.writePacket(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
private writePacket(packet: Packet): void {
|
||||||
|
console.log(`发送数据包: ${packet.command}, trace: ${packet.trace} (${packet.type}) `);
|
||||||
|
if (!this.socket.destroyed) {
|
||||||
|
const buffer = PacketManager.pack(packet);
|
||||||
|
this.socket.write(buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get hasResponseSent(): boolean {
|
||||||
|
return this.responseSent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 带背压控制的Socket包装器
|
||||||
|
class ManagedSocket {
|
||||||
|
private bufferManager = new BufferManager();
|
||||||
|
private isPaused = false;
|
||||||
|
|
||||||
|
constructor(private socket: net.Socket, private onPacket: (packet: Packet) => void) {
|
||||||
|
this.setupSocket();
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupSocket(): void {
|
||||||
|
this.socket.on('data', (chunk) => {
|
||||||
|
this.bufferManager.append(chunk);
|
||||||
|
|
||||||
|
// 背压控制
|
||||||
|
if (this.bufferManager.shouldPause && !this.isPaused) {
|
||||||
|
this.socket.pause();
|
||||||
|
this.isPaused = true;
|
||||||
|
console.warn('Socket暂停读取 - 缓冲区过大');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.processPackets();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket.on('drain', () => {
|
||||||
|
// 当socket的写缓冲区有空间时,检查是否可以恢复读取
|
||||||
|
if (this.isPaused && !this.bufferManager.shouldPause) {
|
||||||
|
this.socket.resume();
|
||||||
|
this.isPaused = false;
|
||||||
|
console.log('Socket恢复读取');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private processPackets(): void {
|
||||||
|
try {
|
||||||
|
const packets = PacketManager.unpack(this.bufferManager);
|
||||||
|
packets.forEach(packet => this.onPacket(packet));
|
||||||
|
|
||||||
|
// 处理完包后检查是否可以恢复读取
|
||||||
|
if (this.isPaused && !this.bufferManager.shouldPause) {
|
||||||
|
this.socket.resume();
|
||||||
|
this.isPaused = false;
|
||||||
|
console.log('Socket恢复读取');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('处理数据包失败:', error);
|
||||||
|
this.bufferManager.reset();
|
||||||
|
if (this.isPaused) {
|
||||||
|
this.socket.resume();
|
||||||
|
this.isPaused = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
write(buffer: Buffer): boolean {
|
||||||
|
return this.socket.write(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(): void {
|
||||||
|
this.socket.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
get destroyed(): boolean {
|
||||||
|
return this.socket.destroyed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type PacketHandler = (packet: Packet, helper: ResponseHelper) => Promise<any> | any;
|
||||||
|
|
||||||
|
// 简化的管道服务端
|
||||||
|
class PipeServer extends EventEmitter {
|
||||||
|
private server: net.Server;
|
||||||
|
private clients: Map<net.Socket, ManagedSocket> = new Map();
|
||||||
|
private handler: PacketHandler | null = null;
|
||||||
|
|
||||||
|
constructor(private pipeName: string) {
|
||||||
|
super();
|
||||||
|
this.server = net.createServer();
|
||||||
|
this.setupServer();
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupServer(): void {
|
||||||
|
this.server.on('connection', (socket) => {
|
||||||
|
console.log('客户端连接');
|
||||||
|
|
||||||
|
const managedSocket = new ManagedSocket(socket, (packet) => {
|
||||||
|
this.handlePacket(packet, socket);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.clients.set(socket, managedSocket);
|
||||||
|
|
||||||
|
socket.on('close', () => {
|
||||||
|
console.log('客户端断开');
|
||||||
|
this.clients.delete(socket);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('error', (error) => {
|
||||||
|
console.error('Socket错误:', error);
|
||||||
|
this.clients.delete(socket);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
registerHandler(handler: PacketHandler): void {
|
||||||
|
this.handler = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handlePacket(packet: Packet, socket: net.Socket): Promise<void> {
|
||||||
|
if (packet.type === 'event_response' || packet.type === 'listener_callback') {
|
||||||
|
this.emit(packet.type, packet);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const helper = new ResponseHelper(socket, packet.trace, packet.command);
|
||||||
|
|
||||||
|
if (!this.handler) {
|
||||||
|
helper.error('未注册处理器');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await this.handler(packet, helper);
|
||||||
|
if (result !== undefined && !helper.hasResponseSent) {
|
||||||
|
helper.success(result);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (!helper.hasResponseSent) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
helper.error(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async start(): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.server.listen(this.pipeName, () => {
|
||||||
|
console.log(`管道服务器启动: ${this.pipeName}`);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
this.server.on('error', reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async stop(): Promise<void> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.clients.forEach((managedSocket) => managedSocket.destroy());
|
||||||
|
this.clients.clear();
|
||||||
|
this.server.close(() => {
|
||||||
|
console.log('管道服务器停止');
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
broadcast<T = any>(command: string, data: T, type: Packet['type'] = 'default'): void {
|
||||||
|
const packet: Packet<T> = {
|
||||||
|
command,
|
||||||
|
trace: randomUUID(),
|
||||||
|
data,
|
||||||
|
type
|
||||||
|
};
|
||||||
|
const buffer = PacketManager.pack(packet);
|
||||||
|
|
||||||
|
this.clients.forEach((managedSocket) => {
|
||||||
|
if (!managedSocket.destroyed) {
|
||||||
|
managedSocket.write(buffer);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get clientCount(): number {
|
||||||
|
return this.clients.size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 简化的管道客户端
|
||||||
|
class PipeClient extends EventEmitter {
|
||||||
|
private socket: net.Socket | null = null;
|
||||||
|
private managedSocket: ManagedSocket | null = null;
|
||||||
|
private isConnected = false;
|
||||||
|
private handler: PacketHandler | null = null;
|
||||||
|
|
||||||
|
constructor(private pipeName: string) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
registerHandler(handler: PacketHandler): void {
|
||||||
|
this.handler = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
async connect(): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.socket = net.createConnection(this.pipeName);
|
||||||
|
|
||||||
|
this.managedSocket = new ManagedSocket(this.socket, (packet) => {
|
||||||
|
this.handlePacket(packet);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket.on('connect', () => {
|
||||||
|
console.log('连接到管道服务器');
|
||||||
|
this.isConnected = true;
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket.on('close', () => {
|
||||||
|
console.log('与服务器断开连接');
|
||||||
|
this.isConnected = false;
|
||||||
|
this.emit('disconnect');
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket.on('error', (error) => {
|
||||||
|
console.error('Socket错误:', error);
|
||||||
|
this.isConnected = false;
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handlePacket(packet: Packet): Promise<void> {
|
||||||
|
if (this.handler && this.socket) {
|
||||||
|
const helper = new ResponseHelper(this.socket, packet.trace, packet.command);
|
||||||
|
try {
|
||||||
|
await this.handler(packet, helper);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('处理数据包失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sendRequest<T = any>(command: string, data: T, trace?: string): void {
|
||||||
|
if (!this.isConnected || !this.managedSocket) {
|
||||||
|
throw new Error('未连接到服务器');
|
||||||
|
}
|
||||||
|
|
||||||
|
const packet = PacketManager.createRequest(command, data, trace);
|
||||||
|
const buffer = PacketManager.pack(packet);
|
||||||
|
this.managedSocket.write(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
sendResponse<T = any>(trace: string, data: T, command = ''): void {
|
||||||
|
if (!this.isConnected || !this.managedSocket) {
|
||||||
|
throw new Error('未连接到服务器');
|
||||||
|
}
|
||||||
|
|
||||||
|
const packet = PacketManager.createResponse(trace, data, command);
|
||||||
|
const buffer = PacketManager.pack(packet);
|
||||||
|
this.managedSocket.write(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect(): void {
|
||||||
|
if (this.managedSocket) {
|
||||||
|
this.managedSocket.destroy();
|
||||||
|
this.managedSocket = null;
|
||||||
|
}
|
||||||
|
this.socket = null;
|
||||||
|
this.isConnected = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
get connected(): boolean {
|
||||||
|
return this.isConnected;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { PipeServer, PipeClient, PacketManager, ResponseHelper, BufferManager };
|
109
src/remote/remoteSession.ts
Normal file
109
src/remote/remoteSession.ts
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import { createRemoteServiceClient } from "@/remote/service";
|
||||||
|
import {
|
||||||
|
NodeIQQNTWrapperSession,
|
||||||
|
WrapperSessionInitConfig
|
||||||
|
} from "../core/wrapper";
|
||||||
|
import { NodeIKernelSessionListener } from "../core/listeners/NodeIKernelSessionListener";
|
||||||
|
import {
|
||||||
|
NodeIDependsAdapter,
|
||||||
|
NodeIDispatcherAdapter
|
||||||
|
} from "../core/adapters";
|
||||||
|
import { ServiceNamingMapping } from "@/core";
|
||||||
|
|
||||||
|
class RemoteServiceManager {
|
||||||
|
private services: Map<string, any> = new Map();
|
||||||
|
private handler;
|
||||||
|
|
||||||
|
constructor(handler: (client: any, listenerCommand: string, ...args: any[]) => Promise<any>) {
|
||||||
|
this.handler = handler;
|
||||||
|
}
|
||||||
|
private createRemoteService<T extends keyof ServiceNamingMapping>(
|
||||||
|
serviceName: T
|
||||||
|
): ServiceNamingMapping[T] {
|
||||||
|
if (this.services.has(serviceName)) {
|
||||||
|
return this.services.get(serviceName);
|
||||||
|
}
|
||||||
|
|
||||||
|
let serviceClient: any;
|
||||||
|
serviceClient = createRemoteServiceClient(serviceName, async (serviceCommand, ...args) => {
|
||||||
|
return await this.handler(serviceClient, serviceCommand, ...args);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.services.set(serviceName, serviceClient.object);
|
||||||
|
return serviceClient.object;
|
||||||
|
}
|
||||||
|
|
||||||
|
getService<T extends keyof ServiceNamingMapping>(
|
||||||
|
serviceName: T
|
||||||
|
): ServiceNamingMapping[T] {
|
||||||
|
return this.createRemoteService(serviceName);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
export class RemoteWrapperSession implements NodeIQQNTWrapperSession {
|
||||||
|
private serviceManager: RemoteServiceManager;
|
||||||
|
|
||||||
|
constructor(handler: (client: { object: keyof ServiceNamingMapping, receiverListener: (command: string, ...args: any[]) => void }, listenerCommand: string, ...args: any[]) => Promise<void>) {
|
||||||
|
this.serviceManager = new RemoteServiceManager(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
create(): RemoteWrapperSession {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
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; }
|
||||||
|
}
|
1210
src/remote/serialize.cpp
Normal file
1210
src/remote/serialize.cpp
Normal file
File diff suppressed because it is too large
Load Diff
131
src/remote/serialize.ts
Normal file
131
src/remote/serialize.ts
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
interface EncodedValue {
|
||||||
|
$type: string;
|
||||||
|
$value?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EncodedNull {
|
||||||
|
$type: "null";
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EncodedUndefined {
|
||||||
|
$type: "undefined";
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EncodedPrimitive {
|
||||||
|
$type: "number" | "string" | "boolean";
|
||||||
|
$value: number | string | boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EncodedBuffer {
|
||||||
|
$type: "Buffer";
|
||||||
|
$value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EncodedMap {
|
||||||
|
$type: "Map";
|
||||||
|
$value: [EncodedValue, EncodedValue][];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EncodedArray {
|
||||||
|
$type: "Array";
|
||||||
|
$value: EncodedValue[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EncodedObject {
|
||||||
|
$type: "Object";
|
||||||
|
$value: { [key: string]: EncodedValue };
|
||||||
|
}
|
||||||
|
|
||||||
|
type SerializedValue = EncodedNull | EncodedUndefined | EncodedPrimitive | EncodedBuffer | EncodedMap | EncodedArray | EncodedObject;
|
||||||
|
|
||||||
|
function rpc_encode<T>(value: T): SerializedValue {
|
||||||
|
if (value === null) return { $type: "null" };
|
||||||
|
if (value === undefined) return { $type: "undefined" };
|
||||||
|
|
||||||
|
if (typeof value === "number") return { $type: "number", $value: value };
|
||||||
|
if (typeof value === "string") return { $type: "string", $value: value };
|
||||||
|
if (typeof value === "boolean") return { $type: "boolean", $value: value };
|
||||||
|
|
||||||
|
if (Buffer.isBuffer(value) || value instanceof Uint8Array) {
|
||||||
|
// Buffer和Uint8Array都转成base64字符串
|
||||||
|
let base64: string = Buffer.from(value).toString("base64");
|
||||||
|
return { $type: "Buffer", $value: base64 };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value instanceof Map) {
|
||||||
|
let arr: [SerializedValue, SerializedValue][] = [];
|
||||||
|
for (let [k, v] of value.entries()) {
|
||||||
|
arr.push([rpc_encode(k), rpc_encode(v)]);
|
||||||
|
}
|
||||||
|
return { $type: "Map", $value: arr };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value) || (typeof value === "object" && value !== null && typeof (value as unknown as ArrayLike<unknown>).length === "number")) {
|
||||||
|
// ArrayLike也认为是Array
|
||||||
|
let arr: SerializedValue[] = [];
|
||||||
|
const arrayLike = value as unknown as ArrayLike<unknown>;
|
||||||
|
for (let i = 0; i < arrayLike.length; i++) {
|
||||||
|
arr.push(rpc_encode(arrayLike[i]));
|
||||||
|
}
|
||||||
|
return { $type: "Array", $value: arr };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === "object" && value !== null) {
|
||||||
|
let obj: { [key: string]: SerializedValue } = {};
|
||||||
|
for (let k in value) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(value, k)) {
|
||||||
|
obj[k] = rpc_encode((value as Record<string, unknown>)[k]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { $type: "Object", $value: obj };
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Unsupported type");
|
||||||
|
}
|
||||||
|
|
||||||
|
function rpc_decode<T = unknown>(obj: EncodedValue): T {
|
||||||
|
if (obj == null || typeof obj !== "object" || !("$type" in obj)) {
|
||||||
|
throw new Error("Invalid encoded object");
|
||||||
|
}
|
||||||
|
switch (obj.$type) {
|
||||||
|
case "null": return null as T;
|
||||||
|
case "undefined": return undefined as T;
|
||||||
|
case "number": return (obj as EncodedPrimitive).$value as T;
|
||||||
|
case "string": return (obj as EncodedPrimitive).$value as T;
|
||||||
|
case "boolean": return (obj as EncodedPrimitive).$value as T;
|
||||||
|
case "Buffer":
|
||||||
|
return Buffer.from((obj as EncodedBuffer).$value, "base64") as T;
|
||||||
|
case "Map":
|
||||||
|
{
|
||||||
|
let map = new Map();
|
||||||
|
for (let [k, v] of (obj as EncodedMap).$value) {
|
||||||
|
map.set(rpc_decode(k), rpc_decode(v));
|
||||||
|
}
|
||||||
|
return map as T;
|
||||||
|
}
|
||||||
|
case "Array":
|
||||||
|
{
|
||||||
|
let arr: unknown[] = [];
|
||||||
|
for (let item of (obj as EncodedArray).$value) {
|
||||||
|
arr.push(rpc_decode(item));
|
||||||
|
}
|
||||||
|
return arr as T;
|
||||||
|
}
|
||||||
|
case "Object":
|
||||||
|
{
|
||||||
|
let out: Record<string, unknown> = {};
|
||||||
|
for (let k in (obj as EncodedObject).$value) {
|
||||||
|
const value = (obj as EncodedObject).$value[k];
|
||||||
|
if (value !== undefined) {
|
||||||
|
out[k] = rpc_decode(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out as T;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new Error("Unknown $type: " + obj.$type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { rpc_encode, rpc_decode };
|
||||||
|
export type { SerializedValue };
|
114
src/remote/service.ts
Normal file
114
src/remote/service.ts
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import { FuncKeys, NTEventWrapper } from "@/common/event";
|
||||||
|
import { ServiceNamingMapping } from "@/core";
|
||||||
|
|
||||||
|
export type ServiceMethodCommand = {
|
||||||
|
[Service in keyof ServiceNamingMapping]: `${Service}/${FuncKeys<ServiceNamingMapping[Service]>}`
|
||||||
|
}[keyof ServiceNamingMapping];
|
||||||
|
|
||||||
|
const LISTENER_COMMAND_PATTERN = /\/addKernel\w*Listener$/;
|
||||||
|
|
||||||
|
function isListenerCommand(command: ServiceMethodCommand): boolean {
|
||||||
|
return LISTENER_COMMAND_PATTERN.test(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createRemoteServiceServer<T extends keyof ServiceNamingMapping>(
|
||||||
|
serviceName: T,
|
||||||
|
ntevent: NTEventWrapper,
|
||||||
|
callback: (command: ServiceMethodCommand, ...args: any[]) => Promise<any>
|
||||||
|
): ServiceNamingMapping[T] {
|
||||||
|
return new Proxy(() => { }, {
|
||||||
|
get: (_target: any, functionName: string) => {
|
||||||
|
const command = `${serviceName}/${functionName}` as ServiceMethodCommand;
|
||||||
|
if (isListenerCommand(command)) {
|
||||||
|
return async (..._args: any[]) => {
|
||||||
|
const listener = new Proxy(new class { }(), {
|
||||||
|
apply: (_target, _thisArg, _arguments) => {
|
||||||
|
return callback(command, ..._arguments);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return await (ntevent.callNoListenerEvent as any)(command, listener);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return async (...args: any[]) => {
|
||||||
|
return await (ntevent.callNoListenerEvent as any)(command, ...args);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 避免重复远程注册 多份传输会消耗很大
|
||||||
|
export const listenerCmdRegisted = new Map<ServiceMethodCommand, boolean>();
|
||||||
|
// 已经注册的Listener实例托管
|
||||||
|
export const clientCallback = new Map<string, Array<(...args: any[]) => Promise<any>>>();
|
||||||
|
|
||||||
|
export async function handleServiceServerOnce(
|
||||||
|
command: ServiceMethodCommand,// 服务注册命令
|
||||||
|
recvListener: (command: string, ...args: any[]) => Promise<any>,//listener监听器
|
||||||
|
ntevent: NTEventWrapper,// 事件处理器
|
||||||
|
...args: any[]//实际参数
|
||||||
|
) {
|
||||||
|
if (isListenerCommand(command)) {
|
||||||
|
if (!listenerCmdRegisted.has(command)) {
|
||||||
|
listenerCmdRegisted.set(command, true);
|
||||||
|
return (ntevent.callNoListenerEvent as any)(command, new Proxy(new class { }(), {
|
||||||
|
get: (_target: any, prop: string) => {
|
||||||
|
return async (..._args: any[]) => {
|
||||||
|
let listenerCmd = `${command.split('/')[0]}/${prop}`;
|
||||||
|
recvListener(listenerCmd, ..._args);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return await (ntevent.callNoListenerEvent as (command: ServiceMethodCommand, ...args: any[]) => Promise<any>)(command, ...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createRemoteServiceClient<T extends keyof ServiceNamingMapping>(
|
||||||
|
serviceName: T,
|
||||||
|
receiverEvent: (command: ServiceMethodCommand, ...args: any[]) => Promise<any>
|
||||||
|
) {
|
||||||
|
const object = new Proxy(() => { }, {
|
||||||
|
get: (_target: any, functionName: string) => {
|
||||||
|
const command = `${serviceName}/${functionName}` as ServiceMethodCommand;
|
||||||
|
if (isListenerCommand(command)) {
|
||||||
|
return async (listener: Record<string, any>) => {
|
||||||
|
for (const key in listener) {
|
||||||
|
if (typeof listener[key] === 'function') {
|
||||||
|
const listenerCmd = `${command.split('/')[0]}/${key}`;
|
||||||
|
if (!clientCallback.has(listenerCmd)) {
|
||||||
|
clientCallback.set(listenerCmd, [listener[key].bind(listener)]);
|
||||||
|
} else {
|
||||||
|
clientCallback.get(listenerCmd)?.push(listener[key].bind(listener));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return await receiverEvent(command);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return async (...args: any[]) => {
|
||||||
|
return await receiverEvent(command, ...args);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const receiverListener = async function (command: string, ...args: any[]) {
|
||||||
|
return clientCallback.get(command)?.forEach(async (callback) => await callback(...args));
|
||||||
|
};
|
||||||
|
return { receiverListener: receiverListener, object: object as ServiceNamingMapping[T] };
|
||||||
|
}
|
||||||
|
export async function receiverServiceListener(
|
||||||
|
command: string,
|
||||||
|
...args: any[]
|
||||||
|
) {
|
||||||
|
if (clientCallback.has(command)) {
|
||||||
|
return clientCallback.get(command)?.forEach(async (callback) => await callback(...args));
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearServiceState() {
|
||||||
|
listenerCmdRegisted.clear();
|
||||||
|
clientCallback.clear();
|
||||||
|
}
|
23
src/remote/wrapper.ts
Normal file
23
src/remote/wrapper.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { NodeIKernelLoginService, NodeIQQNTWrapperEngine, NodeIQQNTWrapperSession, NodeQQNTWrapperUtil, WrapperNodeApi } from "@/core";
|
||||||
|
import { NodeIO3MiscService } from "@/core/services/NodeIO3MiscService";
|
||||||
|
import { dirname } from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
|
||||||
|
export const LocalVirtualWrapper: WrapperNodeApi = {
|
||||||
|
NodeIO3MiscService: {
|
||||||
|
get: () => LocalVirtualWrapper.NodeIO3MiscService,
|
||||||
|
addO3MiscListener: () => 0,
|
||||||
|
setAmgomDataPiece: () => { },
|
||||||
|
reportAmgomWeather: () => { },
|
||||||
|
} as NodeIO3MiscService,
|
||||||
|
NodeQQNTWrapperUtil: {
|
||||||
|
get: () => LocalVirtualWrapper.NodeQQNTWrapperUtil,
|
||||||
|
getNTUserDataInfoConfig: function (): string {
|
||||||
|
let current_path = dirname(fileURLToPath(import.meta.url));
|
||||||
|
return current_path;
|
||||||
|
}
|
||||||
|
} as NodeQQNTWrapperUtil,
|
||||||
|
NodeIQQNTWrapperSession: {} as NodeIQQNTWrapperSession,
|
||||||
|
NodeIQQNTWrapperEngine: {} as NodeIQQNTWrapperEngine,
|
||||||
|
NodeIKernelLoginService: {} as NodeIKernelLoginService,
|
||||||
|
};
|
Reference in New Issue
Block a user