From 578dda2f17a19f4955992fb1a7921a750ec30613 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=89=8B=E7=93=9C=E4=B8=80=E5=8D=81=E9=9B=AA?= Date: Mon, 22 Dec 2025 16:27:06 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E5=85=8D=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E8=B0=83=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/napcat-framework/napcat.ts | 5 +- packages/napcat-shell/base.ts | 6 +- packages/napcat-webui-backend/index.ts | 11 +- .../napcat-webui-backend/src/api/Debug.ts | 410 ++++++++++++++++++ .../napcat-webui-backend/src/helper/Data.ts | 9 + .../napcat-webui-backend/src/router/index.ts | 3 + .../napcat-webui-backend/src/types/index.ts | 1 + .../src/components/onebot/api/debug.tsx | 138 +++++- .../src/pages/dashboard/debug/http/index.tsx | 39 +- .../pages/dashboard/debug/websocket/index.tsx | 102 ++++- packages/napcat-webui-frontend/vite.config.ts | 5 + 11 files changed, 695 insertions(+), 34 deletions(-) create mode 100644 packages/napcat-webui-backend/src/api/Debug.ts diff --git a/packages/napcat-framework/napcat.ts b/packages/napcat-framework/napcat.ts index 5aac337d..75122f6e 100644 --- a/packages/napcat-framework/napcat.ts +++ b/packages/napcat-framework/napcat.ts @@ -79,7 +79,10 @@ export async function NCoreInitFramework ( WebUiDataRuntime.setWorkingEnv(NapCatCoreWorkingEnv.Framework); InitWebUi(logger, pathWrapper, logSubscription, statusHelperSubscription).then().catch(e => logger.logError(e)); // 初始化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 { diff --git a/packages/napcat-shell/base.ts b/packages/napcat-shell/base.ts index 74c79063..75c0b1ad 100644 --- a/packages/napcat-shell/base.ts +++ b/packages/napcat-shell/base.ts @@ -455,7 +455,11 @@ export class NapCatShell { async InitNapCat () { 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)); } } + diff --git a/packages/napcat-webui-backend/index.ts b/packages/napcat-webui-backend/index.ts index 3f5a0ed7..f0a4e908 100644 --- a/packages/napcat-webui-backend/index.ts +++ b/packages/napcat-webui-backend/index.ts @@ -22,6 +22,7 @@ import { existsSync, readFileSync } from 'node:fs'; // 引入multer用于错误 import { ILogWrapper } from 'napcat-common/src/log-interface'; import { ISubscription } from 'napcat-common/src/subscription-interface'; import { IStatusHelperSubscription } from '@/napcat-common/src/status-interface'; +import { handleDebugWebSocket } from '@/napcat-webui-backend/src/api/Debug'; // 实例化Express const app = express(); /** @@ -187,7 +188,15 @@ export async function InitWebUi (logger: ILogWrapper, pathWrapper: NapCatPathWra const isHttps = !!sslCerts; const server = isHttps && sslCerts ? createHttpsServer(sslCerts, app) : createServer(app); 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接口 app.use('/api', ALLRouter); diff --git a/packages/napcat-webui-backend/src/api/Debug.ts b/packages/napcat-webui-backend/src/api/Debug.ts new file mode 100644 index 00000000..60586841 --- /dev/null +++ b/packages/napcat-webui-backend/src/api/Debug.ts @@ -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 = 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 { } + async close (): Promise { this.cleanup(); } + async reload (_config: any): Promise { 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 { + 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; diff --git a/packages/napcat-webui-backend/src/helper/Data.ts b/packages/napcat-webui-backend/src/helper/Data.ts index d452eb33..bdf8f7f6 100644 --- a/packages/napcat-webui-backend/src/helper/Data.ts +++ b/packages/napcat-webui-backend/src/helper/Data.ts @@ -15,6 +15,7 @@ const LoginRuntime: LoginRuntimeType = { nick: '', }, QQVersion: 'unknown', + OneBotContext: null, onQQLoginStatusChange: async (status: boolean) => { LoginRuntime.QQLoginStatus = status; }, @@ -154,4 +155,12 @@ export const WebUiDataRuntime = { runWebUiConfigQuickFunction: async function () { await LoginRuntime.WebUiConfigQuickFunction(); }, + + setOneBotContext (context: any): void { + LoginRuntime.OneBotContext = context; + }, + + getOneBotContext (): any | null { + return LoginRuntime.OneBotContext; + }, }; diff --git a/packages/napcat-webui-backend/src/router/index.ts b/packages/napcat-webui-backend/src/router/index.ts index 21d094dd..768f3e61 100644 --- a/packages/napcat-webui-backend/src/router/index.ts +++ b/packages/napcat-webui-backend/src/router/index.ts @@ -15,6 +15,7 @@ import { BaseRouter } from '@/napcat-webui-backend/src/router/Base'; import { FileRouter } from './File'; import { WebUIConfigRouter } from './WebUIConfig'; import { UpdateNapCatRouter } from './UpdateNapCat'; +import DebugRouter from '@/napcat-webui-backend/src/api/Debug'; const router = Router(); @@ -41,5 +42,7 @@ router.use('/File', FileRouter); router.use('/WebUIConfig', WebUIConfigRouter); // router:更新NapCat相关路由 router.use('/UpdateNapCat', UpdateNapCatRouter); +// router:调试相关路由 +router.use('/Debug', DebugRouter); export { router as ALLRouter }; diff --git a/packages/napcat-webui-backend/src/types/index.ts b/packages/napcat-webui-backend/src/types/index.ts index 21c31d44..d2d3a7ff 100644 --- a/packages/napcat-webui-backend/src/types/index.ts +++ b/packages/napcat-webui-backend/src/types/index.ts @@ -47,6 +47,7 @@ export interface LoginRuntimeType { onQQLoginStatusChange: (status: boolean) => Promise; onWebUiTokenChange: (token: string) => Promise; WebUiConfigQuickFunction: () => Promise; + OneBotContext: any | null; // OneBot 上下文,用于调试功能 NapCatHelper: { onQuickLoginRequested: (uin: string) => Promise<{ result: boolean; message: string; }>; onOB11ConfigChanged: (ob11: OneBotConfig) => Promise; diff --git a/packages/napcat-webui-frontend/src/components/onebot/api/debug.tsx b/packages/napcat-webui-frontend/src/components/onebot/api/debug.tsx index be40c712..3bb89b05 100644 --- a/packages/napcat-webui-frontend/src/components/onebot/api/debug.tsx +++ b/packages/napcat-webui-frontend/src/components/onebot/api/debug.tsx @@ -6,7 +6,7 @@ import { Tab, Tabs } from '@heroui/tabs'; import { Chip } from '@heroui/chip'; import { useLocalStorage } from '@uidotdev/usehooks'; import clsx from 'clsx'; -import { useEffect, useState } from 'react'; +import { useEffect, useState, useCallback } from 'react'; import toast from 'react-hot-toast'; import { IoChevronDown, IoSend, IoSettingsSharp, IoCopy } from 'react-icons/io5'; import { TbCode, TbMessageCode } from 'react-icons/tb'; @@ -19,7 +19,7 @@ import CodeEditor from '@/components/code_editor'; import PageLoading from '@/components/page_loading'; import { request } from '@/utils/request'; -import { parseAxiosResponse } from '@/utils/url'; + import { generateDefaultJson, parse } from '@/utils/zod'; import DisplayStruct from './display_struct'; @@ -27,10 +27,11 @@ import DisplayStruct from './display_struct'; export interface OneBotApiDebugProps { path: OneBotHttpApiPath; data: OneBotHttpApiContent; + adapterName?: string; } const OneBotApiDebug: React.FC = (props) => { - const { path, data } = props; + const { path, data, adapterName } = props; const currentURL = new URL(window.location.origin); currentURL.port = '3000'; const defaultHttpUrl = currentURL.href; @@ -38,12 +39,15 @@ const OneBotApiDebug: React.FC = (props) => { url: defaultHttpUrl, token: '', }); + const [requestBody, setRequestBody] = useState('{}'); const [responseContent, setResponseContent] = useState(''); const [isFetching, setIsFetching] = useState(false); const [activeTab, setActiveTab] = useState('request'); const [responseExpanded, setResponseExpanded] = useState(true); 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 parsedResponse = parse(data.response); const [backgroundImage] = useLocalStorage(key.backgroundImage, ''); @@ -54,8 +58,42 @@ const OneBotApiDebug: React.FC = (props) => { setIsFetching(true); setResponseStatus(null); const r = toast.loading('正在发送请求...'); + try { 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); requestURL.pathname = path; request @@ -63,17 +101,16 @@ const OneBotApiDebug: React.FC = (props) => { headers: { Authorization: `Bearer ${httpConfig.token}`, }, - responseType: 'text', - }) + }) // 移除 responseType: 'text',以便 axios 自动解析 JSON .then((res) => { - setResponseContent(parseAxiosResponse(res)); + setResponseContent(JSON.stringify(res.data, null, 2)); setResponseStatus({ code: res.status, text: res.statusText }); setResponseExpanded(true); toast.success('请求成功'); }) .catch((err) => { toast.error('请求失败:' + err.message); - setResponseContent(parseAxiosResponse(err.response)); + setResponseContent(JSON.stringify(err.response?.data || { error: err.message }, null, 2)); if (err.response) { setResponseStatus({ code: err.response.status, text: err.response.statusText }); } @@ -96,6 +133,50 @@ const OneBotApiDebug: React.FC = (props) => { setResponseStatus(null); }, [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 (
{/* URL Bar */} @@ -231,14 +312,27 @@ const OneBotApiDebug: React.FC = (props) => {
+ {/* Header & Resize Handle */}
setResponseExpanded(!responseExpanded)} > + {/* Invisble Resize Area that becomes visible/active */} + {responseExpanded && ( +
{ e.stopPropagation(); handleMouseDown(e); }} + onTouchStart={(e) => { e.stopPropagation(); handleTouchStart(e); }} + onClick={(e) => e.stopPropagation()} + > +
+
+ )} +
Response @@ -254,15 +348,27 @@ const OneBotApiDebug: React.FC = (props) => {
+ + {/* Response Content - Code Editor */} {responseExpanded && ( -
+
-
- {responseContent || '...'} -
+
)}
diff --git a/packages/napcat-webui-frontend/src/pages/dashboard/debug/http/index.tsx b/packages/napcat-webui-frontend/src/pages/dashboard/debug/http/index.tsx index e734d72f..ccafb31a 100644 --- a/packages/napcat-webui-frontend/src/pages/dashboard/debug/http/index.tsx +++ b/packages/napcat-webui-frontend/src/pages/dashboard/debug/http/index.tsx @@ -19,6 +19,8 @@ export default function HttpDebug () { const [backgroundImage] = useLocalStorage(key.backgroundImage, ''); const hasBackground = !!backgroundImage; + const [adapterName, setAdapterName] = useState(''); + // Auto-collapse sidebar on mobile initial load useEffect(() => { 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) => { if (!openApis.includes(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' )} > - +
))}
diff --git a/packages/napcat-webui-frontend/src/pages/dashboard/debug/websocket/index.tsx b/packages/napcat-webui-frontend/src/pages/dashboard/debug/websocket/index.tsx index 044eb1e3..f717837c 100644 --- a/packages/napcat-webui-frontend/src/pages/dashboard/debug/websocket/index.tsx +++ b/packages/napcat-webui-frontend/src/pages/dashboard/debug/websocket/index.tsx @@ -3,9 +3,9 @@ import { Card, CardBody } from '@heroui/card'; import { Input } from '@heroui/input'; import { useLocalStorage } from '@uidotdev/usehooks'; import clsx from 'clsx'; -import { useCallback, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; 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'; @@ -33,13 +33,68 @@ export default function WSDebug () { const { sendMessage, readyState, FilterMessagesType, filteredMessages, clearMessages } = 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(() => { - 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 不合法'); return; } + setSocketConfig({ - url: inputUrl, + url: finalUrl, token: inputToken, }); setShouldConnect(true); @@ -49,6 +104,12 @@ export default function WSDebug () { setShouldConnect(false); }, []); + const handleResetConfig = useCallback(() => { + setSocketConfig({ url: '', token: '' }); + // 刷新页面以重新触发初始逻辑 + window.location.reload(); + }, [setSocketConfig]); + return ( <> Websocket调试 - NapCat WebUI @@ -101,16 +162,29 @@ export default function WSDebug () { input: hasBackground ? 'text-white placeholder:text-white/50' : '', }} /> - +
+ + +
{/* Status Bar */} diff --git a/packages/napcat-webui-frontend/vite.config.ts b/packages/napcat-webui-frontend/vite.config.ts index f7087409..a575aecb 100644 --- a/packages/napcat-webui-frontend/vite.config.ts +++ b/packages/napcat-webui-frontend/vite.config.ts @@ -34,6 +34,11 @@ export default defineConfig(({ mode }) => { ws: true, changeOrigin: true, }, + '/api/Debug/ws': { + target: backendDebugUrl, + ws: true, + changeOrigin: true, + }, '/api': backendDebugUrl, '/files': backendDebugUrl, },