feat: 装饰器与装饰器路由注册

This commit is contained in:
手瓜一十雪 2025-11-14 19:49:13 +08:00
parent 36aa08a8f5
commit 5043a49779
13 changed files with 233 additions and 28 deletions

View File

@ -12,13 +12,14 @@
}, },
"devDependencies": { "devDependencies": {
"@rollup/plugin-node-resolve": "^16.0.3", "@rollup/plugin-node-resolve": "^16.0.3",
"@vitejs/plugin-react-swc": "^4.2.2",
"typescript": "^5.3.0",
"vite": "^6.4.1", "vite": "^6.4.1",
"vite-plugin-cp": "^6.0.3", "vite-plugin-cp": "^6.0.3"
"typescript": "^5.3.0"
}, },
"dependencies": { "dependencies": {
"silk-wasm": "^3.6.1",
"express": "^5.0.0", "express": "^5.0.0",
"silk-wasm": "^3.6.1",
"ws": "^8.18.3" "ws": "^8.18.3"
} }
} }

View File

@ -2,7 +2,9 @@ import cp from 'vite-plugin-cp';
import { defineConfig, PluginOption, UserConfig } from 'vite'; import { defineConfig, PluginOption, UserConfig } from 'vite';
import path, { resolve } from 'path'; import path, { resolve } from 'path';
import nodeResolve from '@rollup/plugin-node-resolve'; import nodeResolve from '@rollup/plugin-node-resolve';
import { autoIncludeTSPlugin } from "napcat-vite/vite-auto-include.js";
import { builtinModules } from 'module'; import { builtinModules } from 'module';
import react from '@vitejs/plugin-react-swc';
//依赖排除 //依赖排除
const external = [ const external = [
'silk-wasm', 'silk-wasm',
@ -11,6 +13,12 @@ const external = [
]; ];
const nodeModules = [...builtinModules, builtinModules.map((m) => `node:${m}`)].flat(); const nodeModules = [...builtinModules, builtinModules.map((m) => `node:${m}`)].flat();
const FrameworkBaseConfigPlugin: PluginOption[] = [ const FrameworkBaseConfigPlugin: PluginOption[] = [
autoIncludeTSPlugin({
entries: [
{ entry: 'napcat.ts', dir: path.resolve(__dirname, '../napcat-onebot/action/test') }
]
}),
react({ tsDecorators: true }),
cp({ cp({
targets: [ targets: [
{ src: '../napcat-napi-loader/', dest: 'dist', flatten: true }, { src: '../napcat-napi-loader/', dest: 'dist', flatten: true },

View File

@ -0,0 +1,6 @@
import { OneBotAction } from './OneBotAction';
export const AutoRegisterRouter: Array<new (...args: any[]) => OneBotAction<unknown, unknown>> = [];
export function ActionHandler(target: new (...args: any[]) => OneBotAction<unknown, unknown>) {
AutoRegisterRouter.push(target);
}

View File

@ -136,8 +136,9 @@ import { DownloadFileRecordStream } from './stream/DownloadFileRecordStream';
import { DownloadFileImageStream } from './stream/DownloadFileImageStream'; import { DownloadFileImageStream } from './stream/DownloadFileImageStream';
import { TestDownloadStream } from './stream/TestStreamDownload'; import { TestDownloadStream } from './stream/TestStreamDownload';
import { UploadFileStream } from './stream/UploadFileStream'; import { UploadFileStream } from './stream/UploadFileStream';
import { AutoRegisterRouter } from './auto-register';
export function createActionMap (obContext: NapCatOneBot11Adapter, core: NapCatCore) { export function createActionMap(obContext: NapCatOneBot11Adapter, core: NapCatCore) {
const actionHandlers = [ const actionHandlers = [
new CleanStreamTempFile(obContext, core), new CleanStreamTempFile(obContext, core),
new DownloadFileStream(obContext, core), new DownloadFileStream(obContext, core),
@ -304,14 +305,19 @@ export function createActionMap (obContext: NapCatOneBot11Adapter, core: NapCatC
_map.set(`${h.actionName}_async` as keyof MapType, h); _map.set(`${h.actionName}_async` as keyof MapType, h);
_map.set(`${h.actionName}_rate_limited` as keyof MapType, h); _map.set(`${h.actionName}_rate_limited` as keyof MapType, h);
}); });
AutoRegisterRouter.forEach((ActionClass) => {
const handlerInstance = new ActionClass(obContext, core); // 延迟实例化
const actionName = handlerInstance.actionName; // 获取 actionName
_map.set(actionName as keyof MapType, handlerInstance as HandlerUnion);
_map.set(`${actionName}_async` as keyof MapType, handlerInstance as HandlerUnion);
_map.set(`${actionName}_rate_limited` as keyof MapType, handlerInstance as HandlerUnion);
});
// function get<K extends keyof MapType>(key: K): MapType[K]; // function get<K extends keyof MapType>(key: K): MapType[K];
// function get<K extends keyof MapType>(key: K): null; // function get<K extends keyof MapType>(key: K): null;
// function get<K extends keyof MapType>(key: K): HandlerUnion | null | MapType[K] // function get<K extends keyof MapType>(key: K): HandlerUnion | null | MapType[K]
function get<K extends keyof MapType> (key: K): MapType[K] | undefined { function get<K extends keyof MapType>(key: K): MapType[K] | undefined {
return _map.get(key as keyof MapType) as MapType[K] | undefined; return _map.get(key as keyof MapType) as MapType[K] | undefined;
} }
return { get }; return { get };
} }
export type ActionMap = ReturnType<typeof createActionMap>; export type ActionMap = ReturnType<typeof createActionMap>;

View File

@ -10,6 +10,8 @@ export interface InvalidCheckResult {
} }
export const ActionName = { export const ActionName = {
TestAutoRegister01: 'test_auto_register_01',
TestAutoRegister02: 'test_auto_register_02',
// 所有 Normal Stream Api 表示并未流传输 表示与流传输有关 // 所有 Normal Stream Api 表示并未流传输 表示与流传输有关
CleanStreamTempFile: 'clean_stream_temp_file', CleanStreamTempFile: 'clean_stream_temp_file',

View File

@ -0,0 +1,17 @@
# 自动注册路由测试
## 参与工具
vite-auto-include 自动化收集所有文件并引入
@vitejs/plugin-react-swc 包含对装饰器的支持
## 支持示例
```typescript
@ActionHandler
export default class TestAutoRegister02 extends OneBotAction<void, string> {
override actionName = ActionName.TestAutoRegister02;
async _handle (_payload: void): Promise<string> {
return 'AutoRegister Router Test';
}
}
```

View File

@ -0,0 +1,12 @@
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
import { ActionName } from '@/napcat-onebot/action/router';
import { ActionHandler } from '../auto-register';
@ActionHandler
export default class TestAutoRegister01 extends OneBotAction<void, string> {
override actionName = ActionName.TestAutoRegister01;
async _handle (_payload: void): Promise<string> {
return 'AutoRegister Router Test';
}
}

View File

@ -0,0 +1,12 @@
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
import { ActionName } from '@/napcat-onebot/action/router';
import { ActionHandler } from '../auto-register';
@ActionHandler
export default class TestAutoRegister02 extends OneBotAction<void, string> {
override actionName = ActionName.TestAutoRegister02;
async _handle (_payload: void): Promise<string> {
return 'AutoRegister Router Test';
}
}

View File

@ -0,0 +1,31 @@
// 日志装饰器
function log2(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Calling ${propertyKey} with args: ${JSON.stringify(args)}`);
const result = originalMethod.apply(this, args);
console.log(`${propertyKey} returned: ${result}`);
return result;
};
return descriptor;
}
// 示例类
class MathOperations2 {
@log2
add(a: number, b: number): number {
return a + b;
}
@log2
multiply(a: number, b: number): number {
return a * b;
}
}
// 创建实例并调用方法
const math2 = new MathOperations2();
math2.add(1, 2); // 调用加法
math2.multiply(3, 4); // 调用乘法

View File

@ -0,0 +1,31 @@
// 日志装饰器
function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Calling ${propertyKey} with args: ${JSON.stringify(args)}`);
const result = originalMethod.apply(this, args);
console.log(`${propertyKey} returned: ${result}`);
return result;
};
return descriptor;
}
// 示例类
class MathOperations {
@log
add(a: number, b: number): number {
return a + b;
}
@log
multiply(a: number, b: number): number {
return a * b;
}
}
// 创建实例并调用方法
const math = new MathOperations();
math.add(1, 2); // 调用加法
math.multiply(3, 4); // 调用乘法

View File

@ -4,6 +4,8 @@ import path, { resolve } from 'path';
import nodeResolve from '@rollup/plugin-node-resolve'; import nodeResolve from '@rollup/plugin-node-resolve';
import { builtinModules } from 'module'; import { builtinModules } from 'module';
import napcatVersion from "napcat-vite/vite-plugin-version.js"; import napcatVersion from "napcat-vite/vite-plugin-version.js";
import { autoIncludeTSPlugin } from "napcat-vite/vite-auto-include.js";
import react from '@vitejs/plugin-react-swc';
//依赖排除 //依赖排除
const external = [ const external = [
@ -11,8 +13,15 @@ const external = [
'ws', 'ws',
'express' 'express'
]; ];
const nodeModules = [...builtinModules, builtinModules.map((m) => `node:${m}`)].flat(); const nodeModules = [...builtinModules, builtinModules.map((m) => `node:${m}`)].flat();
const ShellBaseConfigPlugin: PluginOption[] = [ const ShellBaseConfigPlugin: PluginOption[] = [
react({ tsDecorators: true }),
autoIncludeTSPlugin({
entries: [
{ entry: 'napcat.ts', dir: path.resolve(__dirname, '../napcat-onebot/action/test') }
]
}),
cp({ cp({
targets: [ targets: [
{ src: '../napcat-native/', dest: 'dist/native', flatten: false }, { src: '../napcat-native/', dest: 'dist/native', flatten: false },

View File

@ -40,7 +40,8 @@
"allowJs": true "allowJs": true
}, },
"include": [ "include": [
"vite-plugin-version.js" "*.js",
"**/*.js"
], ],
"exclude": [ "exclude": [
"node_modules", "node_modules",

View File

@ -0,0 +1,69 @@
import path from 'path';
import fs from 'fs';
export function autoIncludeTSPlugin(options) {
// options: { entries: [{ entry: 'napcat.ts', dir: './utils' }, ...] }
const { entries } = options;
let tsFilesMap = {};
return {
name: 'vite-auto-include',
async buildStart() {
tsFilesMap = {};
for (const { entry, dir } of entries) {
const fullDir = path.resolve(dir);
const allTsFiles = await findTSFiles(fullDir);
const validFiles = [];
allTsFiles.forEach((filePath) => {
try {
const source = fs.readFileSync(filePath, 'utf-8');
if (source && source.trim() !== '') {
validFiles.push(filePath);
} else {
// Skipping empty file: ${filePath}
}
} catch (error) {
console.error(`Error reading file: ${filePath}`, error);
}
});
tsFilesMap[entry] = validFiles;
}
},
transform(code, id) {
for (const [entry, tsFiles] of Object.entries(tsFilesMap)) {
// 检查id是否匹配entry支持完整路径或相对路径
const isMatch = id.endsWith(entry) || id.includes(entry);
if (isMatch && tsFiles.length > 0) {
const imports = tsFiles.map(filePath => {
const relativePath = path.relative(path.dirname(id), filePath).replace(/\\/g, '/');
return `import './${relativePath}';`;
}).join('\n');
return imports + '\n' + code;
}
}
return code;
},
};
}
// 辅助函数:查找所有 .ts 文件
async function findTSFiles(dir) {
const files = [];
const items = await fs.promises.readdir(dir, { withFileTypes: true });
for (let item of items) {
const fullPath = path.join(dir, item.name);
if (item.isDirectory()) {
files.push(...await findTSFiles(fullPath)); // 递归查找子目录
} else if (item.isFile() && fullPath.endsWith('.ts')) {
files.push(fullPath); // 收集 .ts 文件
}
}
return files;
}