diff --git a/packages/napcat-common/src/log-interface.ts b/packages/napcat-common/src/log-interface.ts new file mode 100644 index 00000000..783887df --- /dev/null +++ b/packages/napcat-common/src/log-interface.ts @@ -0,0 +1,24 @@ +export enum LogLevel { + DEBUG = 'debug', + INFO = 'info', + WARN = 'warn', + ERROR = 'error', + FATAL = 'fatal', +} +export interface ILogWrapper { + fileLogEnabled: boolean; + consoleLogEnabled: boolean; + cleanOldLogs (logDir: string): void; + setFileAndConsoleLogLevel (fileLogLevel: LogLevel, consoleLogLevel: LogLevel): void; + setLogSelfInfo (selfInfo: { nick: string; uid: string; }): void; + setFileLogEnabled (isEnabled: boolean): void; + setConsoleLogEnabled (isEnabled: boolean): void; + formatMsg (msg: any[]): string; + _log (level: LogLevel, ...args: any[]): void; + log (...args: any[]): void; + logDebug (...args: any[]): void; + logError (...args: any[]): void; + logWarn (...args: any[]): void; + logFatal (...args: any[]): void; + logMessage (msg: unknown, selfInfo: unknown): void; +} \ No newline at end of file diff --git a/packages/napcat-common/src/status-interface.ts b/packages/napcat-common/src/status-interface.ts new file mode 100644 index 00000000..8385c281 --- /dev/null +++ b/packages/napcat-common/src/status-interface.ts @@ -0,0 +1,24 @@ +export interface SystemStatus { + cpu: { + model: string, + speed: string; + usage: { + system: string; + qq: string; + }, + core: number; + }, + memory: { + total: string; + usage: { + system: string; + qq: string; + }; + }, + arch: string; +} +export interface IStatusHelperSubscription { + on (event: 'statusUpdate', listener: (status: SystemStatus) => void): this; + off (event: 'statusUpdate', listener: (status: SystemStatus) => void): this; + emit (event: 'statusUpdate', status: SystemStatus): boolean; +} \ No newline at end of file diff --git a/packages/napcat-common/src/subscription-interface.ts b/packages/napcat-common/src/subscription-interface.ts new file mode 100644 index 00000000..8f2978a6 --- /dev/null +++ b/packages/napcat-common/src/subscription-interface.ts @@ -0,0 +1,6 @@ +export type LogListener = (msg: string) => void; +export interface ISubscription { + subscribe (listener: LogListener): void; + unsubscribe (listener: LogListener): void; + notify (msg: string): void; +} \ No newline at end of file diff --git a/packages/napcat-core/helper/log.ts b/packages/napcat-core/helper/log.ts index ce399a3f..ba200f68 100644 --- a/packages/napcat-core/helper/log.ts +++ b/packages/napcat-core/helper/log.ts @@ -3,6 +3,7 @@ import { truncateString } from 'napcat-common/src/helper'; import path from 'node:path'; import fs from 'node:fs/promises'; import { NTMsgAtType, ChatType, ElementType, MessageElement, RawMessage, SelfInfo } from '@/napcat-core/index'; +import { ILogWrapper } from 'napcat-common/src/log-interface'; import EventEmitter from 'node:events'; export enum LogLevel { DEBUG = 'debug', @@ -56,7 +57,7 @@ class Subscription { export const logSubscription = new Subscription(); -export class LogWrapper { +export class LogWrapper implements ILogWrapper { fileLogEnabled = true; consoleLogEnabled = true; logger: winston.Logger; diff --git a/packages/napcat-core/helper/status.ts b/packages/napcat-core/helper/status.ts index b7fd860a..696b2fc8 100644 --- a/packages/napcat-core/helper/status.ts +++ b/packages/napcat-core/helper/status.ts @@ -1,24 +1,24 @@ import os from 'node:os'; import EventEmitter from 'node:events'; - +import { IStatusHelperSubscription } from 'napcat-common/src/status-interface'; export interface SystemStatus { cpu: { model: string, - speed: string + speed: string; usage: { - system: string - qq: string + system: string; + qq: string; }, - core: number + core: number; }, memory: { - total: string + total: string; usage: { - system: string - qq: string - } + system: string; + qq: string; + }; }, - arch: string + arch: string; } export class StatusHelper { @@ -101,7 +101,7 @@ export class StatusHelper { } } -class StatusHelperSubscription extends EventEmitter { +class StatusHelperSubscription extends EventEmitter implements IStatusHelperSubscription { private statusHelper: StatusHelper; private interval: NodeJS.Timeout | null = null; diff --git a/packages/napcat-framework/napcat.ts b/packages/napcat-framework/napcat.ts index 868a7f86..57444baf 100644 --- a/packages/napcat-framework/napcat.ts +++ b/packages/napcat-framework/napcat.ts @@ -3,10 +3,11 @@ import { InitWebUi, WebUiConfig, webUiRuntimePort } from 'napcat-webui-backend/i import { NapCatOneBot11Adapter } from 'napcat-onebot/index'; import { NativePacketHandler } from 'napcat-core/packet/handler/client'; import { FFmpegService } from 'napcat-core/helper/ffmpeg/ffmpeg'; -import { LogWrapper } from 'napcat-core/helper/log'; +import { logSubscription, LogWrapper } from 'napcat-core/helper/log'; import { QQBasicInfoWrapper } from '@/napcat-core/helper/qq-basic-info'; import { InstanceContext, loadQQWrapper, NapCatCore, NapCatCoreWorkingEnv, NodeIKernelLoginListener, NodeIKernelLoginService, NodeIQQNTWrapperSession, SelfInfo, WrapperNodeApi } from '@/napcat-core'; import { proxiedListenerOf } from '@/napcat-core/helper/proxy-handler'; +import { statusHelperSubscription } from '@/napcat-core/helper/status'; // Framework ES入口文件 export async function getWebUiUrl () { @@ -72,7 +73,7 @@ export async function NCoreInitFramework ( await loaderObject.core.initCore(); // 启动WebUi - InitWebUi(logger, pathWrapper).then().catch(e => logger.logError(e)); + InitWebUi(logger, pathWrapper, logSubscription, statusHelperSubscription).then().catch(e => logger.logError(e)); // 初始化LLNC的Onebot实现 await new NapCatOneBot11Adapter(loaderObject.core, loaderObject.context, pathWrapper).InitOneBot(); } diff --git a/packages/napcat-shell/base.ts b/packages/napcat-shell/base.ts index cba8eb64..5ebb6ca0 100644 --- a/packages/napcat-shell/base.ts +++ b/packages/napcat-shell/base.ts @@ -31,9 +31,10 @@ import { sleep } from 'napcat-common/src/helper'; import { FFmpegService } from '@/napcat-core/helper/ffmpeg/ffmpeg'; import { connectToNamedPipe } from './pipe'; import { NativePacketHandler } from 'napcat-core/packet/handler/client'; -import { LogWrapper } from '@/napcat-core/helper/log'; +import { logSubscription, LogWrapper } from '@/napcat-core/helper/log'; import { proxiedListenerOf } from '@/napcat-core/helper/proxy-handler'; import { QQBasicInfoWrapper } from '@/napcat-core/helper/qq-basic-info'; +import { statusHelperSubscription } from '@/napcat-core/helper/status'; // NapCat Shell App ES 入口文件 async function handleUncaughtExceptions (logger: LogWrapper) { process.on('uncaughtException', (err) => { @@ -337,7 +338,7 @@ export async function NCoreInitShell () { o3Service.addO3MiscListener(new NodeIO3MiscListener()); logger.log('[NapCat] [Core] NapCat.Core Version: ' + napCatVersion); - InitWebUi(logger, pathWrapper).then().catch(e => logger.logError(e)); + InitWebUi(logger, pathWrapper, logSubscription, statusHelperSubscription).then().catch(e => logger.logError(e)); const engine = wrapper.NodeIQQNTWrapperEngine.get(); const loginService = wrapper.NodeIKernelLoginService.get(); diff --git a/packages/napcat-webui-backend/index.ts b/packages/napcat-webui-backend/index.ts index 012d7d7d..1c826d48 100644 --- a/packages/napcat-webui-backend/index.ts +++ b/packages/napcat-webui-backend/index.ts @@ -19,8 +19,9 @@ import multer from 'multer'; import * as net from 'node:net'; import { WebUiDataRuntime } from './src/helper/Data'; import { existsSync, readFileSync } from 'node:fs'; // 引入multer用于错误捕获 -import { LogWrapper } from '@/napcat-core/helper/log'; - +import { ILogWrapper } from 'napcat-common/src/log-interface'; +import { ISubscription } from 'napcat-common/src/subscription-interface'; +import { IStatusHelperSubscription } from '@/napcat-common/src/status-interface'; // 实例化Express const app = express(); /** @@ -31,6 +32,8 @@ const app = express(); */ export let WebUiConfig: WebUiConfigWrapper; export let webUiPathWrapper: NapCatPathWrapper; +export let logSubscription: ISubscription; +export let statusHelperSubscription: IStatusHelperSubscription; const MAX_PORT_TRY = 100; export let webUiRuntimePort = 6099; @@ -68,7 +71,7 @@ export async function InitPort (parsedConfig: WebUiConfigType): Promise<[string, } } -async function checkCertificates (logger: LogWrapper): Promise<{ key: string, cert: string } | null> { +async function checkCertificates (logger: ILogWrapper): Promise<{ key: string, cert: string; } | null> { try { const certPath = join(webUiPathWrapper.configPath, 'cert.pem'); const keyPath = join(webUiPathWrapper.configPath, 'key.pem'); @@ -85,8 +88,10 @@ async function checkCertificates (logger: LogWrapper): Promise<{ key: string, ce return null; } } -export async function InitWebUi (logger: LogWrapper, pathWrapper: NapCatPathWrapper) { +export async function InitWebUi (logger: ILogWrapper, pathWrapper: NapCatPathWrapper, Subscription: ISubscription, statusSubscription: IStatusHelperSubscription) { webUiPathWrapper = pathWrapper; + logSubscription = Subscription; + statusHelperSubscription = statusSubscription; WebUiConfig = new WebUiConfigWrapper(); let config = await WebUiConfig.GetWebUIConfig(); @@ -216,11 +221,11 @@ export async function InitWebUi (logger: LogWrapper, pathWrapper: NapCatPathWrap const searchParams = { token }; logger.log(`[NapCat] [WebUi] WebUi Token: ${token}`); logger.log( - `[NapCat] [WebUi] WebUi User Panel Url: ${createUrl('127.0.0.1', port.toString(), '/webui', searchParams)}` + `[NapCat] [WebUi] WebUi User Panel Url: ${createUrl('127.0.0.1', port.toString(), '/webui', searchParams)}` ); if (host !== '') { logger.log( - `[NapCat] [WebUi] WebUi User Panel Url: ${createUrl(host, port.toString(), '/webui', searchParams)}` + `[NapCat] [WebUi] WebUi User Panel Url: ${createUrl(host, port.toString(), '/webui', searchParams)}` ); } }); diff --git a/packages/napcat-webui-backend/package.json b/packages/napcat-webui-backend/package.json index def0a219..8ea4f068 100644 --- a/packages/napcat-webui-backend/package.json +++ b/packages/napcat-webui-backend/package.json @@ -1,41 +1,39 @@ { - "name": "napcat-webui-backend", - "version": "0.0.1", - "private": true, - "type": "module", - "main": "index.ts", - "scripts": { - "typecheck": "tsc --noEmit --skipLibCheck -p tsconfig.json" + "name": "napcat-webui-backend", + "version": "0.0.1", + "private": true, + "type": "module", + "main": "index.ts", + "scripts": { + "typecheck": "tsc --noEmit --skipLibCheck -p tsconfig.json" + }, + "exports": { + ".": { + "import": "./index.ts" }, - "exports": { - ".": { - "import": "./index.ts" - }, - "./*": { - "import": "./*" - } - }, - "dependencies": { - "@sinclair/typebox": "^0.34.38", - "ajv": "^8.13.0", - "compressing": "^1.10.3", - "express": "^5.0.0", - "express-rate-limit": "^7.5.0", - "json5": "^2.2.3", - "multer": "^2.0.1", - "napcat-common": "workspace:*", - "napcat-core": "workspace:*", - "napcat-onebot": "workspace:*", - "napcat-pty": "workspace:*", - "ws": "^8.18.3" - }, - "devDependencies": { - "@types/express": "^5.0.0", - "@types/multer": "^1.4.12", - "@types/node": "^22.0.1", - "@types/ws": "^8.5.12" - }, - "engines": { - "node": ">=18.0.0" + "./*": { + "import": "./*" } + }, + "dependencies": { + "@sinclair/typebox": "^0.34.38", + "ajv": "^8.13.0", + "compressing": "^1.10.3", + "express": "^5.0.0", + "express-rate-limit": "^7.5.0", + "json5": "^2.2.3", + "multer": "^2.0.1", + "napcat-common": "workspace:*", + "napcat-pty": "workspace:*", + "ws": "^8.18.3" + }, + "devDependencies": { + "@types/express": "^5.0.0", + "@types/multer": "^1.4.12", + "@types/node": "^22.0.1", + "@types/ws": "^8.5.12" + }, + "engines": { + "node": ">=18.0.0" + } } \ No newline at end of file diff --git a/packages/napcat-webui-backend/src/api/Log.ts b/packages/napcat-webui-backend/src/api/Log.ts index a1851942..b0fc27f0 100644 --- a/packages/napcat-webui-backend/src/api/Log.ts +++ b/packages/napcat-webui-backend/src/api/Log.ts @@ -1,8 +1,7 @@ import type { RequestHandler } from 'express'; import { sendError, sendSuccess } from '../utils/response'; -import { logSubscription } from 'napcat-core/helper/log'; import { terminalManager } from '../terminal/terminal_manager'; -import { WebUiConfig } from '@/napcat-webui-backend/index'; +import { logSubscription, WebUiConfig } from '@/napcat-webui-backend/index'; // 判断是否是 macos const isMacOS = process.platform === 'darwin'; diff --git a/packages/napcat-webui-backend/src/api/OB11Config.ts b/packages/napcat-webui-backend/src/api/OB11Config.ts index 934172dc..07874874 100644 --- a/packages/napcat-webui-backend/src/api/OB11Config.ts +++ b/packages/napcat-webui-backend/src/api/OB11Config.ts @@ -1,7 +1,7 @@ import { RequestHandler } from 'express'; import { existsSync, readFileSync } from 'node:fs'; import { resolve } from 'node:path'; -import { loadConfig, OneBotConfig } from 'napcat-onebot/config/config'; +import { loadConfig, OneBotConfig } from '@/napcat-webui-backend/src/onebot/config'; import { webUiPathWrapper } from '@/napcat-webui-backend/index'; import { WebUiDataRuntime } from '@/napcat-webui-backend/src/helper/Data'; import { sendError, sendSuccess } from '@/napcat-webui-backend/src/utils/response'; diff --git a/packages/napcat-webui-backend/src/api/Status.ts b/packages/napcat-webui-backend/src/api/Status.ts index f0caa50b..998c5392 100644 --- a/packages/napcat-webui-backend/src/api/Status.ts +++ b/packages/napcat-webui-backend/src/api/Status.ts @@ -1,6 +1,6 @@ +import { statusHelperSubscription } from '@/napcat-webui-backend'; import { RequestHandler } from 'express'; -import { SystemStatus, statusHelperSubscription } from 'napcat-core/helper/status'; - +import { SystemStatus } from 'napcat-common/src/status-interface'; export const StatusRealTimeHandler: RequestHandler = async (req, res) => { res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Connection', 'keep-alive'); diff --git a/packages/napcat-webui-backend/src/onebot/config.ts b/packages/napcat-webui-backend/src/onebot/config.ts new file mode 100644 index 00000000..b43c2c49 --- /dev/null +++ b/packages/napcat-webui-backend/src/onebot/config.ts @@ -0,0 +1,106 @@ +import { Type, Static } from '@sinclair/typebox'; +import Ajv from 'ajv'; +const HttpServerConfigSchema = Type.Object({ + name: Type.String({ default: 'http-server' }), + enable: Type.Boolean({ default: false }), + port: Type.Number({ default: 3000 }), + host: Type.String({ default: '127.0.0.1' }), + enableCors: Type.Boolean({ default: true }), + enableWebsocket: Type.Boolean({ default: true }), + messagePostFormat: Type.String({ default: 'array' }), + token: Type.String({ default: '' }), + debug: Type.Boolean({ default: false }), +}); + +const HttpSseServerConfigSchema = Type.Object({ + name: Type.String({ default: 'http-sse-server' }), + enable: Type.Boolean({ default: false }), + port: Type.Number({ default: 3000 }), + host: Type.String({ default: '127.0.0.1' }), + enableCors: Type.Boolean({ default: true }), + enableWebsocket: Type.Boolean({ default: true }), + messagePostFormat: Type.String({ default: 'array' }), + token: Type.String({ default: '' }), + debug: Type.Boolean({ default: false }), + reportSelfMessage: Type.Boolean({ default: false }), +}); + +const HttpClientConfigSchema = Type.Object({ + name: Type.String({ default: 'http-client' }), + enable: Type.Boolean({ default: false }), + url: Type.String({ default: 'http://localhost:8080' }), + messagePostFormat: Type.String({ default: 'array' }), + reportSelfMessage: Type.Boolean({ default: false }), + token: Type.String({ default: '' }), + debug: Type.Boolean({ default: false }), +}); + +const WebsocketServerConfigSchema = Type.Object({ + name: Type.String({ default: 'websocket-server' }), + enable: Type.Boolean({ default: false }), + host: Type.String({ default: '127.0.0.1' }), + port: Type.Number({ default: 3001 }), + messagePostFormat: Type.String({ default: 'array' }), + reportSelfMessage: Type.Boolean({ default: false }), + token: Type.String({ default: '' }), + enableForcePushEvent: Type.Boolean({ default: true }), + debug: Type.Boolean({ default: false }), + heartInterval: Type.Number({ default: 30000 }), +}); + +const WebsocketClientConfigSchema = Type.Object({ + name: Type.String({ default: 'websocket-client' }), + enable: Type.Boolean({ default: false }), + url: Type.String({ default: 'ws://localhost:8082' }), + messagePostFormat: Type.String({ default: 'array' }), + reportSelfMessage: Type.Boolean({ default: false }), + reconnectInterval: Type.Number({ default: 5000 }), + token: Type.String({ default: '' }), + debug: Type.Boolean({ default: false }), + heartInterval: Type.Number({ default: 30000 }), +}); + +const PluginConfigSchema = Type.Object({ + name: Type.String({ default: 'plugin' }), + enable: Type.Boolean({ default: false }), + messagePostFormat: Type.String({ default: 'array' }), + reportSelfMessage: Type.Boolean({ default: false }), + debug: Type.Boolean({ default: false }), +}); + +const NetworkConfigSchema = Type.Object({ + httpServers: Type.Array(HttpServerConfigSchema, { default: [] }), + httpSseServers: Type.Array(HttpSseServerConfigSchema, { default: [] }), + httpClients: Type.Array(HttpClientConfigSchema, { default: [] }), + websocketServers: Type.Array(WebsocketServerConfigSchema, { default: [] }), + websocketClients: Type.Array(WebsocketClientConfigSchema, { default: [] }), + plugins: Type.Array(PluginConfigSchema, { default: [] }), +}, { default: {} }); + +export const OneBotConfigSchema = Type.Object({ + network: NetworkConfigSchema, + musicSignUrl: Type.String({ default: '' }), + enableLocalFile2Url: Type.Boolean({ default: false }), + parseMultMsg: Type.Boolean({ default: false }), +}); + +export type OneBotConfig = Static; +export type HttpServerConfig = Static; +export type HttpSseServerConfig = Static; +export type HttpClientConfig = Static; +export type WebsocketServerConfig = Static; +export type WebsocketClientConfig = Static; +export type PluginConfig = Static; + +export type NetworkAdapterConfig = HttpServerConfig | HttpSseServerConfig | HttpClientConfig | WebsocketServerConfig | WebsocketClientConfig | PluginConfig; +export type NetworkConfigKey = keyof OneBotConfig['network']; + +export function loadConfig (config: Partial): OneBotConfig { + const ajv = new Ajv({ useDefaults: true, coerceTypes: true }); + const validate = ajv.compile(OneBotConfigSchema); + const valid = validate(config); + if (!valid) { + throw new Error(ajv.errorsText(validate.errors)); + } + return config as OneBotConfig; +} diff --git a/packages/napcat-webui-backend/src/terminal/terminal_manager.ts b/packages/napcat-webui-backend/src/terminal/terminal_manager.ts index 9e99381c..0892703a 100644 --- a/packages/napcat-webui-backend/src/terminal/terminal_manager.ts +++ b/packages/napcat-webui-backend/src/terminal/terminal_manager.ts @@ -6,7 +6,7 @@ import { WebSocket, WebSocketServer } from 'ws'; import os from 'os'; import { IPty, spawn as ptySpawn } from 'napcat-pty'; import { randomUUID } from 'crypto'; -import { LogWrapper } from '@/napcat-core/helper/log'; +import { ILogWrapper } from 'napcat-common/src/log-interface'; interface TerminalInstance { pty: IPty; // 改用 PTY 实例 @@ -22,7 +22,7 @@ class TerminalManager { private terminals: Map = new Map(); private wss: WebSocketServer | null = null; - initialize (req: any, socket: any, head: any, logger?: LogWrapper) { + initialize (req: any, socket: any, head: any, logger?: ILogWrapper) { logger?.log('[NapCat] [WebUi] terminal websocket initialized'); this.wss = new WebSocketServer({ noServer: true, diff --git a/packages/napcat-webui-backend/src/types/index.ts b/packages/napcat-webui-backend/src/types/index.ts index 3d4a97d9..79f9cfe5 100644 --- a/packages/napcat-webui-backend/src/types/index.ts +++ b/packages/napcat-webui-backend/src/types/index.ts @@ -1,6 +1,20 @@ -import type { LoginListItem, SelfInfo } from 'napcat-core'; -import type { OneBotConfig } from 'napcat-onebot/config/config'; +import type { OneBotConfig } from '@/napcat-webui-backend/src/onebot/config'; +export interface LoginListItem { + uin: string; + uid: string; + nickName: string; + faceUrl: string; + facePath: string; + loginType: 1; // 1是二维码登录? + isQuickLogin: boolean; // 是否可以快速登录 + isAutoLogin: boolean; // 是否可以自动登录 +} +interface SelfInfo { + uid: string; + uin: string; + nick: string; +} export interface WebUiConfigType { host: string; port: number; @@ -29,7 +43,7 @@ export interface LoginRuntimeType { onWebUiTokenChange: (token: string) => Promise; WebUiConfigQuickFunction: () => Promise; NapCatHelper: { - onQuickLoginRequested: (uin: string) => Promise<{ result: boolean; message: string }>; + onQuickLoginRequested: (uin: string) => Promise<{ result: boolean; message: string; }>; onOB11ConfigChanged: (ob11: OneBotConfig) => Promise; QQLoginList: string[]; NewQQLoginList: LoginListItem[];