Refactor interfaces and decouple backend dependencies

Introduced new interface definitions in napcat-common for logging, status, and subscription. Refactored napcat-webui-backend to use these interfaces, decoupling it from napcat-core and napcat-onebot. Moved OneBot config schema to backend and updated imports. Updated framework and shell to pass subscriptions to InitWebUi. Improved type safety and modularity across backend and shared packages.
This commit is contained in:
手瓜一十雪 2025-11-16 10:58:30 +08:00
parent 6068abdec0
commit 3d3f718fd5
15 changed files with 248 additions and 69 deletions

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -3,6 +3,7 @@ import { truncateString } from 'napcat-common/src/helper';
import path from 'node:path'; import path from 'node:path';
import fs from 'node:fs/promises'; import fs from 'node:fs/promises';
import { NTMsgAtType, ChatType, ElementType, MessageElement, RawMessage, SelfInfo } from '@/napcat-core/index'; import { NTMsgAtType, ChatType, ElementType, MessageElement, RawMessage, SelfInfo } from '@/napcat-core/index';
import { ILogWrapper } from 'napcat-common/src/log-interface';
import EventEmitter from 'node:events'; import EventEmitter from 'node:events';
export enum LogLevel { export enum LogLevel {
DEBUG = 'debug', DEBUG = 'debug',
@ -56,7 +57,7 @@ class Subscription {
export const logSubscription = new Subscription(); export const logSubscription = new Subscription();
export class LogWrapper { export class LogWrapper implements ILogWrapper {
fileLogEnabled = true; fileLogEnabled = true;
consoleLogEnabled = true; consoleLogEnabled = true;
logger: winston.Logger; logger: winston.Logger;

View File

@ -1,24 +1,24 @@
import os from 'node:os'; import os from 'node:os';
import EventEmitter from 'node:events'; import EventEmitter from 'node:events';
import { IStatusHelperSubscription } from 'napcat-common/src/status-interface';
export interface SystemStatus { export interface SystemStatus {
cpu: { cpu: {
model: string, model: string,
speed: string speed: string;
usage: { usage: {
system: string system: string;
qq: string qq: string;
}, },
core: number core: number;
}, },
memory: { memory: {
total: string total: string;
usage: { usage: {
system: string system: string;
qq: string qq: string;
} };
}, },
arch: string arch: string;
} }
export class StatusHelper { export class StatusHelper {
@ -101,7 +101,7 @@ export class StatusHelper {
} }
} }
class StatusHelperSubscription extends EventEmitter { class StatusHelperSubscription extends EventEmitter implements IStatusHelperSubscription {
private statusHelper: StatusHelper; private statusHelper: StatusHelper;
private interval: NodeJS.Timeout | null = null; private interval: NodeJS.Timeout | null = null;

View File

@ -3,10 +3,11 @@ import { InitWebUi, WebUiConfig, webUiRuntimePort } from 'napcat-webui-backend/i
import { NapCatOneBot11Adapter } from 'napcat-onebot/index'; import { NapCatOneBot11Adapter } from 'napcat-onebot/index';
import { NativePacketHandler } from 'napcat-core/packet/handler/client'; import { NativePacketHandler } from 'napcat-core/packet/handler/client';
import { FFmpegService } from 'napcat-core/helper/ffmpeg/ffmpeg'; 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 { QQBasicInfoWrapper } from '@/napcat-core/helper/qq-basic-info';
import { InstanceContext, loadQQWrapper, NapCatCore, NapCatCoreWorkingEnv, NodeIKernelLoginListener, NodeIKernelLoginService, NodeIQQNTWrapperSession, SelfInfo, WrapperNodeApi } from '@/napcat-core'; import { InstanceContext, loadQQWrapper, NapCatCore, NapCatCoreWorkingEnv, NodeIKernelLoginListener, NodeIKernelLoginService, NodeIQQNTWrapperSession, SelfInfo, WrapperNodeApi } from '@/napcat-core';
import { proxiedListenerOf } from '@/napcat-core/helper/proxy-handler'; import { proxiedListenerOf } from '@/napcat-core/helper/proxy-handler';
import { statusHelperSubscription } from '@/napcat-core/helper/status';
// Framework ES入口文件 // Framework ES入口文件
export async function getWebUiUrl () { export async function getWebUiUrl () {
@ -72,7 +73,7 @@ export async function NCoreInitFramework (
await loaderObject.core.initCore(); await loaderObject.core.initCore();
// 启动WebUi // 启动WebUi
InitWebUi(logger, pathWrapper).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(); await new NapCatOneBot11Adapter(loaderObject.core, loaderObject.context, pathWrapper).InitOneBot();
} }

View File

@ -31,9 +31,10 @@ import { sleep } from 'napcat-common/src/helper';
import { FFmpegService } from '@/napcat-core/helper/ffmpeg/ffmpeg'; import { FFmpegService } from '@/napcat-core/helper/ffmpeg/ffmpeg';
import { connectToNamedPipe } from './pipe'; import { connectToNamedPipe } from './pipe';
import { NativePacketHandler } from 'napcat-core/packet/handler/client'; 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 { proxiedListenerOf } from '@/napcat-core/helper/proxy-handler';
import { QQBasicInfoWrapper } from '@/napcat-core/helper/qq-basic-info'; import { QQBasicInfoWrapper } from '@/napcat-core/helper/qq-basic-info';
import { statusHelperSubscription } from '@/napcat-core/helper/status';
// NapCat Shell App ES 入口文件 // NapCat Shell App ES 入口文件
async function handleUncaughtExceptions (logger: LogWrapper) { async function handleUncaughtExceptions (logger: LogWrapper) {
process.on('uncaughtException', (err) => { process.on('uncaughtException', (err) => {
@ -337,7 +338,7 @@ export async function NCoreInitShell () {
o3Service.addO3MiscListener(new NodeIO3MiscListener()); o3Service.addO3MiscListener(new NodeIO3MiscListener());
logger.log('[NapCat] [Core] NapCat.Core Version: ' + napCatVersion); 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 engine = wrapper.NodeIQQNTWrapperEngine.get();
const loginService = wrapper.NodeIKernelLoginService.get(); const loginService = wrapper.NodeIKernelLoginService.get();

View File

@ -19,8 +19,9 @@ import multer from 'multer';
import * as net from 'node:net'; import * as net from 'node:net';
import { WebUiDataRuntime } from './src/helper/Data'; import { WebUiDataRuntime } from './src/helper/Data';
import { existsSync, readFileSync } from 'node:fs'; // 引入multer用于错误捕获 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 // 实例化Express
const app = express(); const app = express();
/** /**
@ -31,6 +32,8 @@ const app = express();
*/ */
export let WebUiConfig: WebUiConfigWrapper; export let WebUiConfig: WebUiConfigWrapper;
export let webUiPathWrapper: NapCatPathWrapper; export let webUiPathWrapper: NapCatPathWrapper;
export let logSubscription: ISubscription;
export let statusHelperSubscription: IStatusHelperSubscription;
const MAX_PORT_TRY = 100; const MAX_PORT_TRY = 100;
export let webUiRuntimePort = 6099; 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 { try {
const certPath = join(webUiPathWrapper.configPath, 'cert.pem'); const certPath = join(webUiPathWrapper.configPath, 'cert.pem');
const keyPath = join(webUiPathWrapper.configPath, 'key.pem'); const keyPath = join(webUiPathWrapper.configPath, 'key.pem');
@ -85,8 +88,10 @@ async function checkCertificates (logger: LogWrapper): Promise<{ key: string, ce
return null; return null;
} }
} }
export async function InitWebUi (logger: LogWrapper, pathWrapper: NapCatPathWrapper) { export async function InitWebUi (logger: ILogWrapper, pathWrapper: NapCatPathWrapper, Subscription: ISubscription, statusSubscription: IStatusHelperSubscription) {
webUiPathWrapper = pathWrapper; webUiPathWrapper = pathWrapper;
logSubscription = Subscription;
statusHelperSubscription = statusSubscription;
WebUiConfig = new WebUiConfigWrapper(); WebUiConfig = new WebUiConfigWrapper();
let config = await WebUiConfig.GetWebUIConfig(); let config = await WebUiConfig.GetWebUIConfig();
@ -216,11 +221,11 @@ export async function InitWebUi (logger: LogWrapper, pathWrapper: NapCatPathWrap
const searchParams = { token }; const searchParams = { token };
logger.log(`[NapCat] [WebUi] WebUi Token: ${token}`); logger.log(`[NapCat] [WebUi] WebUi Token: ${token}`);
logger.log( 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 !== '') { if (host !== '') {
logger.log( 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)}`
); );
} }
}); });

View File

@ -1,41 +1,39 @@
{ {
"name": "napcat-webui-backend", "name": "napcat-webui-backend",
"version": "0.0.1", "version": "0.0.1",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "index.ts", "main": "index.ts",
"scripts": { "scripts": {
"typecheck": "tsc --noEmit --skipLibCheck -p tsconfig.json" "typecheck": "tsc --noEmit --skipLibCheck -p tsconfig.json"
},
"exports": {
".": {
"import": "./index.ts"
}, },
"exports": { "./*": {
".": { "import": "./*"
"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"
} }
},
"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"
}
} }

View File

@ -1,8 +1,7 @@
import type { RequestHandler } from 'express'; import type { RequestHandler } from 'express';
import { sendError, sendSuccess } from '../utils/response'; import { sendError, sendSuccess } from '../utils/response';
import { logSubscription } from 'napcat-core/helper/log';
import { terminalManager } from '../terminal/terminal_manager'; import { terminalManager } from '../terminal/terminal_manager';
import { WebUiConfig } from '@/napcat-webui-backend/index'; import { logSubscription, WebUiConfig } from '@/napcat-webui-backend/index';
// 判断是否是 macos // 判断是否是 macos
const isMacOS = process.platform === 'darwin'; const isMacOS = process.platform === 'darwin';

View File

@ -1,7 +1,7 @@
import { RequestHandler } from 'express'; import { RequestHandler } from 'express';
import { existsSync, readFileSync } from 'node:fs'; import { existsSync, readFileSync } from 'node:fs';
import { resolve } from 'node:path'; 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 { webUiPathWrapper } from '@/napcat-webui-backend/index';
import { WebUiDataRuntime } from '@/napcat-webui-backend/src/helper/Data'; import { WebUiDataRuntime } from '@/napcat-webui-backend/src/helper/Data';
import { sendError, sendSuccess } from '@/napcat-webui-backend/src/utils/response'; import { sendError, sendSuccess } from '@/napcat-webui-backend/src/utils/response';

View File

@ -1,6 +1,6 @@
import { statusHelperSubscription } from '@/napcat-webui-backend';
import { RequestHandler } from 'express'; 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) => { export const StatusRealTimeHandler: RequestHandler = async (req, res) => {
res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Connection', 'keep-alive'); res.setHeader('Connection', 'keep-alive');

View File

@ -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<typeof OneBotConfigSchema>;
export type HttpServerConfig = Static<typeof HttpServerConfigSchema>;
export type HttpSseServerConfig = Static<typeof HttpSseServerConfigSchema>;
export type HttpClientConfig = Static<typeof HttpClientConfigSchema>;
export type WebsocketServerConfig = Static<typeof WebsocketServerConfigSchema>;
export type WebsocketClientConfig = Static<typeof WebsocketClientConfigSchema>;
export type PluginConfig = Static<typeof PluginConfigSchema>;
export type NetworkAdapterConfig = HttpServerConfig | HttpSseServerConfig | HttpClientConfig | WebsocketServerConfig | WebsocketClientConfig | PluginConfig;
export type NetworkConfigKey = keyof OneBotConfig['network'];
export function loadConfig (config: Partial<OneBotConfig>): 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;
}

View File

@ -6,7 +6,7 @@ import { WebSocket, WebSocketServer } from 'ws';
import os from 'os'; import os from 'os';
import { IPty, spawn as ptySpawn } from 'napcat-pty'; import { IPty, spawn as ptySpawn } from 'napcat-pty';
import { randomUUID } from 'crypto'; import { randomUUID } from 'crypto';
import { LogWrapper } from '@/napcat-core/helper/log'; import { ILogWrapper } from 'napcat-common/src/log-interface';
interface TerminalInstance { interface TerminalInstance {
pty: IPty; // 改用 PTY 实例 pty: IPty; // 改用 PTY 实例
@ -22,7 +22,7 @@ class TerminalManager {
private terminals: Map<string, TerminalInstance> = new Map(); private terminals: Map<string, TerminalInstance> = new Map();
private wss: WebSocketServer | null = null; 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'); logger?.log('[NapCat] [WebUi] terminal websocket initialized');
this.wss = new WebSocketServer({ this.wss = new WebSocketServer({
noServer: true, noServer: true,

View File

@ -1,6 +1,20 @@
import type { LoginListItem, SelfInfo } from 'napcat-core'; import type { OneBotConfig } from '@/napcat-webui-backend/src/onebot/config';
import type { OneBotConfig } from 'napcat-onebot/config/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 { export interface WebUiConfigType {
host: string; host: string;
port: number; port: number;
@ -29,7 +43,7 @@ export interface LoginRuntimeType {
onWebUiTokenChange: (token: string) => Promise<void>; onWebUiTokenChange: (token: string) => Promise<void>;
WebUiConfigQuickFunction: () => Promise<void>; WebUiConfigQuickFunction: () => Promise<void>;
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>;
QQLoginList: string[]; QQLoginList: string[];
NewQQLoginList: LoginListItem[]; NewQQLoginList: LoginListItem[];