mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-02-04 14:41:14 +00:00
Corrected the type definitions for user_id and target_id to only allow strings, and fixed a syntax error in group_id. This ensures payload validation is consistent and accurate. Refactor fileset ID API response and schema handling Updated GetFilesetId action to return a structured object with fileset_id and adjusted its return schema accordingly. Improved frontend TypeBox schema parsing to support allOf (intersection) merging and updated API debug component to construct response schemas in a more robust way for object recognition. Refactor OneBot API schema handling to use TypeBox Replaces Zod-based static API schema definitions with dynamic fetching of schemas from the backend using TypeBox. Removes legacy static schema files, updates frontend API debug components to use TypeBox utilities, and adds @sinclair/typebox as a dependency. Backend now exposes a /schemas endpoint for all OneBot actions. Various schema and description fields are updated for clarity and consistency.
452 lines
15 KiB
TypeScript
452 lines
15 KiB
TypeScript
import { Router, Request, Response } from 'express';
|
||
import { WebSocket, WebSocketServer, RawData } from 'ws';
|
||
import { sendError, sendSuccess } from '@/napcat-webui-backend/src/utils/response';
|
||
import { WebUiDataRuntime } from '@/napcat-webui-backend/src/helper/Data';
|
||
import { IncomingMessage } from 'http';
|
||
import { OB11Response } from '@/napcat-onebot/action/OneBotAction';
|
||
import { ActionName } from '@/napcat-onebot/action/router';
|
||
import { OB11LifeCycleEvent, LifeCycleSubType } from '@/napcat-onebot/event/meta/OB11LifeCycleEvent';
|
||
import { IOB11NetworkAdapter } from '@/napcat-onebot/network/adapter';
|
||
import { WebsocketServerConfig } from '@/napcat-onebot/config/config';
|
||
import { ActionMap } from '@/napcat-onebot/action';
|
||
import { NapCatCore } from '@/napcat-core/index';
|
||
import { NapCatOneBot11Adapter } from '@/napcat-onebot/index';
|
||
import { OB11EmitEventContent, OB11NetworkReloadType } from '@/napcat-onebot/network/index';
|
||
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||
import json5 from 'json5';
|
||
|
||
type ActionNameType = typeof ActionName[keyof typeof ActionName];
|
||
|
||
const router: Router = Router();
|
||
const DEFAULT_ADAPTER_NAME = 'debug-primary';
|
||
|
||
/**
|
||
* 获取所有 Action 的 Schema 信息
|
||
*/
|
||
router.get('/schemas', async (_req: Request, res: Response) => {
|
||
try {
|
||
const obContext = WebUiDataRuntime.getOneBotContext();
|
||
if (!obContext) {
|
||
return sendError(res, 'OneBot 未初始化');
|
||
}
|
||
const schemas: Record<string, any> = {};
|
||
|
||
// 遍历 ActionName 中定义的所有路由
|
||
for (const key in ActionName) {
|
||
const actionName = (ActionName as any)[key];
|
||
if (actionName === ActionName.Unknown) continue;
|
||
|
||
const handler = obContext.actions.get(actionName);
|
||
if (handler) {
|
||
const action = handler as OneBotAction<unknown, unknown>;
|
||
schemas[actionName] = {
|
||
description: action.actionSummary || action.actionDescription,
|
||
payload: action.payloadSchema,
|
||
response: action.returnSchema,
|
||
payloadExample: action.payloadExample,
|
||
};
|
||
}
|
||
}
|
||
|
||
sendSuccess(res, schemas);
|
||
} catch (error: unknown) {
|
||
sendError(res, (error as Error).message);
|
||
}
|
||
});
|
||
|
||
/**
|
||
* 统一的调试适配器
|
||
* 用于注入到 OneBot NetworkManager,接收所有事件并转发给 WebSocket 客户端
|
||
*/
|
||
class DebugAdapter extends IOB11NetworkAdapter<WebsocketServerConfig> {
|
||
readonly token: string;
|
||
wsClients: WebSocket[] = [];
|
||
wsClientWithEvent: WebSocket[] = [];
|
||
lastActivityTime: number = Date.now();
|
||
inactivityTimer: NodeJS.Timeout | null = null;
|
||
readonly INACTIVITY_TIMEOUT = 5 * 60 * 1000; // 5分钟不活跃
|
||
|
||
override get isActive (): boolean {
|
||
return this.isEnable && this.wsClientWithEvent.length > 0;
|
||
}
|
||
|
||
constructor (sessionId: string, core: NapCatCore, obContext: NapCatOneBot11Adapter, actions: ActionMap) {
|
||
const config: WebsocketServerConfig = {
|
||
enable: true,
|
||
name: `debug-${sessionId}`,
|
||
host: '127.0.0.1',
|
||
port: 0,
|
||
messagePostFormat: 'array',
|
||
reportSelfMessage: true,
|
||
token: '',
|
||
enableForcePushEvent: true,
|
||
debug: true,
|
||
heartInterval: 0
|
||
};
|
||
|
||
super(`debug-${sessionId}`, config, core, obContext, actions);
|
||
this.token = Math.random().toString(36).substring(2) + Math.random().toString(36).substring(2);
|
||
this.isEnable = false;
|
||
this.startInactivityCheck();
|
||
}
|
||
|
||
async open (): Promise<void> {
|
||
if (this.isEnable) {
|
||
this.logger.logError('[Debug] Cannot open an already opened adapter');
|
||
return;
|
||
}
|
||
this.logger.log('[Debug] Adapter opened:', this.name);
|
||
this.isEnable = true;
|
||
}
|
||
|
||
async close (): Promise<void> {
|
||
if (!this.isEnable) {
|
||
return;
|
||
}
|
||
this.logger.log('[Debug] Adapter closing:', this.name);
|
||
this.isEnable = false;
|
||
|
||
// 停止不活跃检查定时器
|
||
if (this.inactivityTimer) {
|
||
clearInterval(this.inactivityTimer);
|
||
this.inactivityTimer = null;
|
||
}
|
||
|
||
// 关闭所有 WebSocket 连接并移除事件监听器
|
||
this.wsClients.forEach((client) => {
|
||
try {
|
||
client.removeAllListeners();
|
||
client.close();
|
||
} catch (error) {
|
||
this.logger.logError('[Debug] 关闭 WebSocket 失败:', error);
|
||
}
|
||
});
|
||
this.wsClients = [];
|
||
this.wsClientWithEvent = [];
|
||
}
|
||
|
||
async reload (_config: unknown): Promise<OB11NetworkReloadType> {
|
||
return OB11NetworkReloadType.NetWorkReload;
|
||
}
|
||
|
||
async onEvent<T extends OB11EmitEventContent> (event: T): Promise<void> {
|
||
this.updateActivity();
|
||
|
||
const payload = JSON.stringify(event);
|
||
this.wsClientWithEvent.forEach((wsClient) => {
|
||
if (wsClient.readyState === WebSocket.OPEN) {
|
||
try {
|
||
wsClient.send(payload);
|
||
} catch (error) {
|
||
this.logger.logError('[Debug] 发送事件失败:', error);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
async callApi (actionName: ActionNameType, params: Record<string, unknown>): Promise<unknown> {
|
||
this.updateActivity();
|
||
|
||
const action = this.actions.get(actionName as Parameters<typeof this.actions.get>[0]);
|
||
if (!action) {
|
||
throw new Error(`不支持的 API: ${actionName}`);
|
||
}
|
||
|
||
type ActionHandler = { handle: (params: unknown, ...args: unknown[]) => Promise<unknown>; };
|
||
return await (action as ActionHandler).handle(params, this.name, this.config);
|
||
}
|
||
|
||
private async handleMessage (wsClient: WebSocket, message: RawData): Promise<void> {
|
||
this.updateActivity();
|
||
let receiveData: { action: ActionNameType, params?: Record<string, unknown>, echo?: unknown; } = {
|
||
action: ActionName.Unknown,
|
||
params: {}
|
||
};
|
||
let echo: unknown = undefined;
|
||
|
||
try {
|
||
receiveData = json5.parse(message.toString());
|
||
echo = receiveData.echo;
|
||
} catch {
|
||
this.sendToClient(wsClient, OB11Response.error('json解析失败,请检查数据格式', 1400, echo));
|
||
return;
|
||
}
|
||
|
||
receiveData.params = receiveData?.params || {};
|
||
|
||
const action = this.actions.get(receiveData.action as Parameters<typeof this.actions.get>[0]);
|
||
if (!action) {
|
||
this.logger.logError('[Debug] 不支持的API:', receiveData.action);
|
||
this.sendToClient(wsClient, OB11Response.error('不支持的API ' + receiveData.action, 1404, echo));
|
||
return;
|
||
}
|
||
|
||
try {
|
||
type ActionHandler = { websocketHandle: (params: unknown, ...args: unknown[]) => Promise<unknown>; };
|
||
const retdata = await (action as ActionHandler).websocketHandle(receiveData.params, echo ?? '', this.name, this.config, {
|
||
send: async (data: object) => {
|
||
this.sendToClient(wsClient, OB11Response.ok(data, echo ?? '', true));
|
||
},
|
||
});
|
||
this.sendToClient(wsClient, retdata);
|
||
} catch (e: unknown) {
|
||
const error = e as Error;
|
||
this.logger.logError('[Debug] 处理消息失败:', error);
|
||
this.sendToClient(wsClient, OB11Response.error(error.message || '内部错误', 1200, echo));
|
||
}
|
||
}
|
||
|
||
private sendToClient (wsClient: WebSocket, data: unknown): void {
|
||
if (wsClient.readyState === WebSocket.OPEN) {
|
||
try {
|
||
wsClient.send(JSON.stringify(data));
|
||
} catch (error) {
|
||
this.logger.logError('[Debug] 发送消息失败:', error);
|
||
}
|
||
}
|
||
}
|
||
|
||
async addWsClient (ws: WebSocket): Promise<void> {
|
||
this.wsClientWithEvent.push(ws);
|
||
this.wsClients.push(ws);
|
||
this.updateActivity();
|
||
|
||
// 发送连接事件
|
||
this.sendToClient(ws, new OB11LifeCycleEvent(this.core, LifeCycleSubType.CONNECT));
|
||
|
||
ws.on('error', (err) => this.logger.log('[Debug] WebSocket Error:', err.message));
|
||
ws.on('message', (message) => {
|
||
this.handleMessage(ws, message).catch((e: unknown) => {
|
||
this.logger.logError('[Debug] handleMessage error:', e);
|
||
});
|
||
});
|
||
ws.on('ping', () => ws.pong());
|
||
ws.once('close', () => this.removeWsClient(ws));
|
||
}
|
||
|
||
private removeWsClient (ws: WebSocket): void {
|
||
const normalIndex = this.wsClients.indexOf(ws);
|
||
if (normalIndex !== -1) {
|
||
this.wsClients.splice(normalIndex, 1);
|
||
}
|
||
const eventIndex = this.wsClientWithEvent.indexOf(ws);
|
||
if (eventIndex !== -1) {
|
||
this.wsClientWithEvent.splice(eventIndex, 1);
|
||
}
|
||
}
|
||
|
||
updateActivity (): void {
|
||
this.lastActivityTime = Date.now();
|
||
}
|
||
|
||
startInactivityCheck (): void {
|
||
this.inactivityTimer = setInterval(() => {
|
||
const inactive = Date.now() - this.lastActivityTime;
|
||
if (inactive > this.INACTIVITY_TIMEOUT && this.wsClients.length === 0) {
|
||
this.logger.log(`[Debug] Adapter ${this.name} 不活跃,自动关闭`);
|
||
const oneBotContext = WebUiDataRuntime.getOneBotContext();
|
||
if (oneBotContext) {
|
||
// 先从管理器移除,避免重复销毁
|
||
debugAdapterManager.removeAdapter(this.name);
|
||
// 使用 NetworkManager 标准流程关闭
|
||
oneBotContext.networkManager.closeSomeAdapters([this]).catch((e: unknown) => {
|
||
this.logger.logError('[Debug] 自动关闭适配器失败:', e);
|
||
});
|
||
}
|
||
}
|
||
}, 30000);
|
||
}
|
||
|
||
validateToken (inputToken: string): boolean {
|
||
return this.token === inputToken;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 调试适配器管理器(单例管理)
|
||
*/
|
||
class DebugAdapterManager {
|
||
private currentAdapter: DebugAdapter | null = null;
|
||
|
||
getOrCreateAdapter (): DebugAdapter {
|
||
// 如果已存在且活跃,直接返回
|
||
if (this.currentAdapter) {
|
||
this.currentAdapter.updateActivity();
|
||
return this.currentAdapter;
|
||
}
|
||
|
||
// 获取 OneBot 上下文
|
||
const oneBotContext = WebUiDataRuntime.getOneBotContext();
|
||
if (!oneBotContext) {
|
||
throw new Error('OneBot 未初始化,无法创建调试适配器');
|
||
}
|
||
|
||
// 创建新实例
|
||
const adapter = new DebugAdapter('primary', oneBotContext.core, oneBotContext, oneBotContext.actions);
|
||
this.currentAdapter = adapter;
|
||
|
||
// 使用 NetworkManager 标准流程注册并打开适配器
|
||
oneBotContext.networkManager.registerAdapterAndOpen(adapter).catch((e: unknown) => {
|
||
console.error('[Debug] 注册适配器失败:', e);
|
||
});
|
||
|
||
return adapter;
|
||
}
|
||
|
||
getAdapter (name: string): DebugAdapter | undefined {
|
||
if (this.currentAdapter && this.currentAdapter.name === name) {
|
||
return this.currentAdapter;
|
||
}
|
||
return undefined;
|
||
}
|
||
|
||
removeAdapter (name: string) {
|
||
if (this.currentAdapter && this.currentAdapter.name === name) {
|
||
this.currentAdapter = null;
|
||
}
|
||
}
|
||
}
|
||
|
||
const debugAdapterManager = new DebugAdapterManager();
|
||
|
||
/**
|
||
* 获取或创建调试会话
|
||
*/
|
||
router.post('/create', async (_req: Request, res: Response) => {
|
||
try {
|
||
const adapter = debugAdapterManager.getOrCreateAdapter();
|
||
sendSuccess(res, {
|
||
adapterName: adapter.name,
|
||
token: adapter.token,
|
||
message: '调试适配器已就绪',
|
||
});
|
||
} catch (error: unknown) {
|
||
const err = error as Error;
|
||
sendError(res, err.message);
|
||
}
|
||
});
|
||
|
||
/**
|
||
* HTTP 调用 OneBot API (支持默认 adapter)
|
||
*/
|
||
const handleCallApi = async (req: Request, res: Response) => {
|
||
try {
|
||
let adapterName = req.params['adapterName'] || req.body.adapterName || DEFAULT_ADAPTER_NAME;
|
||
|
||
let adapter = debugAdapterManager.getAdapter(adapterName);
|
||
|
||
// 如果是默认 adapter 且不存在,尝试创建
|
||
if (!adapter && adapterName === DEFAULT_ADAPTER_NAME) {
|
||
adapter = debugAdapterManager.getOrCreateAdapter();
|
||
}
|
||
|
||
if (!adapter) {
|
||
return sendError(res, '调试适配器不存在');
|
||
}
|
||
|
||
const { action, params } = req.body;
|
||
const result = await adapter.callApi(action as ActionNameType, params || {});
|
||
sendSuccess(res, result);
|
||
} catch (error: unknown) {
|
||
const err = error as Error;
|
||
sendError(res, err.message);
|
||
}
|
||
};
|
||
|
||
router.post('/call/:adapterName', handleCallApi);
|
||
router.post('/call', handleCallApi);
|
||
|
||
/**
|
||
* 关闭调试适配器
|
||
*/
|
||
router.post('/close/:adapterName', async (req: Request, res: Response) => {
|
||
try {
|
||
const { adapterName } = req.params;
|
||
if (!adapterName) {
|
||
return sendError(res, '缺少 adapterName 参数');
|
||
}
|
||
|
||
const adapter = debugAdapterManager.getAdapter(adapterName);
|
||
if (!adapter) {
|
||
return sendError(res, '调试适配器不存在');
|
||
}
|
||
|
||
// 先从管理器移除,避免重复销毁
|
||
debugAdapterManager.removeAdapter(adapterName);
|
||
|
||
// 使用 NetworkManager 标准流程关闭适配器
|
||
const oneBotContext = WebUiDataRuntime.getOneBotContext();
|
||
if (oneBotContext) {
|
||
await oneBotContext.networkManager.closeSomeAdapters([adapter]);
|
||
}
|
||
|
||
sendSuccess(res, { message: '调试适配器已关闭' });
|
||
} catch (error: unknown) {
|
||
const err = error as Error;
|
||
sendError(res, err.message);
|
||
}
|
||
});
|
||
|
||
/**
|
||
* WebSocket 连接处理
|
||
* 路径: /api/Debug/ws?adapterName=xxx&token=xxx
|
||
*/
|
||
export function handleDebugWebSocket (request: IncomingMessage, socket: unknown, head: unknown) {
|
||
const url = new URL(request.url || '', `http://${request.headers.host}`);
|
||
let adapterName = url.searchParams.get('adapterName');
|
||
const token = url.searchParams.get('token') || url.searchParams.get('access_token');
|
||
|
||
// 默认 adapterName
|
||
if (!adapterName) {
|
||
adapterName = DEFAULT_ADAPTER_NAME;
|
||
}
|
||
|
||
// Debug session should provide token
|
||
if (!token) {
|
||
console.log('[Debug] WebSocket 连接被拒绝: 缺少 Token');
|
||
(socket as { write: (data: string) => void; destroy: () => void; }).write('HTTP/1.1 401 Unauthorized\r\n\r\n');
|
||
(socket as { destroy: () => void; }).destroy();
|
||
return;
|
||
}
|
||
|
||
let adapter = debugAdapterManager.getAdapter(adapterName);
|
||
|
||
// 如果是默认 adapter 且不存在,尝试创建
|
||
if (!adapter && adapterName === DEFAULT_ADAPTER_NAME) {
|
||
try {
|
||
adapter = debugAdapterManager.getOrCreateAdapter();
|
||
} catch (error) {
|
||
console.log('[Debug] WebSocket 连接被拒绝: 无法创建适配器', error);
|
||
(socket as { write: (data: string) => void; destroy: () => void; }).write('HTTP/1.1 500 Internal Server Error\r\n\r\n');
|
||
(socket as { destroy: () => void; }).destroy();
|
||
return;
|
||
}
|
||
}
|
||
|
||
if (!adapter) {
|
||
console.log('[Debug] WebSocket 连接被拒绝: 适配器不存在');
|
||
(socket as { write: (data: string) => void; destroy: () => void; }).write('HTTP/1.1 404 Not Found\r\n\r\n');
|
||
(socket as { destroy: () => void; }).destroy();
|
||
return;
|
||
}
|
||
|
||
if (!adapter.validateToken(token)) {
|
||
console.log('[Debug] WebSocket 连接被拒绝: Token 无效');
|
||
(socket as { write: (data: string) => void; destroy: () => void; }).write('HTTP/1.1 403 Forbidden\r\n\r\n');
|
||
(socket as { destroy: () => void; }).destroy();
|
||
return;
|
||
}
|
||
|
||
// 创建 WebSocket 服务器
|
||
const wsServer = new WebSocketServer({ noServer: true });
|
||
|
||
wsServer.handleUpgrade(request, socket as never, head as Buffer, (ws) => {
|
||
adapter.addWsClient(ws).catch((e: unknown) => {
|
||
console.error('[Debug] 添加 WebSocket 客户端失败:', e);
|
||
ws.close();
|
||
});
|
||
});
|
||
}
|
||
|
||
export default router;
|