mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-01-18 14:30:29 +00:00
feat: 支持免配置调试
This commit is contained in:
parent
649165bf00
commit
578dda2f17
@ -79,7 +79,10 @@ export async function NCoreInitFramework (
|
|||||||
WebUiDataRuntime.setWorkingEnv(NapCatCoreWorkingEnv.Framework);
|
WebUiDataRuntime.setWorkingEnv(NapCatCoreWorkingEnv.Framework);
|
||||||
InitWebUi(logger, pathWrapper, logSubscription, statusHelperSubscription).then().catch(e => logger.logError(e));
|
InitWebUi(logger, pathWrapper, logSubscription, statusHelperSubscription).then().catch(e => logger.logError(e));
|
||||||
// 初始化LLNC的Onebot实现
|
// 初始化LLNC的Onebot实现
|
||||||
await new NapCatOneBot11Adapter(loaderObject.core, loaderObject.context, pathWrapper).InitOneBot();
|
const oneBotAdapter = new NapCatOneBot11Adapter(loaderObject.core, loaderObject.context, pathWrapper);
|
||||||
|
// 注册到 WebUiDataRuntime,供调试功能使用
|
||||||
|
WebUiDataRuntime.setOneBotContext(oneBotAdapter);
|
||||||
|
await oneBotAdapter.InitOneBot();
|
||||||
}
|
}
|
||||||
|
|
||||||
export class NapCatFramework {
|
export class NapCatFramework {
|
||||||
|
|||||||
@ -455,7 +455,11 @@ export class NapCatShell {
|
|||||||
|
|
||||||
async InitNapCat () {
|
async InitNapCat () {
|
||||||
await this.core.initCore();
|
await this.core.initCore();
|
||||||
new NapCatOneBot11Adapter(this.core, this.context, this.context.pathWrapper).InitOneBot()
|
const oneBotAdapter = new NapCatOneBot11Adapter(this.core, this.context, this.context.pathWrapper);
|
||||||
|
// 注册到 WebUiDataRuntime,供调试功能使用
|
||||||
|
WebUiDataRuntime.setOneBotContext(oneBotAdapter);
|
||||||
|
oneBotAdapter.InitOneBot()
|
||||||
.catch(e => this.context.logger.logError('初始化OneBot失败', e));
|
.catch(e => this.context.logger.logError('初始化OneBot失败', e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -22,6 +22,7 @@ import { existsSync, readFileSync } from 'node:fs'; // 引入multer用于错误
|
|||||||
import { ILogWrapper } from 'napcat-common/src/log-interface';
|
import { ILogWrapper } from 'napcat-common/src/log-interface';
|
||||||
import { ISubscription } from 'napcat-common/src/subscription-interface';
|
import { ISubscription } from 'napcat-common/src/subscription-interface';
|
||||||
import { IStatusHelperSubscription } from '@/napcat-common/src/status-interface';
|
import { IStatusHelperSubscription } from '@/napcat-common/src/status-interface';
|
||||||
|
import { handleDebugWebSocket } from '@/napcat-webui-backend/src/api/Debug';
|
||||||
// 实例化Express
|
// 实例化Express
|
||||||
const app = express();
|
const app = express();
|
||||||
/**
|
/**
|
||||||
@ -187,7 +188,15 @@ export async function InitWebUi (logger: ILogWrapper, pathWrapper: NapCatPathWra
|
|||||||
const isHttps = !!sslCerts;
|
const isHttps = !!sslCerts;
|
||||||
const server = isHttps && sslCerts ? createHttpsServer(sslCerts, app) : createServer(app);
|
const server = isHttps && sslCerts ? createHttpsServer(sslCerts, app) : createServer(app);
|
||||||
server.on('upgrade', (request, socket, head) => {
|
server.on('upgrade', (request, socket, head) => {
|
||||||
terminalManager.initialize(request, socket, head, logger);
|
const url = new URL(request.url || '', `http://${request.headers.host}`);
|
||||||
|
|
||||||
|
// 检查是否是调试 WebSocket 连接
|
||||||
|
if (url.pathname.startsWith('/api/Debug/ws')) {
|
||||||
|
handleDebugWebSocket(request, socket, head);
|
||||||
|
} else {
|
||||||
|
// 默认为终端 WebSocket
|
||||||
|
terminalManager.initialize(request, socket, head, logger);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
// 挂载API接口
|
// 挂载API接口
|
||||||
app.use('/api', ALLRouter);
|
app.use('/api', ALLRouter);
|
||||||
|
|||||||
410
packages/napcat-webui-backend/src/api/Debug.ts
Normal file
410
packages/napcat-webui-backend/src/api/Debug.ts
Normal file
@ -0,0 +1,410 @@
|
|||||||
|
import { Router, Request, Response } from 'express';
|
||||||
|
import { WebSocket, WebSocketServer } 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';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
const DEFAULT_ADAPTER_NAME = 'debug-primary';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统一的调试适配器
|
||||||
|
* 用于注入到 OneBot NetworkManager,接收所有事件并转发给 WebSocket 客户端
|
||||||
|
*/
|
||||||
|
class DebugAdapter {
|
||||||
|
name: string;
|
||||||
|
isEnable: boolean = true;
|
||||||
|
// 安全令牌
|
||||||
|
readonly token: string;
|
||||||
|
|
||||||
|
// 添加 config 属性,模拟 PluginConfig 结构
|
||||||
|
config: {
|
||||||
|
enable: boolean;
|
||||||
|
name: string;
|
||||||
|
messagePostFormat?: string;
|
||||||
|
reportSelfMessage?: boolean;
|
||||||
|
debug?: boolean;
|
||||||
|
token?: string;
|
||||||
|
heartInterval?: number;
|
||||||
|
};
|
||||||
|
wsClients: Set<WebSocket> = new Set();
|
||||||
|
lastActivityTime: number = Date.now();
|
||||||
|
inactivityTimer: NodeJS.Timeout | null = null;
|
||||||
|
readonly INACTIVITY_TIMEOUT = 5 * 60 * 1000; // 5分钟不活跃
|
||||||
|
|
||||||
|
constructor (sessionId: string) {
|
||||||
|
this.name = `debug-${sessionId}`;
|
||||||
|
// 生成简单的随机 token
|
||||||
|
this.token = Math.random().toString(36).substring(2) + Math.random().toString(36).substring(2);
|
||||||
|
|
||||||
|
this.config = {
|
||||||
|
enable: true,
|
||||||
|
name: this.name,
|
||||||
|
messagePostFormat: 'array',
|
||||||
|
reportSelfMessage: true,
|
||||||
|
debug: true,
|
||||||
|
token: this.token,
|
||||||
|
heartInterval: 30000
|
||||||
|
};
|
||||||
|
this.startInactivityCheck();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 实现 IOB11NetworkAdapter 接口所需的抽象方法
|
||||||
|
async open (): Promise<void> { }
|
||||||
|
async close (): Promise<void> { this.cleanup(); }
|
||||||
|
async reload (_config: any): Promise<any> { return 0; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OneBot 事件回调 - 转发给所有 WebSocket 客户端 (原始流)
|
||||||
|
*/
|
||||||
|
async onEvent (event: any) {
|
||||||
|
this.updateActivity();
|
||||||
|
console.log(`[Debug] Adapter ${this.name} 收到事件, 类型: ${event.post_type || 'unknown'}, 客户端数: ${this.wsClients.size}`);
|
||||||
|
|
||||||
|
const payload = JSON.stringify(event);
|
||||||
|
|
||||||
|
if (this.wsClients.size === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.wsClients.forEach((client) => {
|
||||||
|
if (client.readyState === WebSocket.OPEN) {
|
||||||
|
try {
|
||||||
|
client.send(payload);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Debug] 发送事件到 WebSocket 失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调用 OneBot API (HTTP 接口使用)
|
||||||
|
*/
|
||||||
|
async callApi (actionName: string, params: any): Promise<any> {
|
||||||
|
this.updateActivity();
|
||||||
|
|
||||||
|
const oneBotContext = WebUiDataRuntime.getOneBotContext();
|
||||||
|
if (!oneBotContext) {
|
||||||
|
throw new Error('OneBot 未初始化');
|
||||||
|
}
|
||||||
|
|
||||||
|
const action = oneBotContext.actions.get(actionName);
|
||||||
|
if (!action) {
|
||||||
|
throw new Error(`不支持的 API: ${actionName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await action.handle(params, this.name, {
|
||||||
|
name: this.name,
|
||||||
|
enable: true,
|
||||||
|
messagePostFormat: 'array',
|
||||||
|
reportSelfMessage: true,
|
||||||
|
debug: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理 WebSocket 消息 (OneBot 标准)
|
||||||
|
*/
|
||||||
|
async handleWsMessage (ws: WebSocket, message: string | Buffer) {
|
||||||
|
this.updateActivity();
|
||||||
|
let receiveData: { action: typeof ActionName[keyof typeof ActionName], params?: any, echo?: any; } = { action: ActionName.Unknown, params: {} };
|
||||||
|
let echo;
|
||||||
|
|
||||||
|
try {
|
||||||
|
receiveData = JSON.parse(message.toString());
|
||||||
|
echo = receiveData.echo;
|
||||||
|
} catch {
|
||||||
|
this.sendWsResponse(ws, OB11Response.error('json解析失败,请检查数据格式', 1400, echo));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
receiveData.params = (receiveData?.params) ? receiveData.params : {};
|
||||||
|
|
||||||
|
// 兼容 WebUI 之前可能的一些非标准格式 (如果用户是旧前端)
|
||||||
|
// 但既然用户说要"原始流",我们优先支持标准格式
|
||||||
|
|
||||||
|
const oneBotContext = WebUiDataRuntime.getOneBotContext();
|
||||||
|
if (!oneBotContext) {
|
||||||
|
this.sendWsResponse(ws, OB11Response.error('OneBot 未初始化', 1404, echo));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const action = oneBotContext.actions.get(receiveData.action as any);
|
||||||
|
if (!action) {
|
||||||
|
this.sendWsResponse(ws, OB11Response.error('不支持的API ' + receiveData.action, 1404, echo));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const retdata = await action.websocketHandle(receiveData.params, echo ?? '', this.name, this.config, {
|
||||||
|
send: async (data: object) => {
|
||||||
|
this.sendWsResponse(ws, OB11Response.ok(data, echo ?? '', true));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.sendWsResponse(ws, retdata);
|
||||||
|
} catch (e: any) {
|
||||||
|
this.sendWsResponse(ws, OB11Response.error(e.message || '内部错误', 1200, echo));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sendWsResponse (ws: WebSocket, data: any) {
|
||||||
|
if (ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(JSON.stringify(data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加 WebSocket 客户端
|
||||||
|
*/
|
||||||
|
addWsClient (ws: WebSocket) {
|
||||||
|
this.wsClients.add(ws);
|
||||||
|
this.updateActivity();
|
||||||
|
console.log(`[Debug] WebSocket 客户端已连接 (${this.wsClients.size})`);
|
||||||
|
|
||||||
|
// 发送生命周期事件 (Connect)
|
||||||
|
const oneBotContext = WebUiDataRuntime.getOneBotContext();
|
||||||
|
if (oneBotContext && oneBotContext.core) {
|
||||||
|
try {
|
||||||
|
const event = new OB11LifeCycleEvent(oneBotContext.core, LifeCycleSubType.CONNECT);
|
||||||
|
ws.send(JSON.stringify(event));
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[Debug] 发送生命周期事件失败', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除 WebSocket 客户端
|
||||||
|
*/
|
||||||
|
removeWsClient (ws: WebSocket) {
|
||||||
|
this.wsClients.delete(ws);
|
||||||
|
console.log(`[Debug] WebSocket 客户端已断开 (${this.wsClients.size})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateActivity () {
|
||||||
|
this.lastActivityTime = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
startInactivityCheck () {
|
||||||
|
this.inactivityTimer = setInterval(() => {
|
||||||
|
const inactive = Date.now() - this.lastActivityTime;
|
||||||
|
// 如果没有 WebSocket 连接且超时,则自动清理
|
||||||
|
if (inactive > this.INACTIVITY_TIMEOUT && this.wsClients.size === 0) {
|
||||||
|
console.log(`[Debug] Adapter ${this.name} 不活跃,自动关闭`);
|
||||||
|
this.cleanup();
|
||||||
|
}
|
||||||
|
}, 30000);
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup () {
|
||||||
|
if (this.inactivityTimer) {
|
||||||
|
clearInterval(this.inactivityTimer);
|
||||||
|
this.inactivityTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭所有 WebSocket 连接
|
||||||
|
this.wsClients.forEach((client) => {
|
||||||
|
try {
|
||||||
|
client.close();
|
||||||
|
} catch (error) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.wsClients.clear();
|
||||||
|
|
||||||
|
// 从 OneBot NetworkManager 移除
|
||||||
|
const oneBotContext = WebUiDataRuntime.getOneBotContext();
|
||||||
|
if (oneBotContext) {
|
||||||
|
oneBotContext.networkManager.adapters.delete(this.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从管理器中移除
|
||||||
|
debugAdapterManager.removeAdapter(this.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证 Token
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新实例
|
||||||
|
const adapter = new DebugAdapter('primary');
|
||||||
|
this.currentAdapter = adapter;
|
||||||
|
|
||||||
|
// 注册到 OneBot NetworkManager
|
||||||
|
const oneBotContext = WebUiDataRuntime.getOneBotContext();
|
||||||
|
if (oneBotContext) {
|
||||||
|
oneBotContext.networkManager.adapters.set(adapter.name, adapter as any);
|
||||||
|
console.log(`[Debug] 已注册调试适配器: ${adapter.name}, NetworkManager中适配器数量: ${oneBotContext.networkManager.adapters.size}`);
|
||||||
|
} else {
|
||||||
|
console.warn('[Debug] OneBot 未初始化,无法注册适配器');
|
||||||
|
}
|
||||||
|
|
||||||
|
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: any) {
|
||||||
|
sendError(res, error.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, params || {});
|
||||||
|
sendSuccess(res, result);
|
||||||
|
} catch (error: any) {
|
||||||
|
sendError(res, error.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 参数');
|
||||||
|
}
|
||||||
|
debugAdapterManager.removeAdapter(adapterName);
|
||||||
|
sendSuccess(res, { message: '调试适配器已关闭' });
|
||||||
|
} catch (error: any) {
|
||||||
|
sendError(res, error.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WebSocket 连接处理
|
||||||
|
* 路径: /api/Debug/ws?adapterName=xxx&token=xxx
|
||||||
|
*/
|
||||||
|
export function handleDebugWebSocket (request: IncomingMessage, socket: any, head: any) {
|
||||||
|
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.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
|
||||||
|
socket.destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let adapter = debugAdapterManager.getAdapter(adapterName);
|
||||||
|
|
||||||
|
// 如果是默认 adapter 且不存在,尝试创建
|
||||||
|
if (!adapter && adapterName === DEFAULT_ADAPTER_NAME) {
|
||||||
|
adapter = debugAdapterManager.getOrCreateAdapter();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!adapter) {
|
||||||
|
console.log('[Debug] WebSocket 连接被拒绝: 适配器不存在');
|
||||||
|
socket.write('HTTP/1.1 404 Not Found\r\n\r\n');
|
||||||
|
socket.destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!adapter.validateToken(token)) {
|
||||||
|
console.log('[Debug] WebSocket 连接被拒绝: Token 无效');
|
||||||
|
socket.write('HTTP/1.1 403 Forbidden\r\n\r\n');
|
||||||
|
socket.destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建 WebSocket 服务器
|
||||||
|
const wsServer = new WebSocketServer({ noServer: true });
|
||||||
|
|
||||||
|
wsServer.handleUpgrade(request, socket, head, (ws) => {
|
||||||
|
adapter.addWsClient(ws);
|
||||||
|
|
||||||
|
ws.on('message', async (data) => {
|
||||||
|
try {
|
||||||
|
await adapter.handleWsMessage(ws, data as any);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('[Debug] handleWsMessage error', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('close', () => {
|
||||||
|
adapter.removeWsClient(ws);
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('error', () => {
|
||||||
|
adapter.removeWsClient(ws);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default router;
|
||||||
@ -15,6 +15,7 @@ const LoginRuntime: LoginRuntimeType = {
|
|||||||
nick: '',
|
nick: '',
|
||||||
},
|
},
|
||||||
QQVersion: 'unknown',
|
QQVersion: 'unknown',
|
||||||
|
OneBotContext: null,
|
||||||
onQQLoginStatusChange: async (status: boolean) => {
|
onQQLoginStatusChange: async (status: boolean) => {
|
||||||
LoginRuntime.QQLoginStatus = status;
|
LoginRuntime.QQLoginStatus = status;
|
||||||
},
|
},
|
||||||
@ -154,4 +155,12 @@ export const WebUiDataRuntime = {
|
|||||||
runWebUiConfigQuickFunction: async function () {
|
runWebUiConfigQuickFunction: async function () {
|
||||||
await LoginRuntime.WebUiConfigQuickFunction();
|
await LoginRuntime.WebUiConfigQuickFunction();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setOneBotContext (context: any): void {
|
||||||
|
LoginRuntime.OneBotContext = context;
|
||||||
|
},
|
||||||
|
|
||||||
|
getOneBotContext (): any | null {
|
||||||
|
return LoginRuntime.OneBotContext;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import { BaseRouter } from '@/napcat-webui-backend/src/router/Base';
|
|||||||
import { FileRouter } from './File';
|
import { FileRouter } from './File';
|
||||||
import { WebUIConfigRouter } from './WebUIConfig';
|
import { WebUIConfigRouter } from './WebUIConfig';
|
||||||
import { UpdateNapCatRouter } from './UpdateNapCat';
|
import { UpdateNapCatRouter } from './UpdateNapCat';
|
||||||
|
import DebugRouter from '@/napcat-webui-backend/src/api/Debug';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
@ -41,5 +42,7 @@ router.use('/File', FileRouter);
|
|||||||
router.use('/WebUIConfig', WebUIConfigRouter);
|
router.use('/WebUIConfig', WebUIConfigRouter);
|
||||||
// router:更新NapCat相关路由
|
// router:更新NapCat相关路由
|
||||||
router.use('/UpdateNapCat', UpdateNapCatRouter);
|
router.use('/UpdateNapCat', UpdateNapCatRouter);
|
||||||
|
// router:调试相关路由
|
||||||
|
router.use('/Debug', DebugRouter);
|
||||||
|
|
||||||
export { router as ALLRouter };
|
export { router as ALLRouter };
|
||||||
|
|||||||
@ -47,6 +47,7 @@ export interface LoginRuntimeType {
|
|||||||
onQQLoginStatusChange: (status: boolean) => Promise<void>;
|
onQQLoginStatusChange: (status: boolean) => Promise<void>;
|
||||||
onWebUiTokenChange: (token: string) => Promise<void>;
|
onWebUiTokenChange: (token: string) => Promise<void>;
|
||||||
WebUiConfigQuickFunction: () => Promise<void>;
|
WebUiConfigQuickFunction: () => Promise<void>;
|
||||||
|
OneBotContext: any | null; // OneBot 上下文,用于调试功能
|
||||||
NapCatHelper: {
|
NapCatHelper: {
|
||||||
onQuickLoginRequested: (uin: string) => Promise<{ result: boolean; message: string; }>;
|
onQuickLoginRequested: (uin: string) => Promise<{ result: boolean; message: string; }>;
|
||||||
onOB11ConfigChanged: (ob11: OneBotConfig) => Promise<void>;
|
onOB11ConfigChanged: (ob11: OneBotConfig) => Promise<void>;
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { Tab, Tabs } from '@heroui/tabs';
|
|||||||
import { Chip } from '@heroui/chip';
|
import { Chip } from '@heroui/chip';
|
||||||
import { useLocalStorage } from '@uidotdev/usehooks';
|
import { useLocalStorage } from '@uidotdev/usehooks';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState, useCallback } from 'react';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import { IoChevronDown, IoSend, IoSettingsSharp, IoCopy } from 'react-icons/io5';
|
import { IoChevronDown, IoSend, IoSettingsSharp, IoCopy } from 'react-icons/io5';
|
||||||
import { TbCode, TbMessageCode } from 'react-icons/tb';
|
import { TbCode, TbMessageCode } from 'react-icons/tb';
|
||||||
@ -19,7 +19,7 @@ import CodeEditor from '@/components/code_editor';
|
|||||||
import PageLoading from '@/components/page_loading';
|
import PageLoading from '@/components/page_loading';
|
||||||
|
|
||||||
import { request } from '@/utils/request';
|
import { request } from '@/utils/request';
|
||||||
import { parseAxiosResponse } from '@/utils/url';
|
|
||||||
import { generateDefaultJson, parse } from '@/utils/zod';
|
import { generateDefaultJson, parse } from '@/utils/zod';
|
||||||
|
|
||||||
import DisplayStruct from './display_struct';
|
import DisplayStruct from './display_struct';
|
||||||
@ -27,10 +27,11 @@ import DisplayStruct from './display_struct';
|
|||||||
export interface OneBotApiDebugProps {
|
export interface OneBotApiDebugProps {
|
||||||
path: OneBotHttpApiPath;
|
path: OneBotHttpApiPath;
|
||||||
data: OneBotHttpApiContent;
|
data: OneBotHttpApiContent;
|
||||||
|
adapterName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const OneBotApiDebug: React.FC<OneBotApiDebugProps> = (props) => {
|
const OneBotApiDebug: React.FC<OneBotApiDebugProps> = (props) => {
|
||||||
const { path, data } = props;
|
const { path, data, adapterName } = props;
|
||||||
const currentURL = new URL(window.location.origin);
|
const currentURL = new URL(window.location.origin);
|
||||||
currentURL.port = '3000';
|
currentURL.port = '3000';
|
||||||
const defaultHttpUrl = currentURL.href;
|
const defaultHttpUrl = currentURL.href;
|
||||||
@ -38,12 +39,15 @@ const OneBotApiDebug: React.FC<OneBotApiDebugProps> = (props) => {
|
|||||||
url: defaultHttpUrl,
|
url: defaultHttpUrl,
|
||||||
token: '',
|
token: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const [requestBody, setRequestBody] = useState('{}');
|
const [requestBody, setRequestBody] = useState('{}');
|
||||||
const [responseContent, setResponseContent] = useState('');
|
const [responseContent, setResponseContent] = useState('');
|
||||||
const [isFetching, setIsFetching] = useState(false);
|
const [isFetching, setIsFetching] = useState(false);
|
||||||
const [activeTab, setActiveTab] = useState<any>('request');
|
const [activeTab, setActiveTab] = useState<any>('request');
|
||||||
const [responseExpanded, setResponseExpanded] = useState(true);
|
const [responseExpanded, setResponseExpanded] = useState(true);
|
||||||
const [responseStatus, setResponseStatus] = useState<{ code: number; text: string; } | null>(null);
|
const [responseStatus, setResponseStatus] = useState<{ code: number; text: string; } | null>(null);
|
||||||
|
const [responseHeight, setResponseHeight] = useLocalStorage('napcat_debug_response_height', 240); // 默认高度
|
||||||
|
|
||||||
const parsedRequest = parse(data.request);
|
const parsedRequest = parse(data.request);
|
||||||
const parsedResponse = parse(data.response);
|
const parsedResponse = parse(data.response);
|
||||||
const [backgroundImage] = useLocalStorage<string>(key.backgroundImage, '');
|
const [backgroundImage] = useLocalStorage<string>(key.backgroundImage, '');
|
||||||
@ -54,8 +58,42 @@ const OneBotApiDebug: React.FC<OneBotApiDebugProps> = (props) => {
|
|||||||
setIsFetching(true);
|
setIsFetching(true);
|
||||||
setResponseStatus(null);
|
setResponseStatus(null);
|
||||||
const r = toast.loading('正在发送请求...');
|
const r = toast.loading('正在发送请求...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const parsedRequestBody = JSON.parse(requestBody);
|
const parsedRequestBody = JSON.parse(requestBody);
|
||||||
|
|
||||||
|
// 如果有 adapterName,走后端转发
|
||||||
|
if (adapterName) {
|
||||||
|
request.post(`/api/Debug/call/${adapterName}`, {
|
||||||
|
action: path.replace(/^\//, ''), // 去掉开头的 /
|
||||||
|
params: parsedRequestBody
|
||||||
|
}, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${localStorage.getItem('token')}`
|
||||||
|
}
|
||||||
|
}).then((res) => {
|
||||||
|
if (res.data.code === 0) {
|
||||||
|
setResponseContent(JSON.stringify(res.data.data, null, 2));
|
||||||
|
setResponseStatus({ code: 200, text: 'OK' });
|
||||||
|
} else {
|
||||||
|
setResponseContent(JSON.stringify(res.data, null, 2));
|
||||||
|
setResponseStatus({ code: 500, text: res.data.message });
|
||||||
|
}
|
||||||
|
setResponseExpanded(true);
|
||||||
|
toast.success('请求成功');
|
||||||
|
}).catch((err) => {
|
||||||
|
toast.error('请求失败:' + err.message);
|
||||||
|
setResponseContent(JSON.stringify({ error: err.message }, null, 2));
|
||||||
|
setResponseStatus({ code: 500, text: 'Error' });
|
||||||
|
setResponseExpanded(true);
|
||||||
|
}).finally(() => {
|
||||||
|
setIsFetching(false);
|
||||||
|
toast.dismiss(r);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 回退到旧逻辑 (直接请求)
|
||||||
const requestURL = new URL(httpConfig.url);
|
const requestURL = new URL(httpConfig.url);
|
||||||
requestURL.pathname = path;
|
requestURL.pathname = path;
|
||||||
request
|
request
|
||||||
@ -63,17 +101,16 @@ const OneBotApiDebug: React.FC<OneBotApiDebugProps> = (props) => {
|
|||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${httpConfig.token}`,
|
Authorization: `Bearer ${httpConfig.token}`,
|
||||||
},
|
},
|
||||||
responseType: 'text',
|
}) // 移除 responseType: 'text',以便 axios 自动解析 JSON
|
||||||
})
|
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
setResponseContent(parseAxiosResponse(res));
|
setResponseContent(JSON.stringify(res.data, null, 2));
|
||||||
setResponseStatus({ code: res.status, text: res.statusText });
|
setResponseStatus({ code: res.status, text: res.statusText });
|
||||||
setResponseExpanded(true);
|
setResponseExpanded(true);
|
||||||
toast.success('请求成功');
|
toast.success('请求成功');
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
toast.error('请求失败:' + err.message);
|
toast.error('请求失败:' + err.message);
|
||||||
setResponseContent(parseAxiosResponse(err.response));
|
setResponseContent(JSON.stringify(err.response?.data || { error: err.message }, null, 2));
|
||||||
if (err.response) {
|
if (err.response) {
|
||||||
setResponseStatus({ code: err.response.status, text: err.response.statusText });
|
setResponseStatus({ code: err.response.status, text: err.response.statusText });
|
||||||
}
|
}
|
||||||
@ -96,6 +133,50 @@ const OneBotApiDebug: React.FC<OneBotApiDebugProps> = (props) => {
|
|||||||
setResponseStatus(null);
|
setResponseStatus(null);
|
||||||
}, [path]);
|
}, [path]);
|
||||||
|
|
||||||
|
// Height Resizing Logic
|
||||||
|
const handleMouseDown = useCallback((e: React.MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const startY = e.clientY;
|
||||||
|
const startHeight = responseHeight;
|
||||||
|
|
||||||
|
const handleMouseMove = (mv: MouseEvent) => {
|
||||||
|
const delta = startY - mv.clientY;
|
||||||
|
// 向上拖动 -> 增加高度
|
||||||
|
setResponseHeight(Math.max(100, Math.min(window.innerHeight - 200, startHeight + delta)));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseUp = () => {
|
||||||
|
document.removeEventListener('mousemove', handleMouseMove);
|
||||||
|
document.removeEventListener('mouseup', handleMouseUp);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', handleMouseMove);
|
||||||
|
document.addEventListener('mouseup', handleMouseUp);
|
||||||
|
}, [responseHeight, setResponseHeight]);
|
||||||
|
|
||||||
|
const handleTouchStart = useCallback((e: React.TouchEvent) => {
|
||||||
|
// 阻止默认滚动行为可能需要谨慎,这里尽量只阻止 handle 上的
|
||||||
|
// e.preventDefault();
|
||||||
|
const touch = e.touches[0];
|
||||||
|
const startY = touch.clientY;
|
||||||
|
const startHeight = responseHeight;
|
||||||
|
|
||||||
|
const handleTouchMove = (mv: TouchEvent) => {
|
||||||
|
const mvTouch = mv.touches[0];
|
||||||
|
const delta = startY - mvTouch.clientY;
|
||||||
|
setResponseHeight(Math.max(100, Math.min(window.innerHeight - 200, startHeight + delta)));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTouchEnd = () => {
|
||||||
|
document.removeEventListener('touchmove', handleTouchMove);
|
||||||
|
document.removeEventListener('touchend', handleTouchEnd);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('touchmove', handleTouchMove);
|
||||||
|
document.addEventListener('touchend', handleTouchEnd);
|
||||||
|
}, [responseHeight, setResponseHeight]);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className='h-full flex flex-col overflow-hidden bg-transparent'>
|
<section className='h-full flex flex-col overflow-hidden bg-transparent'>
|
||||||
{/* URL Bar */}
|
{/* URL Bar */}
|
||||||
@ -231,14 +312,27 @@ const OneBotApiDebug: React.FC<OneBotApiDebugProps> = (props) => {
|
|||||||
<div className='flex-shrink-0 px-3 pb-3'>
|
<div className='flex-shrink-0 px-3 pb-3'>
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'rounded-xl transition-all overflow-hidden border border-white/5',
|
'rounded-xl transition-all overflow-hidden border border-white/5 flex flex-col',
|
||||||
hasBackground ? 'bg-white/5' : 'bg-white/5 dark:bg-black/5'
|
hasBackground ? 'bg-white/5' : 'bg-white/5 dark:bg-black/5'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
{/* Header & Resize Handle */}
|
||||||
<div
|
<div
|
||||||
className='flex items-center justify-between px-4 py-2 cursor-pointer hover:bg-white/5 transition-all select-none'
|
className='flex items-center justify-between px-4 py-2 cursor-pointer hover:bg-white/5 transition-all select-none relative group'
|
||||||
onClick={() => setResponseExpanded(!responseExpanded)}
|
onClick={() => setResponseExpanded(!responseExpanded)}
|
||||||
>
|
>
|
||||||
|
{/* Invisble Resize Area that becomes visible/active */}
|
||||||
|
{responseExpanded && (
|
||||||
|
<div
|
||||||
|
className="absolute -top-1 left-0 w-full h-3 cursor-ns-resize z-50 flex items-center justify-center opacity-0 hover:opacity-100 group-hover:opacity-100 transition-opacity"
|
||||||
|
onMouseDown={(e) => { e.stopPropagation(); handleMouseDown(e); }}
|
||||||
|
onTouchStart={(e) => { e.stopPropagation(); handleTouchStart(e); }}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div className="w-12 h-1 bg-white/20 rounded-full" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<IoChevronDown className={clsx('text-[10px] transition-transform duration-300 opacity-20', !responseExpanded && '-rotate-90')} />
|
<IoChevronDown className={clsx('text-[10px] transition-transform duration-300 opacity-20', !responseExpanded && '-rotate-90')} />
|
||||||
<span className='text-[10px] font-semibold tracking-wide opacity-30 uppercase'>Response</span>
|
<span className='text-[10px] font-semibold tracking-wide opacity-30 uppercase'>Response</span>
|
||||||
@ -254,15 +348,27 @@ const OneBotApiDebug: React.FC<OneBotApiDebugProps> = (props) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Response Content - Code Editor */}
|
||||||
{responseExpanded && (
|
{responseExpanded && (
|
||||||
<div className='h-36 overflow-auto relative font-mono text-[11px] px-4 pb-3 no-scrollbar transition-all'>
|
<div style={{ height: responseHeight }} className="relative bg-black/5 dark:bg-black/20">
|
||||||
<PageLoading loading={isFetching} />
|
<PageLoading loading={isFetching} />
|
||||||
<div className={clsx(
|
<CodeEditor
|
||||||
'whitespace-pre-wrap break-all leading-relaxed opacity-40 transition-opacity',
|
value={responseContent || '// Waiting for response...'}
|
||||||
hasBackground ? 'text-white' : 'text-default-600'
|
language='json'
|
||||||
)}>
|
options={{
|
||||||
{responseContent || '...'}
|
minimap: { enabled: false },
|
||||||
</div>
|
fontSize: 11,
|
||||||
|
lineNumbers: 'off',
|
||||||
|
scrollBeyondLastLine: false,
|
||||||
|
wordWrap: 'on',
|
||||||
|
readOnly: true,
|
||||||
|
folding: true,
|
||||||
|
padding: { top: 8, bottom: 8 },
|
||||||
|
renderLineHighlight: 'none',
|
||||||
|
automaticLayout: true
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -19,6 +19,8 @@ export default function HttpDebug () {
|
|||||||
const [backgroundImage] = useLocalStorage<string>(key.backgroundImage, '');
|
const [backgroundImage] = useLocalStorage<string>(key.backgroundImage, '');
|
||||||
const hasBackground = !!backgroundImage;
|
const hasBackground = !!backgroundImage;
|
||||||
|
|
||||||
|
const [adapterName, setAdapterName] = useState<string>('');
|
||||||
|
|
||||||
// Auto-collapse sidebar on mobile initial load
|
// Auto-collapse sidebar on mobile initial load
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (window.innerWidth < 768) {
|
if (window.innerWidth < 768) {
|
||||||
@ -26,6 +28,37 @@ export default function HttpDebug () {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Initialize Debug Adapter
|
||||||
|
useEffect(() => {
|
||||||
|
let currentAdapterName = '';
|
||||||
|
|
||||||
|
const initAdapter = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/Debug/create', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.code === 0) {
|
||||||
|
currentAdapterName = data.data.adapterName;
|
||||||
|
setAdapterName(currentAdapterName);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to create debug adapter:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
initAdapter();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
// 不再主动关闭 adapter,由后端自动管理活跃状态
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
const handleSelectApi = (api: OneBotHttpApiPath) => {
|
const handleSelectApi = (api: OneBotHttpApiPath) => {
|
||||||
if (!openApis.includes(api)) {
|
if (!openApis.includes(api)) {
|
||||||
setOpenApis([...openApis, api]);
|
setOpenApis([...openApis, api]);
|
||||||
@ -149,7 +182,11 @@ export default function HttpDebug () {
|
|||||||
api === activeApi ? 'opacity-100 z-10' : 'opacity-0 z-0 pointer-events-none'
|
api === activeApi ? 'opacity-100 z-10' : 'opacity-0 z-0 pointer-events-none'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<OneBotApiDebug path={api} data={oneBotHttpApi[api]} />
|
<OneBotApiDebug
|
||||||
|
path={api}
|
||||||
|
data={oneBotHttpApi[api]}
|
||||||
|
adapterName={adapterName}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -3,9 +3,9 @@ import { Card, CardBody } from '@heroui/card';
|
|||||||
import { Input } from '@heroui/input';
|
import { Input } from '@heroui/input';
|
||||||
import { useLocalStorage } from '@uidotdev/usehooks';
|
import { useLocalStorage } from '@uidotdev/usehooks';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import { IoFlash, IoFlashOff } from 'react-icons/io5';
|
import { IoFlash, IoFlashOff, IoRefresh } from 'react-icons/io5';
|
||||||
|
|
||||||
import key from '@/const/key';
|
import key from '@/const/key';
|
||||||
|
|
||||||
@ -33,13 +33,68 @@ export default function WSDebug () {
|
|||||||
const { sendMessage, readyState, FilterMessagesType, filteredMessages, clearMessages } =
|
const { sendMessage, readyState, FilterMessagesType, filteredMessages, clearMessages } =
|
||||||
useWebSocketDebug(socketConfig.url, socketConfig.token, shouldConnect);
|
useWebSocketDebug(socketConfig.url, socketConfig.token, shouldConnect);
|
||||||
|
|
||||||
|
// Auto fetch adapter and set URL
|
||||||
|
useEffect(() => {
|
||||||
|
// 检查是否应该覆盖 URL
|
||||||
|
const isDefaultUrl = socketConfig.url === defaultWsUrl || socketConfig.url === '';
|
||||||
|
const isWebDebugUrl = socketConfig.url && socketConfig.url.includes('/api/Debug/ws');
|
||||||
|
|
||||||
|
if (!isDefaultUrl && !isWebDebugUrl) {
|
||||||
|
setInputUrl(socketConfig.url);
|
||||||
|
setInputToken(socketConfig.token);
|
||||||
|
return; // 已经有自定义/有效的配置,跳过自动创建
|
||||||
|
}
|
||||||
|
|
||||||
|
const initAdapter = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/Debug/create', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.code === 0) {
|
||||||
|
//const adapterName = data.data.adapterName;
|
||||||
|
const token = data.data.token;
|
||||||
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
// URL 中不再包含 Token,Token 单独放入输入框
|
||||||
|
const wsUrl = `${protocol}//${window.location.host}/api/Debug/ws`;
|
||||||
|
|
||||||
|
setSocketConfig({
|
||||||
|
url: wsUrl,
|
||||||
|
token: token
|
||||||
|
});
|
||||||
|
setInputUrl(wsUrl);
|
||||||
|
setInputToken(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to create debug adapter:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
initAdapter();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleConnect = useCallback(() => {
|
const handleConnect = useCallback(() => {
|
||||||
if (!inputUrl.startsWith('ws://') && !inputUrl.startsWith('wss://')) {
|
// 允许以 / 开头的相对路径(如代理情况),以及标准的 ws/wss
|
||||||
|
let finalUrl = inputUrl;
|
||||||
|
if (finalUrl.startsWith('/')) {
|
||||||
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
|
finalUrl = `${protocol}//${window.location.host}${finalUrl}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!finalUrl.startsWith('ws://') && !finalUrl.startsWith('wss://')) {
|
||||||
toast.error('WebSocket URL 不合法');
|
toast.error('WebSocket URL 不合法');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setSocketConfig({
|
setSocketConfig({
|
||||||
url: inputUrl,
|
url: finalUrl,
|
||||||
token: inputToken,
|
token: inputToken,
|
||||||
});
|
});
|
||||||
setShouldConnect(true);
|
setShouldConnect(true);
|
||||||
@ -49,6 +104,12 @@ export default function WSDebug () {
|
|||||||
setShouldConnect(false);
|
setShouldConnect(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleResetConfig = useCallback(() => {
|
||||||
|
setSocketConfig({ url: '', token: '' });
|
||||||
|
// 刷新页面以重新触发初始逻辑
|
||||||
|
window.location.reload();
|
||||||
|
}, [setSocketConfig]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<title>Websocket调试 - NapCat WebUI</title>
|
<title>Websocket调试 - NapCat WebUI</title>
|
||||||
@ -101,16 +162,29 @@ export default function WSDebug () {
|
|||||||
input: hasBackground ? 'text-white placeholder:text-white/50' : '',
|
input: hasBackground ? 'text-white placeholder:text-white/50' : '',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Button
|
<div className="flex gap-2">
|
||||||
onPress={shouldConnect ? handleDisconnect : handleConnect}
|
<Button
|
||||||
size='md'
|
isIconOnly
|
||||||
radius='full'
|
size="md"
|
||||||
color={shouldConnect ? 'danger' : 'primary'}
|
radius="full"
|
||||||
className='font-bold shadow-lg min-w-[100px]'
|
color="warning"
|
||||||
startContent={shouldConnect ? <IoFlashOff /> : <IoFlash />}
|
variant="flat"
|
||||||
>
|
onPress={handleResetConfig}
|
||||||
{shouldConnect ? '断开' : '连接'}
|
title="重置配置"
|
||||||
</Button>
|
>
|
||||||
|
<IoRefresh className="text-xl" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onPress={shouldConnect ? handleDisconnect : handleConnect}
|
||||||
|
size='md'
|
||||||
|
radius='full'
|
||||||
|
color={shouldConnect ? 'danger' : 'primary'}
|
||||||
|
className='font-bold shadow-lg min-w-[100px] flex-1'
|
||||||
|
startContent={shouldConnect ? <IoFlashOff /> : <IoFlash />}
|
||||||
|
>
|
||||||
|
{shouldConnect ? '断开' : '连接'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Status Bar */}
|
{/* Status Bar */}
|
||||||
|
|||||||
@ -34,6 +34,11 @@ export default defineConfig(({ mode }) => {
|
|||||||
ws: true,
|
ws: true,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
},
|
},
|
||||||
|
'/api/Debug/ws': {
|
||||||
|
target: backendDebugUrl,
|
||||||
|
ws: true,
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
'/api': backendDebugUrl,
|
'/api': backendDebugUrl,
|
||||||
'/files': backendDebugUrl,
|
'/files': backendDebugUrl,
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user