Add napcat-rpc package with deep RPC

Introduce a new napcat-rpc package implementing a deep-proxy RPC system. Adds client (createDeepProxy, proxy helpers), server (RpcServer, createRpcServer), serializer (serialize/deserialize, callback registry), and transport layers (LocalTransport, MessageTransport, message server handler), plus an easy API (createRpcPair, mockRemote, createServer, createClient). Includes TypeScript types, tsconfig and package.json. Wire-up: add package alias in napcat-schema vite config and add napcat-rpc dependency to napcat-test along with comprehensive rpc tests.
This commit is contained in:
手瓜一十雪 2026-02-02 17:12:05 +08:00
parent 52b6627ebd
commit a4527fd8ca
14 changed files with 2828 additions and 0 deletions

View File

@ -0,0 +1,25 @@
{
"name": "napcat-rpc",
"version": "0.0.1",
"private": true,
"type": "module",
"main": "src/index.ts",
"scripts": {
"typecheck": "tsc --noEmit --skipLibCheck -p tsconfig.json"
},
"exports": {
".": {
"import": "./src/index.ts"
},
"./src/*": {
"import": "./src/*"
}
},
"dependencies": {},
"devDependencies": {
"@types/node": "^22.0.1"
},
"engines": {
"node": ">=18.0.0"
}
}

View File

@ -0,0 +1,352 @@
import {
type DeepProxyOptions,
type ProxyMeta,
RpcOperationType,
PROXY_META,
type RpcRequest,
} from './types.js';
import {
serialize,
deserialize,
SimpleCallbackRegistry,
extractCallbackIds,
} from './serializer.js';
/**
* ID
*/
function generateRequestId (): string {
return `req_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
}
/**
* RPC
*
* 访 RPC
*/
export function createDeepProxy<T = unknown> (options: DeepProxyOptions): T {
const {
transport,
rootPath = [],
// callbackTimeout 可供未来扩展使用
} = options;
void options.callbackTimeout;
const callbackRegistry = new SimpleCallbackRegistry();
// 注册回调处理器
if (transport.onCallback) {
transport.onCallback(async (callbackId, serializedArgs) => {
const callback = callbackRegistry.get(callbackId);
if (!callback) {
throw new Error(`Callback not found: ${callbackId}`);
}
const args = serializedArgs.map(arg => deserialize(arg, {
callbackResolver: (id) => {
const cb = callbackRegistry.get(id);
if (!cb) throw new Error(`Nested callback not found: ${id}`);
return cb;
},
proxyCreator: (path) => createProxyAtPath(path),
}));
const result = await callback(...args);
return serialize(result, { callbackRegistry });
});
}
/**
*
*/
function createProxyAtPath (path: PropertyKey[]): unknown {
const proxyMeta: ProxyMeta = {
path: [...path],
isProxy: true,
};
// 创建一个函数目标,以支持 apply 和 construct
const target = function () { } as unknown as Record<PropertyKey, unknown>;
return new Proxy(target, {
get (_target, prop) {
// 返回代理元数据
if (prop === PROXY_META) {
return proxyMeta;
}
// then 方法特殊处理,使代理可以被 await
if (prop === 'then') {
return undefined;
}
// 返回新的子路径代理
return createProxyAtPath([...path, prop]);
},
set (_target, prop, value) {
const request: RpcRequest = {
id: generateRequestId(),
type: RpcOperationType.SET,
path: [...path, prop],
args: [serialize(value, { callbackRegistry })],
};
// 同步返回,但实际是异步操作
transport.send(request).catch(() => { /* ignore */ });
return true;
},
apply (_target, _thisArg, args) {
const serializedArgs = args.map(arg => serialize(arg, { callbackRegistry }));
const callbackIds = extractCallbackIds(serializedArgs);
const request: RpcRequest = {
id: generateRequestId(),
type: RpcOperationType.APPLY,
path,
args: serializedArgs,
callbackIds: Object.keys(callbackIds).length > 0 ? callbackIds : undefined,
};
return createAsyncResultProxy(request);
},
construct (_target, args): object {
const serializedArgs = args.map(arg => serialize(arg, { callbackRegistry }));
const callbackIds = extractCallbackIds(serializedArgs);
const request: RpcRequest = {
id: generateRequestId(),
type: RpcOperationType.CONSTRUCT,
path,
args: serializedArgs,
callbackIds: Object.keys(callbackIds).length > 0 ? callbackIds : undefined,
};
return createAsyncResultProxy(request) as object;
},
has (_target, prop) {
// 检查是否为代理元数据符号
if (prop === PROXY_META) {
return true;
}
// 同步返回 true实际检查通过异步完成
return true;
},
ownKeys () {
// 返回空数组,实际键需要通过异步获取
return [];
},
getOwnPropertyDescriptor (_target, _prop) {
return {
configurable: true,
enumerable: true,
writable: true,
};
},
deleteProperty (_target, prop) {
const request: RpcRequest = {
id: generateRequestId(),
type: RpcOperationType.DELETE,
path: [...path, prop],
};
transport.send(request).catch(() => { /* ignore */ });
return true;
},
getPrototypeOf () {
return Object.prototype;
},
});
}
/**
*
* Promise-like await
* 访
*/
function createAsyncResultProxy (request: RpcRequest): unknown {
let resultPromise: Promise<unknown> | null = null;
const getResult = async (): Promise<unknown> => {
if (!resultPromise) {
resultPromise = (async () => {
const response = await transport.send(request);
if (!response.success) {
const error = new Error(response.error ?? 'RPC call failed');
if (response.stack) {
error.stack = response.stack;
}
throw error;
}
if (response.result === undefined) {
return undefined;
}
// 如果结果是可代理对象,返回代理
if (response.isProxyable && response.result) {
const deserialized = deserialize(response.result, {
callbackResolver: (id) => {
const cb = callbackRegistry.get(id);
if (!cb) throw new Error(`Callback not found: ${id}`);
return cb;
},
proxyCreator: (proxyPath) => createProxyAtPath(proxyPath),
});
return deserialized;
}
return deserialize(response.result, {
callbackResolver: (id) => {
const cb = callbackRegistry.get(id);
if (!cb) throw new Error(`Callback not found: ${id}`);
return cb;
},
proxyCreator: (proxyPath) => createProxyAtPath(proxyPath),
});
})();
}
return resultPromise;
};
// 创建一个可链式访问的代理
const target = function () { } as unknown as Record<PropertyKey, unknown>;
return new Proxy(target, {
get (_target, prop) {
if (prop === 'then') {
return (resolve: (value: unknown) => void, reject: (error: unknown) => void) => {
getResult().then(resolve, reject);
};
}
if (prop === 'catch') {
return (reject: (error: unknown) => void) => {
getResult().catch(reject);
};
}
if (prop === 'finally') {
return (callback: () => void) => {
getResult().finally(callback);
};
}
if (prop === PROXY_META) {
return undefined;
}
// 链式访问:等待结果后访问其属性
return createChainedProxy(getResult(), [prop]);
},
apply (_target, _thisArg, args) {
// 等待结果后调用
return getResult().then(result => {
if (typeof result === 'function') {
return result(...args);
}
throw new Error('Result is not callable');
});
},
});
}
/**
*
* await result.prop.method()
*/
function createChainedProxy (parentPromise: Promise<unknown>, path: PropertyKey[]): unknown {
const target = function () { } as unknown as Record<PropertyKey, unknown>;
return new Proxy(target, {
get (_target, prop) {
if (prop === 'then') {
return (resolve: (value: unknown) => void, reject: (error: unknown) => void) => {
parentPromise
.then(parent => {
let value: unknown = parent;
for (const key of path) {
if (value === null || value === undefined) {
return undefined;
}
value = (value as Record<PropertyKey, unknown>)[key];
}
resolve(value);
})
.catch(reject);
};
}
if (prop === 'catch') {
return (reject: (error: unknown) => void) => {
parentPromise.catch(reject);
};
}
if (prop === 'finally') {
return (callback: () => void) => {
parentPromise.finally(callback);
};
}
return createChainedProxy(parentPromise, [...path, prop]);
},
apply (_target, _thisArg, args) {
return parentPromise.then(parent => {
let value: unknown = parent;
const pathToMethod = path.slice(0, -1);
const methodName = path[path.length - 1];
for (const key of pathToMethod) {
if (value === null || value === undefined) {
throw new Error(`Cannot access property '${String(key)}' of ${value}`);
}
value = (value as Record<PropertyKey, unknown>)[key];
}
const method = (value as Record<PropertyKey, unknown>)[methodName!];
if (typeof method !== 'function') {
throw new Error(`${String(methodName)} is not a function`);
}
return method.call(value, ...args);
});
},
});
}
return createProxyAtPath(rootPath) as T;
}
/**
*
*/
export function getProxyMeta (proxy: unknown): ProxyMeta | undefined {
if (proxy != null && (typeof proxy === 'object' || typeof proxy === 'function')) {
try {
// 直接访问 Symbol 属性,代理的 get 陷阱会返回元数据
const meta = (proxy as Record<symbol, ProxyMeta | undefined>)[PROXY_META];
if (meta && meta.isProxy === true) {
return meta;
}
} catch {
// 忽略访问错误
}
}
return undefined;
}
/**
* RPC
*/
export function isRpcProxy (value: unknown): boolean {
return getProxyMeta(value) !== undefined;
}

View File

@ -0,0 +1,130 @@
/**
* RPC API
*
* client/server
* client server
*/
import { LocalTransport } from './transport.js';
import { createDeepProxy, getProxyMeta, isRpcProxy } from './client.js';
import { RpcServer } from './server.js';
import type { ProxyMeta } from './types.js';
/**
* RPC
*/
export interface RpcPair<T> {
/** 客户端代理 - 在这里操作就像直接操作服务端的变量 */
client: T;
/** 服务端原始对象 */
server: T;
/** 关闭连接 */
close (): void;
}
/**
* RPC
*
* client/server client RPC server
*
* @example
* ```ts
* const { client, server } = createRpcPair({
* name: 'test',
* greet: (msg: string) => `Hello, ${msg}!`,
* register: (handlers: { onSuccess: Function, onError: Function }) => {
* handlers.onSuccess('done');
* }
* });
*
* // 在 client 端操作,就像直接操作 server 端的变量
* await client.greet('world'); // 返回 'Hello, world!'
*
* // 支持包含多个回调的对象
* await client.register({
* onSuccess: (result) => console.log(result),
* onError: (err) => console.error(err)
* });
* ```
*/
export function createRpcPair<T extends object> (target: T): RpcPair<T> {
const transport = new LocalTransport(target);
const client = createDeepProxy<T>({ transport });
return {
client,
server: target,
close: () => transport.close(),
};
}
/**
*
*
* "看起来像远程变量" RPC
*
* @example
* ```ts
* const remoteApi = mockRemote({
* counter: 0,
* increment() { return ++this.counter; },
* async fetchData(id: number) { return { id, data: 'test' }; }
* });
*
* // 所有操作都是异步的,通过 RPC 隔离
* await remoteApi.increment(); // 1
* await remoteApi.fetchData(123); // { id: 123, data: 'test' }
* ```
*/
export function mockRemote<T extends object> (target: T): T {
return createRpcPair(target).client;
}
/**
* RPC
*
* @example
* ```ts
* const server = createServer({
* users: new Map(),
* addUser(id: string, name: string) {
* this.users.set(id, { name });
* return true;
* }
* });
*
* // 获取传输层供客户端连接
* const transport = server.getTransport();
* ```
*/
export function createServer<T extends object> (target: T): {
target: T;
handler: RpcServer;
getTransport (): LocalTransport;
} {
const handler = new RpcServer({ target });
return {
target,
handler,
getTransport: () => new LocalTransport(target),
};
}
/**
*
*
* @example
* ```ts
* const server = createServer(myApi);
* const client = createClient<typeof myApi>(server.getTransport());
*
* await client.someMethod();
* ```
*/
export function createClient<T extends object> (transport: LocalTransport): T {
return createDeepProxy<T>({ transport });
}
// 重新导出常用工具
export { getProxyMeta, isRpcProxy };
export type { ProxyMeta };

View File

@ -0,0 +1,60 @@
/**
* napcat-rpc
*
* RPC - RPC
*/
// 简化 API推荐使用
export {
createRpcPair,
mockRemote,
createServer,
createClient,
} from './easy.js';
// 类型导出
export {
RpcOperationType,
SerializedValueType,
PROXY_META,
type RpcRequest,
type RpcResponse,
type SerializedValue,
type RpcTransport,
type RpcServerHandler,
type RpcServerOptions,
type DeepProxyOptions,
type ProxyMeta,
} from './types.js';
// 序列化工具
export {
serialize,
deserialize,
extractCallbackIds,
SimpleCallbackRegistry,
type CallbackRegistry,
type SerializeContext,
type DeserializeContext,
} from './serializer.js';
// 客户端代理
export {
createDeepProxy,
getProxyMeta,
isRpcProxy,
} from './client.js';
// 服务端
export {
RpcServer,
createRpcServer,
} from './server.js';
// 传输层
export {
LocalTransport,
MessageTransport,
createMessageServerHandler,
type MessageTransportOptions,
} from './transport.js';

View File

@ -0,0 +1,386 @@
import {
SerializedValue,
SerializedValueType,
PROXY_META,
type ProxyMeta,
} from './types.js';
/**
*
*/
export interface CallbackRegistry {
register (fn: Function): string;
get (id: string): Function | undefined;
remove (id: string): void;
}
/**
*
*/
export class SimpleCallbackRegistry implements CallbackRegistry {
private callbacks = new Map<string, Function>();
private counter = 0;
register (fn: Function): string {
const id = `cb_${++this.counter}_${Date.now()}`;
this.callbacks.set(id, fn);
return id;
}
get (id: string): Function | undefined {
return this.callbacks.get(id);
}
remove (id: string): void {
this.callbacks.delete(id);
}
clear (): void {
this.callbacks.clear();
}
}
/**
*
*/
export interface SerializeContext {
/** 回调注册器 */
callbackRegistry?: CallbackRegistry;
/** 已序列化对象映射(用于循环引用检测) */
seen?: WeakMap<object, SerializedValue>;
/** 深度限制 */
maxDepth?: number;
/** 当前深度 */
currentDepth?: number;
}
/**
*
*/
export interface DeserializeContext {
/** 回调解析器 */
callbackResolver?: (id: string) => Function;
/** 代理创建器 */
proxyCreator?: (path: PropertyKey[]) => unknown;
}
/**
*
*/
export function serialize (value: unknown, context: SerializeContext = {}): SerializedValue {
const {
callbackRegistry,
seen = new WeakMap(),
maxDepth = 50,
currentDepth = 0,
} = context;
// 深度检查
if (currentDepth > maxDepth) {
return { type: SerializedValueType.STRING, value: '[Max depth exceeded]' };
}
// 基本类型处理
if (value === undefined) {
return { type: SerializedValueType.UNDEFINED };
}
if (value === null) {
return { type: SerializedValueType.NULL };
}
const valueType = typeof value;
if (valueType === 'boolean') {
return { type: SerializedValueType.BOOLEAN, value };
}
if (valueType === 'number') {
const numValue = value as number;
if (Number.isNaN(numValue)) {
return { type: SerializedValueType.NUMBER, value: 'NaN' };
}
if (!Number.isFinite(numValue)) {
return { type: SerializedValueType.NUMBER, value: numValue > 0 ? 'Infinity' : '-Infinity' };
}
return { type: SerializedValueType.NUMBER, value };
}
if (valueType === 'bigint') {
return { type: SerializedValueType.BIGINT, value: (value as bigint).toString() };
}
if (valueType === 'string') {
return { type: SerializedValueType.STRING, value };
}
if (valueType === 'symbol') {
return {
type: SerializedValueType.SYMBOL,
value: (value as symbol).description ?? '',
};
}
if (valueType === 'function') {
const fn = value as Function;
if (callbackRegistry) {
const callbackId = callbackRegistry.register(fn);
return {
type: SerializedValueType.FUNCTION,
callbackId,
className: fn.name || 'anonymous',
};
}
return {
type: SerializedValueType.FUNCTION,
className: fn.name || 'anonymous',
};
}
// 对象类型处理
const obj = value as object;
// 检查是否为代理对象
if (PROXY_META in obj) {
const meta = (obj as Record<symbol, ProxyMeta | undefined>)[PROXY_META];
if (meta) {
return {
type: SerializedValueType.PROXY_REF,
proxyPath: meta.path,
};
}
}
// 循环引用检测
if (seen.has(obj)) {
return seen.get(obj)!;
}
// Date
if (obj instanceof Date) {
return { type: SerializedValueType.DATE, value: obj.toISOString() };
}
// RegExp
if (obj instanceof RegExp) {
return {
type: SerializedValueType.REGEXP,
value: { source: obj.source, flags: obj.flags },
};
}
// Error
if (obj instanceof Error) {
return {
type: SerializedValueType.ERROR,
value: obj.message,
className: obj.constructor.name,
properties: {
stack: serialize(obj.stack, { ...context, seen, currentDepth: currentDepth + 1 }),
},
};
}
// Buffer / Uint8Array
if (obj instanceof Uint8Array) {
return {
type: SerializedValueType.BUFFER,
value: Array.from(obj as Uint8Array),
};
}
// Node.js Buffer
if (typeof globalThis !== 'undefined' && 'Buffer' in globalThis) {
const BufferClass = (globalThis as unknown as { Buffer: { isBuffer (obj: unknown): boolean; }; }).Buffer;
if (BufferClass.isBuffer(obj)) {
return {
type: SerializedValueType.BUFFER,
value: Array.from(obj as unknown as Uint8Array),
};
}
}
// Map
if (obj instanceof Map) {
const entries: SerializedValue[] = [];
const nextContext = { ...context, seen, currentDepth: currentDepth + 1 };
for (const [k, v] of obj) {
entries.push(serialize([k, v], nextContext));
}
return {
type: SerializedValueType.MAP,
elements: entries,
};
}
// Set
if (obj instanceof Set) {
const elements: SerializedValue[] = [];
const nextContext = { ...context, seen, currentDepth: currentDepth + 1 };
for (const v of obj) {
elements.push(serialize(v, nextContext));
}
return {
type: SerializedValueType.SET,
elements,
};
}
// Promise
if (obj instanceof Promise) {
return { type: SerializedValueType.PROMISE };
}
// Array
if (Array.isArray(obj)) {
const result: SerializedValue = {
type: SerializedValueType.ARRAY,
elements: [],
};
seen.set(obj, result);
const nextContext = { ...context, seen, currentDepth: currentDepth + 1 };
result.elements = obj.map(item => serialize(item, nextContext));
return result;
}
// 普通对象
const result: SerializedValue = {
type: SerializedValueType.OBJECT,
className: obj.constructor?.name ?? 'Object',
properties: {},
};
seen.set(obj, result);
const nextContext = { ...context, seen, currentDepth: currentDepth + 1 };
for (const key of Object.keys(obj)) {
result.properties![key] = serialize((obj as Record<string, unknown>)[key], nextContext);
}
return result;
}
/**
*
*/
export function deserialize (data: SerializedValue, context: DeserializeContext = {}): unknown {
const { callbackResolver, proxyCreator } = context;
switch (data.type) {
case SerializedValueType.UNDEFINED:
return undefined;
case SerializedValueType.NULL:
return null;
case SerializedValueType.BOOLEAN:
return data.value;
case SerializedValueType.NUMBER:
if (data.value === 'NaN') return NaN;
if (data.value === 'Infinity') return Infinity;
if (data.value === '-Infinity') return -Infinity;
return data.value;
case SerializedValueType.BIGINT:
return BigInt(data.value as string);
case SerializedValueType.STRING:
return data.value;
case SerializedValueType.SYMBOL:
return Symbol(data.value as string);
case SerializedValueType.FUNCTION:
if (data.callbackId && callbackResolver) {
return callbackResolver(data.callbackId);
}
// 返回一个占位函数
return function placeholder () {
throw new Error('Remote function cannot be called without callback resolver');
};
case SerializedValueType.DATE:
return new Date(data.value as string);
case SerializedValueType.REGEXP: {
const { source, flags } = data.value as { source: string; flags: string; };
return new RegExp(source, flags);
}
case SerializedValueType.ERROR: {
const error = new Error(data.value as string);
if (data.properties?.['stack']) {
error.stack = deserialize(data.properties['stack'], context) as string;
}
return error;
}
case SerializedValueType.BUFFER: {
const arr = data.value as number[];
if (typeof globalThis !== 'undefined' && 'Buffer' in globalThis) {
const BufferClass = (globalThis as unknown as { Buffer: { from (arr: number[]): Uint8Array; }; }).Buffer;
return BufferClass.from(arr);
}
return new Uint8Array(arr);
}
case SerializedValueType.MAP: {
const map = new Map();
if (data.elements) {
for (const element of data.elements) {
const [k, v] = deserialize(element, context) as [unknown, unknown];
map.set(k, v);
}
}
return map;
}
case SerializedValueType.SET: {
const set = new Set();
if (data.elements) {
for (const element of data.elements) {
set.add(deserialize(element, context));
}
}
return set;
}
case SerializedValueType.PROMISE:
return Promise.resolve(undefined);
case SerializedValueType.ARRAY:
return (data.elements ?? []).map(elem => deserialize(elem, context));
case SerializedValueType.PROXY_REF:
if (data.proxyPath && proxyCreator) {
return proxyCreator(data.proxyPath);
}
return {};
case SerializedValueType.OBJECT: {
const obj: Record<string, unknown> = {};
if (data.properties) {
for (const [key, val] of Object.entries(data.properties)) {
obj[key] = deserialize(val, context);
}
}
return obj;
}
default:
return undefined;
}
}
/**
* ID映射
*/
export function extractCallbackIds (args: SerializedValue[]): Record<number, string> {
const result: Record<number, string> = {};
args.forEach((arg, index) => {
if (arg.type === SerializedValueType.FUNCTION && arg.callbackId) {
result[index] = arg.callbackId;
}
});
return result;
}

View File

@ -0,0 +1,433 @@
import {
type RpcRequest,
type RpcResponse,
type RpcServerOptions,
type SerializedValue,
RpcOperationType,
} from './types.js';
import { serialize, deserialize, SimpleCallbackRegistry } from './serializer.js';
/**
* RPC
*
* RPC
*/
export class RpcServer {
private target: unknown;
private callbackInvoker?: (callbackId: string, args: unknown[]) => Promise<unknown>;
private localCallbacks = new SimpleCallbackRegistry();
constructor (options: RpcServerOptions) {
this.target = options.target;
this.callbackInvoker = options.callbackInvoker;
}
/**
* RPC
*/
async handleRequest (request: RpcRequest): Promise<RpcResponse> {
try {
switch (request.type) {
case RpcOperationType.GET:
return this.handleGet(request);
case RpcOperationType.SET:
return this.handleSet(request);
case RpcOperationType.APPLY:
return await this.handleApply(request);
case RpcOperationType.CONSTRUCT:
return await this.handleConstruct(request);
case RpcOperationType.HAS:
return this.handleHas(request);
case RpcOperationType.OWNKEYS:
return this.handleOwnKeys(request);
case RpcOperationType.DELETE:
return this.handleDelete(request);
case RpcOperationType.GET_DESCRIPTOR:
return this.handleGetDescriptor(request);
case RpcOperationType.GET_PROTOTYPE:
return this.handleGetPrototype(request);
case RpcOperationType.RELEASE:
return this.handleRelease(request);
default:
return {
id: request.id,
success: false,
error: `Unknown operation type: ${request.type}`,
};
}
} catch (error) {
return this.createErrorResponse(request.id, error);
}
}
/**
*
*/
private resolvePath (path: PropertyKey[]): { parent: unknown; key: PropertyKey | undefined; value: unknown; } {
let current = this.target;
let parent: unknown = null;
let key: PropertyKey | undefined;
for (let i = 0; i < path.length; i++) {
parent = current;
key = path[i];
if (key === undefined) {
throw new Error('Path contains undefined key');
}
if (current === null || current === undefined) {
throw new Error(`Cannot access property '${String(key)}' of ${current}`);
}
current = (current as Record<PropertyKey, unknown>)[key];
}
return { parent, key, value: current };
}
/**
* GET
*/
private handleGet (request: RpcRequest): RpcResponse {
const { value } = this.resolvePath(request.path);
const isProxyable = this.isProxyable(value);
return {
id: request.id,
success: true,
result: serialize(value, { callbackRegistry: this.localCallbacks }),
isProxyable,
};
}
/**
* SET
*/
private handleSet (request: RpcRequest): RpcResponse {
const path = request.path;
if (path.length === 0) {
throw new Error('Cannot set root object');
}
const parentPath = path.slice(0, -1);
const key = path[path.length - 1]!;
const { value: parent } = this.resolvePath(parentPath);
if (parent === null || parent === undefined) {
throw new Error(`Cannot set property '${String(key)}' of ${parent}`);
}
const newValue = request.args?.[0]
? deserialize(request.args[0], {
callbackResolver: this.createCallbackResolver(request),
})
: undefined;
(parent as Record<PropertyKey, unknown>)[key] = newValue;
return {
id: request.id,
success: true,
};
}
/**
* APPLY
*/
private async handleApply (request: RpcRequest): Promise<RpcResponse> {
const path = request.path;
if (path.length === 0) {
throw new Error('Cannot call root object');
}
const methodPath = path.slice(0, -1);
const methodName = path[path.length - 1]!;
const { value: parent } = this.resolvePath(methodPath);
if (parent === null || parent === undefined) {
throw new Error(`Cannot call method on ${parent}`);
}
const method = (parent as Record<PropertyKey, unknown>)[methodName];
if (typeof method !== 'function') {
throw new Error(`${String(methodName)} is not a function`);
}
const args = (request.args ?? []).map(arg =>
deserialize(arg, {
callbackResolver: this.createCallbackResolver(request),
})
);
let result = method.call(parent, ...args);
// 处理 Promise
if (result instanceof Promise) {
result = await result;
}
const isProxyable = this.isProxyable(result);
return {
id: request.id,
success: true,
result: serialize(result, { callbackRegistry: this.localCallbacks }),
isProxyable,
};
}
/**
* CONSTRUCT
*/
private async handleConstruct (request: RpcRequest): Promise<RpcResponse> {
const { value: Constructor } = this.resolvePath(request.path);
if (typeof Constructor !== 'function') {
throw new Error('Target is not a constructor');
}
const args = (request.args ?? []).map(arg =>
deserialize(arg, {
callbackResolver: this.createCallbackResolver(request),
})
);
const instance = new (Constructor as new (...args: unknown[]) => unknown)(...args);
const isProxyable = this.isProxyable(instance);
return {
id: request.id,
success: true,
result: serialize(instance, { callbackRegistry: this.localCallbacks }),
isProxyable,
};
}
/**
* HAS
*/
private handleHas (request: RpcRequest): RpcResponse {
const path = request.path;
if (path.length === 0) {
return {
id: request.id,
success: true,
result: serialize(true),
};
}
const parentPath = path.slice(0, -1);
const key = path[path.length - 1]!;
const { value: parent } = this.resolvePath(parentPath);
const has = parent !== null && parent !== undefined && key in (parent as object);
return {
id: request.id,
success: true,
result: serialize(has),
};
}
/**
* OWNKEYS
*/
private handleOwnKeys (request: RpcRequest): RpcResponse {
const { value } = this.resolvePath(request.path);
if (value === null || value === undefined) {
return {
id: request.id,
success: true,
result: serialize([]),
};
}
const keys = Reflect.ownKeys(value as object);
return {
id: request.id,
success: true,
result: serialize(keys.map(k => (typeof k === 'symbol' ? k.description ?? '' : String(k)))),
};
}
/**
* DELETE
*/
private handleDelete (request: RpcRequest): RpcResponse {
const path = request.path;
if (path.length === 0) {
throw new Error('Cannot delete root object');
}
const parentPath = path.slice(0, -1);
const key = path[path.length - 1]!;
const { value: parent } = this.resolvePath(parentPath);
if (parent === null || parent === undefined) {
throw new Error(`Cannot delete property from ${parent}`);
}
const deleted = delete (parent as Record<PropertyKey, unknown>)[key];
return {
id: request.id,
success: true,
result: serialize(deleted),
};
}
/**
* GET_DESCRIPTOR
*/
private handleGetDescriptor (request: RpcRequest): RpcResponse {
const path = request.path;
if (path.length === 0) {
return {
id: request.id,
success: true,
result: serialize(undefined),
};
}
const parentPath = path.slice(0, -1);
const key = path[path.length - 1]!;
const { value: parent } = this.resolvePath(parentPath);
if (parent === null || parent === undefined) {
return {
id: request.id,
success: true,
result: serialize(undefined),
};
}
const descriptor = Object.getOwnPropertyDescriptor(parent as object, key);
if (!descriptor) {
return {
id: request.id,
success: true,
result: serialize(undefined),
};
}
// 序列化描述符(排除 value 和 get/set 函数)
return {
id: request.id,
success: true,
result: serialize({
configurable: descriptor.configurable,
enumerable: descriptor.enumerable,
writable: descriptor.writable,
}),
};
}
/**
* GET_PROTOTYPE
*/
private handleGetPrototype (request: RpcRequest): RpcResponse {
const { value } = this.resolvePath(request.path);
if (value === null || value === undefined) {
return {
id: request.id,
success: true,
result: serialize(null),
};
}
const proto = Object.getPrototypeOf(value);
const name = proto?.constructor?.name ?? 'Object';
return {
id: request.id,
success: true,
result: serialize({ name }),
};
}
/**
* RELEASE
*/
private handleRelease (request: RpcRequest): RpcResponse {
// 清理与该路径相关的资源(如果有)
return {
id: request.id,
success: true,
};
}
/**
*
*/
private createCallbackResolver (_request: RpcRequest): (id: string) => Function {
return (callbackId: string) => {
// 创建一个代理函数,调用时会通过 callbackInvoker 发送回客户端
return async (...args: unknown[]) => {
if (!this.callbackInvoker) {
throw new Error('Callback invoker not configured');
}
return this.callbackInvoker(callbackId, args);
};
};
}
/**
*
*/
private isProxyable (value: unknown): boolean {
if (value === null || value === undefined) {
return false;
}
const type = typeof value;
return type === 'object' || type === 'function';
}
/**
*
*/
private createErrorResponse (requestId: string, error: unknown): RpcResponse {
if (error instanceof Error) {
return {
id: requestId,
success: false,
error: error.message,
stack: error.stack,
};
}
return {
id: requestId,
success: false,
error: String(error),
};
}
/**
*
*/
async invokeCallback (callbackId: string, args: unknown[]): Promise<SerializedValue> {
if (!this.callbackInvoker) {
throw new Error('Callback invoker not configured');
}
const result = await this.callbackInvoker(callbackId, args);
return serialize(result, { callbackRegistry: this.localCallbacks });
}
}
/**
* RPC
*/
export function createRpcServer (options: RpcServerOptions): RpcServer {
return new RpcServer(options);
}

View File

@ -0,0 +1,204 @@
import {
type RpcTransport,
type RpcRequest,
type RpcResponse,
type SerializedValue,
} from './types.js';
import { RpcServer } from './server.js';
import { serialize, deserialize, SimpleCallbackRegistry } from './serializer.js';
/**
*
*
* RPC
*/
export class LocalTransport implements RpcTransport {
private server: RpcServer;
private callbackHandler?: (callbackId: string, args: SerializedValue[]) => Promise<SerializedValue>;
private clientCallbacks = new SimpleCallbackRegistry();
constructor (target: unknown) {
this.server = new RpcServer({
target,
callbackInvoker: async (callbackId, args) => {
if (!this.callbackHandler) {
throw new Error('Callback handler not registered');
}
const serializedArgs = args.map(arg => serialize(arg, { callbackRegistry: this.clientCallbacks }));
const result = await this.callbackHandler(callbackId, serializedArgs);
return deserialize(result);
},
});
}
async send (request: RpcRequest): Promise<RpcResponse> {
// 模拟网络延迟(可选)
// await new Promise(resolve => setTimeout(resolve, 0));
return this.server.handleRequest(request);
}
onCallback (handler: (callbackId: string, args: SerializedValue[]) => Promise<SerializedValue>): void {
this.callbackHandler = handler;
}
close (): void {
this.clientCallbacks.clear();
}
}
/**
*
*/
export interface MessageTransportOptions {
/** 发送消息 */
sendMessage: (message: string) => void | Promise<void>;
/** 接收消息时的回调 */
onMessage: (handler: (message: string) => void) => void;
}
/**
*
*
* /
*/
export class MessageTransport implements RpcTransport {
private pendingRequests = new Map<string, {
resolve: (response: RpcResponse) => void;
reject: (error: Error) => void;
}>();
private callbackHandler?: (callbackId: string, args: SerializedValue[]) => Promise<SerializedValue>;
private sendMessage: (message: string) => void | Promise<void>;
constructor (options: MessageTransportOptions) {
this.sendMessage = options.sendMessage;
options.onMessage(async (message) => {
const data = JSON.parse(message) as {
type: 'response' | 'callback' | 'callback_response';
id: string;
response?: RpcResponse;
callbackId?: string;
args?: SerializedValue[];
result?: SerializedValue;
error?: string;
};
if (data.type === 'response') {
const pending = this.pendingRequests.get(data.id);
if (pending && data.response) {
this.pendingRequests.delete(data.id);
pending.resolve(data.response);
}
} else if (data.type === 'callback') {
// 处理来自服务端的回调调用
if (this.callbackHandler && data.callbackId && data.args) {
try {
const result = await this.callbackHandler(data.callbackId, data.args);
await this.sendMessage(JSON.stringify({
type: 'callback_response',
id: data.id,
result,
}));
} catch (error) {
await this.sendMessage(JSON.stringify({
type: 'callback_response',
id: data.id,
error: error instanceof Error ? error.message : String(error),
}));
}
}
}
});
}
async send (request: RpcRequest): Promise<RpcResponse> {
return new Promise((resolve, reject) => {
this.pendingRequests.set(request.id, { resolve, reject });
const message = JSON.stringify({
type: 'request',
request,
});
Promise.resolve(this.sendMessage(message)).catch(reject);
});
}
onCallback (handler: (callbackId: string, args: SerializedValue[]) => Promise<SerializedValue>): void {
this.callbackHandler = handler;
}
close (): void {
for (const [, pending] of this.pendingRequests) {
pending.reject(new Error('Transport closed'));
}
this.pendingRequests.clear();
}
}
/**
*
*/
export function createMessageServerHandler (target: unknown, options: {
sendMessage: (message: string) => void | Promise<void>;
onMessage: (handler: (message: string) => void) => void;
}): void {
const pendingCallbacks = new Map<string, {
resolve: (result: SerializedValue) => void;
reject: (error: Error) => void;
}>();
let callbackIdCounter = 0;
const server = new RpcServer({
target,
callbackInvoker: async (callbackId, args) => {
const id = `cb_call_${++callbackIdCounter}`;
const serializedArgs = args.map(arg => serialize(arg));
return new Promise<unknown>((resolve, reject) => {
pendingCallbacks.set(id, {
resolve: (result) => resolve(deserialize(result)),
reject,
});
options.sendMessage(JSON.stringify({
type: 'callback',
id,
callbackId,
args: serializedArgs,
}));
});
},
});
options.onMessage(async (message) => {
const data = JSON.parse(message) as {
type: 'request' | 'callback_response';
id: string;
request?: RpcRequest;
result?: SerializedValue;
error?: string;
};
if (data.type === 'request' && data.request) {
const response = await server.handleRequest(data.request);
await options.sendMessage(JSON.stringify({
type: 'response',
id: data.request.id,
response,
}));
} else if (data.type === 'callback_response') {
const pending = pendingCallbacks.get(data.id);
if (pending) {
pendingCallbacks.delete(data.id);
if (data.error) {
pending.reject(new Error(data.error));
} else if (data.result) {
pending.resolve(data.result);
}
}
}
});
}

View File

@ -0,0 +1,166 @@
/**
* RPC
*/
export enum RpcOperationType {
/** 获取属性 */
GET = 'get',
/** 设置属性 */
SET = 'set',
/** 调用方法 */
APPLY = 'apply',
/** 构造函数调用 */
CONSTRUCT = 'construct',
/** 检查属性是否存在 */
HAS = 'has',
/** 获取所有键 */
OWNKEYS = 'ownKeys',
/** 删除属性 */
DELETE = 'deleteProperty',
/** 获取属性描述符 */
GET_DESCRIPTOR = 'getOwnPropertyDescriptor',
/** 获取原型 */
GET_PROTOTYPE = 'getPrototypeOf',
/** 回调调用 */
CALLBACK = 'callback',
/** 释放资源 */
RELEASE = 'release',
}
/**
* RPC
*/
export interface RpcRequest {
/** 请求 ID */
id: string;
/** 操作类型 */
type: RpcOperationType;
/** 访问路径 (从根对象开始) */
path: PropertyKey[];
/** 参数 (用于 set, apply, construct) */
args?: SerializedValue[];
/** 回调 ID 映射 (参数索引 -> 回调 ID) */
callbackIds?: Record<number, string>;
}
/**
* RPC
*/
export interface RpcResponse {
/** 请求 ID */
id: string;
/** 是否成功 */
success: boolean;
/** 返回值 */
result?: SerializedValue;
/** 错误信息 */
error?: string;
/** 错误堆栈 */
stack?: string;
/** 结果是否为可代理对象 */
isProxyable?: boolean;
}
/**
*
*/
export interface SerializedValue {
/** 值类型 */
type: SerializedValueType;
/** 原始值(用于基本类型) */
value?: unknown;
/** 对象类型名称 */
className?: string;
/** 回调 ID用于函数 */
callbackId?: string;
/** 代理路径(用于可代理对象) */
proxyPath?: PropertyKey[];
/** 数组元素或对象属性 */
properties?: Record<string, SerializedValue>;
/** 数组元素 */
elements?: SerializedValue[];
}
/**
*
*/
export enum SerializedValueType {
UNDEFINED = 'undefined',
NULL = 'null',
BOOLEAN = 'boolean',
NUMBER = 'number',
BIGINT = 'bigint',
STRING = 'string',
SYMBOL = 'symbol',
FUNCTION = 'function',
OBJECT = 'object',
ARRAY = 'array',
DATE = 'date',
REGEXP = 'regexp',
ERROR = 'error',
PROMISE = 'promise',
PROXY_REF = 'proxyRef',
BUFFER = 'buffer',
MAP = 'map',
SET = 'set',
}
/**
* RPC
*/
export interface RpcTransport {
/** 发送请求并等待响应 */
send (request: RpcRequest): Promise<RpcResponse>;
/** 注册回调处理器 */
onCallback?(handler: (callbackId: string, args: SerializedValue[]) => Promise<SerializedValue>): void;
/** 关闭连接 */
close?(): void;
}
/**
* RPC
*/
export interface RpcServerHandler {
/** 处理请求 */
handleRequest (request: RpcRequest): Promise<RpcResponse>;
/** 调用客户端回调 */
invokeCallback?(callbackId: string, args: unknown[]): Promise<unknown>;
}
/**
*
*/
export interface DeepProxyOptions {
/** 传输层 */
transport: RpcTransport;
/** 根路径 */
rootPath?: PropertyKey[];
/** 是否缓存属性 */
cacheProperties?: boolean;
/** 回调超时时间 (ms) */
callbackTimeout?: number;
}
/**
* RPC
*/
export interface RpcServerOptions {
/** 目标对象 */
target: unknown;
/** 回调调用器 */
callbackInvoker?: (callbackId: string, args: unknown[]) => Promise<unknown>;
}
/**
*
*/
export const PROXY_META = Symbol('PROXY_META');
/**
*
*/
export interface ProxyMeta {
/** 访问路径 */
path: PropertyKey[];
/** 是否为代理 */
isProxy: true;
}

View File

@ -0,0 +1,21 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"noEmit": true,
"baseUrl": ".",
"paths": {
"@/*": [
"../*/"
]
}
},
"include": [
"src/**/*.ts"
],
"exclude": [
"node_modules",
"dist"
]
}

View File

@ -15,6 +15,7 @@ export default defineConfig({
resolve: {
conditions: ['node', 'default'],
alias: {
'@/napcat-rpc': resolve(__dirname, '../napcat-rpc'),
'@/napcat-onebot': resolve(__dirname, '../napcat-onebot'),
'@/napcat-common': resolve(__dirname, '../napcat-common'),
'@/napcat-schema': resolve(__dirname, './src'),

View File

@ -12,6 +12,7 @@
},
"dependencies": {
"napcat-core": "workspace:*",
"napcat-rpc": "workspace:*",
"napcat-image-size": "workspace:*"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -8,6 +8,7 @@ export default defineConfig({
},
resolve: {
alias: {
'@/napcat-rpc': resolve(__dirname, '../napcat-rpc'),
'@/napcat-image-size': resolve(__dirname, '../napcat-image-size'),
'@/napcat-test': resolve(__dirname, '.'),
'@/napcat-common': resolve(__dirname, '../napcat-common'),

View File

@ -302,6 +302,12 @@ importers:
specifier: ^22.0.1
version: 22.19.1
packages/napcat-rpc:
devDependencies:
'@types/node':
specifier: ^22.0.1
version: 22.19.1
packages/napcat-schema:
dependencies:
'@sinclair/typebox':
@ -357,6 +363,9 @@ importers:
napcat-image-size:
specifier: workspace:*
version: link:../napcat-image-size
napcat-rpc:
specifier: workspace:*
version: link:../napcat-rpc
devDependencies:
vitest:
specifier: ^4.0.9