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

View File

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

View File

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

View File

@ -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();

View File

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

View File

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

View File

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

View File

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

View File

@ -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');

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 { 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<string, TerminalInstance> = 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,

View File

@ -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<void>;
WebUiConfigQuickFunction: () => Promise<void>;
NapCatHelper: {
onQuickLoginRequested: (uin: string) => Promise<{ result: boolean; message: string }>;
onQuickLoginRequested: (uin: string) => Promise<{ result: boolean; message: string; }>;
onOB11ConfigChanged: (ob11: OneBotConfig) => Promise<void>;
QQLoginList: string[];
NewQQLoginList: LoginListItem[];