26 包 monorepo(TS 全源码): - snowcat-core: NT 内核 API 封装 + listener(含 9.9.37 六回调适配)+ Packet 发包 - snowcat-onebot: OneBot v11 全动作 + MCP Streamable HTTP 适配器 + 稳定线 - snowcat-webui-frontend/backend: 雪猫控制台(React+HeroUI) - snowcat-shell: 单文件 bundle 出口(vite inlineDynamicImports) - snowcat-native: napi2native / MoeHoo / ffmpeg / pty 运行时 - 56 项 SnowLuma 独门动作移植(qzone 全家桶/群相册/装扮/闪照/收藏) - 工具件: 解包器/启动器/便携制作器/doctor GitHub 关联已全量指向 DandanLLab/SnowCatQQ
316 lines
8.0 KiB
TypeScript
316 lines
8.0 KiB
TypeScript
import { Router, Request, Response, NextFunction } from 'express';
|
|
import path from 'path';
|
|
import {
|
|
PluginRouterRegistry,
|
|
PluginRequestHandler,
|
|
PluginApiRouteDefinition,
|
|
PluginPageDefinition,
|
|
PluginHttpRequest,
|
|
PluginHttpResponse,
|
|
HttpMethod,
|
|
MemoryStaticFile,
|
|
} from './types';
|
|
|
|
/**
|
|
* 包装 Express Request 为 PluginHttpRequest
|
|
*/
|
|
function wrapRequest (req: Request): PluginHttpRequest {
|
|
return {
|
|
path: req.path,
|
|
method: req.method,
|
|
query: req.query as Record<string, string | string[] | undefined>,
|
|
body: req.body,
|
|
headers: req.headers as Record<string, string | string[] | undefined>,
|
|
params: req.params,
|
|
raw: req,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 包装 Express Response 为 PluginHttpResponse
|
|
*/
|
|
function wrapResponse (res: Response): PluginHttpResponse {
|
|
const wrapped: PluginHttpResponse = {
|
|
status (code: number) {
|
|
res.status(code);
|
|
return wrapped;
|
|
},
|
|
json (data: unknown) {
|
|
res.json(data);
|
|
},
|
|
send (data: string | Buffer) {
|
|
res.send(data);
|
|
},
|
|
setHeader (name: string, value: string) {
|
|
res.setHeader(name, value);
|
|
return wrapped;
|
|
},
|
|
sendFile (filePath: string) {
|
|
res.sendFile(filePath);
|
|
},
|
|
redirect (url: string) {
|
|
res.redirect(url);
|
|
},
|
|
raw: res,
|
|
};
|
|
return wrapped;
|
|
}
|
|
|
|
/**
|
|
* 插件路由注册器实现
|
|
* 为每个插件创建独立的路由注册器,收集路由定义
|
|
*/
|
|
/** 内存静态路由定义 */
|
|
interface MemoryStaticRoute {
|
|
urlPath: string;
|
|
files: MemoryStaticFile[];
|
|
}
|
|
|
|
export class PluginRouterRegistryImpl implements PluginRouterRegistry {
|
|
private apiRoutes: PluginApiRouteDefinition[] = [];
|
|
private apiNoAuthRoutes: PluginApiRouteDefinition[] = [];
|
|
private pageDefinitions: PluginPageDefinition[] = [];
|
|
private staticRoutes: Array<{ urlPath: string; localPath: string; }> = [];
|
|
private memoryStaticRoutes: MemoryStaticRoute[] = [];
|
|
|
|
constructor (
|
|
private readonly pluginId: string,
|
|
private readonly pluginPath: string
|
|
) { }
|
|
|
|
// ==================== API 路由注册 ====================
|
|
|
|
api (method: HttpMethod, routePath: string, handler: PluginRequestHandler): void {
|
|
this.apiRoutes.push({ method, path: routePath, handler });
|
|
}
|
|
|
|
get (routePath: string, handler: PluginRequestHandler): void {
|
|
this.api('get', routePath, handler);
|
|
}
|
|
|
|
post (routePath: string, handler: PluginRequestHandler): void {
|
|
this.api('post', routePath, handler);
|
|
}
|
|
|
|
put (routePath: string, handler: PluginRequestHandler): void {
|
|
this.api('put', routePath, handler);
|
|
}
|
|
|
|
delete (routePath: string, handler: PluginRequestHandler): void {
|
|
this.api('delete', routePath, handler);
|
|
}
|
|
|
|
// ==================== 无认证 API 路由注册 ====================
|
|
|
|
apiNoAuth (method: HttpMethod, routePath: string, handler: PluginRequestHandler): void {
|
|
this.apiNoAuthRoutes.push({ method, path: routePath, handler });
|
|
}
|
|
|
|
getNoAuth (routePath: string, handler: PluginRequestHandler): void {
|
|
this.apiNoAuth('get', routePath, handler);
|
|
}
|
|
|
|
postNoAuth (routePath: string, handler: PluginRequestHandler): void {
|
|
this.apiNoAuth('post', routePath, handler);
|
|
}
|
|
|
|
putNoAuth (routePath: string, handler: PluginRequestHandler): void {
|
|
this.apiNoAuth('put', routePath, handler);
|
|
}
|
|
|
|
deleteNoAuth (routePath: string, handler: PluginRequestHandler): void {
|
|
this.apiNoAuth('delete', routePath, handler);
|
|
}
|
|
|
|
// ==================== 页面注册 ====================
|
|
|
|
page (pageDef: PluginPageDefinition): void {
|
|
this.pageDefinitions.push(pageDef);
|
|
}
|
|
|
|
pages (pageDefs: PluginPageDefinition[]): void {
|
|
this.pageDefinitions.push(...pageDefs);
|
|
}
|
|
|
|
// ==================== 静态资源 ====================
|
|
|
|
static (urlPath: string, localPath: string): void {
|
|
// 如果是相对路径,则相对于插件目录
|
|
const absolutePath = path.isAbsolute(localPath)
|
|
? localPath
|
|
: path.join(this.pluginPath, localPath);
|
|
this.staticRoutes.push({ urlPath, localPath: absolutePath });
|
|
}
|
|
|
|
staticOnMem (urlPath: string, files: MemoryStaticFile[]): void {
|
|
this.memoryStaticRoutes.push({ urlPath, files });
|
|
}
|
|
|
|
// ==================== 构建路由 ====================
|
|
|
|
/**
|
|
* 构建 Express Router(用于 API 路由)
|
|
* 注意:静态资源路由不在此处挂载,由 webui-backend 直接在不需要鉴权的路径下处理
|
|
*/
|
|
buildApiRouter (): Router {
|
|
const router = Router();
|
|
|
|
// 注册 API 路由
|
|
for (const route of this.apiRoutes) {
|
|
const handler = this.wrapHandler(route.handler);
|
|
switch (route.method) {
|
|
case 'get':
|
|
router.get(route.path, handler);
|
|
break;
|
|
case 'post':
|
|
router.post(route.path, handler);
|
|
break;
|
|
case 'put':
|
|
router.put(route.path, handler);
|
|
break;
|
|
case 'delete':
|
|
router.delete(route.path, handler);
|
|
break;
|
|
case 'patch':
|
|
router.patch(route.path, handler);
|
|
break;
|
|
case 'all':
|
|
router.all(route.path, handler);
|
|
break;
|
|
}
|
|
}
|
|
|
|
return router;
|
|
}
|
|
|
|
/**
|
|
* 包装处理器,添加错误处理和请求/响应包装
|
|
*/
|
|
private wrapHandler (handler: PluginRequestHandler): (req: Request, res: Response, next: NextFunction) => void {
|
|
return async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const wrappedReq = wrapRequest(req);
|
|
const wrappedRes = wrapResponse(res);
|
|
await handler(wrappedReq, wrappedRes, next);
|
|
} catch (error: any) {
|
|
console.error(`[Plugin: ${this.pluginId}] Route error:`, error);
|
|
if (!res.headersSent) {
|
|
res.status(500).json({
|
|
code: -1,
|
|
message: `Plugin error: ${error.message || 'Unknown error'}`,
|
|
});
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
// ==================== 查询方法 ====================
|
|
|
|
/**
|
|
* 检查是否有注册的 API 路由(需要认证)
|
|
*/
|
|
hasApiRoutes (): boolean {
|
|
return this.apiRoutes.length > 0;
|
|
}
|
|
|
|
/**
|
|
* 检查是否有注册的无认证 API 路由
|
|
*/
|
|
hasApiNoAuthRoutes (): boolean {
|
|
return this.apiNoAuthRoutes.length > 0;
|
|
}
|
|
|
|
/**
|
|
* 构建无认证 Express Router(用于 /plugin/{pluginId}/api/ 路径)
|
|
*/
|
|
buildApiNoAuthRouter (): Router {
|
|
const router = Router();
|
|
|
|
for (const route of this.apiNoAuthRoutes) {
|
|
const handler = this.wrapHandler(route.handler);
|
|
switch (route.method) {
|
|
case 'get':
|
|
router.get(route.path, handler);
|
|
break;
|
|
case 'post':
|
|
router.post(route.path, handler);
|
|
break;
|
|
case 'put':
|
|
router.put(route.path, handler);
|
|
break;
|
|
case 'delete':
|
|
router.delete(route.path, handler);
|
|
break;
|
|
case 'patch':
|
|
router.patch(route.path, handler);
|
|
break;
|
|
case 'all':
|
|
router.all(route.path, handler);
|
|
break;
|
|
}
|
|
}
|
|
|
|
return router;
|
|
}
|
|
|
|
/**
|
|
* 检查是否有注册的静态资源路由
|
|
*/
|
|
hasStaticRoutes (): boolean {
|
|
return this.staticRoutes.length > 0 || this.memoryStaticRoutes.length > 0;
|
|
}
|
|
|
|
/**
|
|
* 检查是否有注册的页面
|
|
*/
|
|
hasPages (): boolean {
|
|
return this.pageDefinitions.length > 0;
|
|
}
|
|
|
|
/**
|
|
* 获取所有注册的页面定义
|
|
*/
|
|
getPages (): PluginPageDefinition[] {
|
|
return [...this.pageDefinitions];
|
|
}
|
|
|
|
/**
|
|
* 获取插件 ID
|
|
*/
|
|
getPluginId (): string {
|
|
return this.pluginId;
|
|
}
|
|
|
|
/**
|
|
* 获取插件路径
|
|
*/
|
|
getPluginPath (): string {
|
|
return this.pluginPath;
|
|
}
|
|
|
|
/**
|
|
* 获取所有注册的静态路由
|
|
*/
|
|
getStaticRoutes (): Array<{ urlPath: string; localPath: string; }> {
|
|
return [...this.staticRoutes];
|
|
}
|
|
|
|
/**
|
|
* 获取所有注册的内存静态路由
|
|
*/
|
|
getMemoryStaticRoutes (): MemoryStaticRoute[] {
|
|
return [...this.memoryStaticRoutes];
|
|
}
|
|
|
|
/**
|
|
* 清空路由(用于插件卸载)
|
|
*/
|
|
clear (): void {
|
|
this.apiRoutes = [];
|
|
this.apiNoAuthRoutes = [];
|
|
this.pageDefinitions = [];
|
|
this.staticRoutes = [];
|
|
this.memoryStaticRoutes = [];
|
|
}
|
|
}
|