Compare commits

..

7 Commits

Author SHA1 Message Date
时瑾
5bb8f9af8d fix: 刷新二维码清楚错误信息 2026-01-17 15:35:11 +08:00
时瑾
1b8860ea7d Merge branch 'main' into feature/relogin-enhancement 2026-01-17 15:24:44 +08:00
时瑾
434bc69ddb refactor: 重构重启流程,移除旧的重启逻辑,新增基于 WebUI 的重启请求处理 2026-01-17 15:24:03 +08:00
时瑾
de33ab10e5 Merge branch 'main' into feature/relogin-enhancement 2026-01-17 14:48:40 +08:00
时瑾
c44a7e4b57 cp napcat-shell-loader/launcher-win.bat 2026-01-14 23:27:26 +08:00
时瑾
b97a224a14 feat: 新增看门狗汪汪汪 2026-01-14 23:25:54 +08:00
时瑾
0918b17257 feat: 优化离线重连机制,增加前端登录错误提示与二维码刷新功能
- 增加全局掉线检测弹窗
- 增强登录错误解析,支持显示 serverErrorCode 和 message
- 优化二维码登录 UI,错误时显示详细原因并提供大按钮重新获取
- 核心层解耦,通过事件抛出 KickedOffLine 通知
- 支持前端点击刷新二维码接口
2026-01-14 18:54:59 +08:00
25 changed files with 185 additions and 1220 deletions

View File

@@ -41,7 +41,6 @@ jobs:
pnpm test || exit 1
pnpm --filter napcat-webui-frontend run build || exit 1
pnpm run build:framework
pnpm --filter napcat-plugin-builtin run build || exit 1
mv packages/napcat-framework/dist framework-dist
cd framework-dist
npm install --omit=dev
@@ -84,7 +83,6 @@ jobs:
pnpm test || exit 1
pnpm --filter napcat-webui-frontend run build || exit 1
pnpm run build:shell
pnpm --filter napcat-plugin-builtin run build || exit 1
mv packages/napcat-shell/dist shell-dist
cd shell-dist
npm install --omit=dev

View File

@@ -10,7 +10,7 @@ permissions: write-all
env:
OPENROUTER_API_URL: https://91vip.futureppo.top/v1/chat/completions
OPENROUTER_MODEL: "copilot/gemini-3-flash-preview"
OPENROUTER_MODEL: "copilot/ant/gemini-3-flash-preview"
RELEASE_NAME: "NapCat"
jobs:
@@ -62,7 +62,6 @@ jobs:
pnpm i
pnpm --filter napcat-webui-frontend run build || exit 1
pnpm run build:framework
pnpm --filter napcat-plugin-builtin run build || exit 1
mv packages/napcat-framework/dist framework-dist
cd framework-dist
npm install --omit=dev
@@ -92,7 +91,6 @@ jobs:
pnpm i
pnpm --filter napcat-webui-frontend run build || exit 1
pnpm run build:shell
pnpm --filter napcat-plugin-builtin run build || exit 1
mv packages/napcat-shell/dist shell-dist
cd shell-dist
npm install --omit=dev

View File

@@ -67,8 +67,6 @@ import GoCQHTTPUploadPrivateFile from './go-cqhttp/UploadPrivateFile';
import { FetchEmojiLike } from './extends/FetchEmojiLike';
import { NapCatCore } from 'napcat-core';
import { NapCatOneBot11Adapter } from '@/napcat-onebot/index';
import type { NetworkAdapterConfig } from '../config/config';
import { OneBotAction } from './OneBotAction';
import { SetInputStatus } from './extends/SetInputStatus';
import { GetCSRF } from './system/GetCSRF';
import { DelGroupNotice } from './group/DelGroupNotice';
@@ -324,30 +322,6 @@ export function createActionMap (obContext: NapCatOneBot11Adapter, core: NapCatC
function get<K extends keyof MapType> (key: K): MapType[K] | undefined {
return _map.get(key as keyof MapType) as MapType[K] | undefined;
}
/**
* 类型安全的 action 调用辅助函数
* 根据 action 名称自动推导返回类型
*/
async function call<K extends keyof MapType> (
actionName: K,
params: unknown,
adapter: string,
config: NetworkAdapterConfig
): Promise<MapType[K] extends OneBotAction<any, infer R> ? R : never> {
const action = _map.get(actionName);
if (!action) {
throw new Error(`Action ${String(actionName)} not found`);
}
const result = await (action as any).handle(params, adapter, config);
if (result.status !== 'ok' || !result.data) {
throw new Error(`Action ${String(actionName)} failed: ${result.message || 'No data returned'}`);
}
return result.data;
}
return { get, call };
return { get };
}
export type ActionMap = ReturnType<typeof createActionMap>;

View File

@@ -587,33 +587,15 @@ export class OneBotMsgApi {
return at(atQQ, uid, NTMsgAtType.ATTYPEONE, info.nick || '');
},
[OB11MessageDataType.reply]: async ({ data: { id, seq } }, context) => {
let replyMsg: RawMessage | undefined;
let replyMsgPeer: Peer | undefined;
// 优先使用 seq
if (seq) {
const msgList = (await this.core.apis.MsgApi.getMsgsBySeqAndCount(
context.peer, seq.toString(), 1, true, true
)).msgList;
replyMsg = msgList[0];
replyMsgPeer = context.peer;
} else if (id) {
// 降级使用 id
const replyMsgM = MessageUnique.getMsgIdAndPeerByShortId(parseInt(id));
if (!replyMsgM) {
this.core.context.logger.logWarn('回复消息不存在', id);
return undefined;
}
replyMsg = (await this.core.apis.MsgApi.getMsgsByMsgId(
replyMsgM.Peer, [replyMsgM.MsgId])).msgList[0];
replyMsgPeer = replyMsgM.Peer;
} else {
this.core.context.logger.logWarn('回复消息缺少id或seq参数');
[OB11MessageDataType.reply]: async ({ data: { id } }) => {
const replyMsgM = MessageUnique.getMsgIdAndPeerByShortId(parseInt(id));
if (!replyMsgM) {
this.core.context.logger.logWarn('回复消息不存在', id);
return undefined;
}
return replyMsg && replyMsgPeer
const replyMsg = (await this.core.apis.MsgApi.getMsgsByMsgId(
replyMsgM.Peer, [replyMsgM.MsgId])).msgList[0];
return replyMsg
? {
elementType: ElementType.REPLY,
elementId: '',
@@ -623,7 +605,7 @@ export class OneBotMsgApi {
senderUin: replyMsg.senderUin,
senderUinStr: replyMsg.senderUin,
replyMsgClientSeq: replyMsg.clientSeq,
_replyMsgPeer: replyMsgPeer,
_replyMsgPeer: replyMsgM.Peer,
},
}
: undefined;

View File

@@ -49,11 +49,10 @@ import {
OneBotConfigSchema,
} from './config/config';
import { OB11Message } from './types';
import { existsSync } from 'node:fs';
import { IOB11NetworkAdapter } from '@/napcat-onebot/network/adapter';
import { OB11HttpSSEServerAdapter } from './network/http-server-sse';
import { OB11PluginMangerAdapter } from './network/plugin-manger';
import { existsSync } from 'node:fs';
import { proxiedListenerOf } from '@/napcat-core/helper/proxy-handler';
import { OneBotFileApi } from './api/file';
@@ -161,7 +160,6 @@ export class NapCatOneBot11Adapter {
// this.networkManager.registerAdapter(
// new OB11PluginAdapter('myPlugin', this.core, this,this.actions)
// );
// 检查插件目录是否存在,不存在则不加载插件管理器
if (existsSync(this.context.pathWrapper.pluginPath)) {
this.context.logger.log('[Plugins] 插件目录存在,开始加载插件');
this.networkManager.registerAdapter(

View File

@@ -1,9 +1,9 @@
import { ActionMap } from '../action';
import { NapCatCore } from 'napcat-core';
import { NapCatOneBot11Adapter, OB11Message } from '@/napcat-onebot/index';
import { OB11EmitEventContent, OB11NetworkReloadType } from './index';
import { IOB11NetworkAdapter } from '@/napcat-onebot/network/adapter';
import { NapCatOneBot11Adapter, OB11Message } from '@/napcat-onebot/index';
import { NapCatCore } from 'napcat-core';
import { PluginConfig } from '../config/config';
import { ActionMap } from '../action';
import { IOB11NetworkAdapter } from '@/napcat-onebot/network/adapter';
import fs from 'fs';
import path from 'path';
@@ -11,39 +11,13 @@ export interface PluginPackageJson {
name?: string;
version?: string;
main?: string;
description?: string;
author?: string;
}
export interface PluginModule<T extends OB11EmitEventContent = OB11EmitEventContent> {
plugin_init: (
core: NapCatCore,
obContext: NapCatOneBot11Adapter,
actions: ActionMap,
instance: OB11PluginMangerAdapter
) => void | Promise<void>;
plugin_onmessage?: (
adapter: string,
core: NapCatCore,
obCtx: NapCatOneBot11Adapter,
event: OB11Message,
actions: ActionMap,
instance: OB11PluginMangerAdapter
) => void | Promise<void>;
plugin_onevent?: (
adapter: string,
core: NapCatCore,
obCtx: NapCatOneBot11Adapter,
event: T,
actions: ActionMap,
instance: OB11PluginMangerAdapter
) => void | Promise<void>;
plugin_cleanup?: (
core: NapCatCore,
obContext: NapCatOneBot11Adapter,
actions: ActionMap,
instance: OB11PluginMangerAdapter
) => void | Promise<void>;
plugin_init: (core: NapCatCore, obContext: NapCatOneBot11Adapter, actions: ActionMap, instance: OB11PluginMangerAdapter) => void | Promise<void>;
plugin_onmessage?: (adapter: string, core: NapCatCore, obCtx: NapCatOneBot11Adapter, event: OB11Message, actions: ActionMap, instance: OB11PluginMangerAdapter) => void | Promise<void>;
plugin_onevent?: (adapter: string, core: NapCatCore, obCtx: NapCatOneBot11Adapter, event: T, actions: ActionMap, instance: OB11PluginMangerAdapter) => void | Promise<void>;
plugin_cleanup?: (core: NapCatCore, obContext: NapCatOneBot11Adapter, actions: ActionMap, instance: OB11PluginMangerAdapter) => void | Promise<void>;
}
export interface LoadedPlugin {
@@ -55,25 +29,16 @@ export interface LoadedPlugin {
module: PluginModule;
}
export interface PluginStatusConfig {
[key: string]: boolean; // key: pluginName, value: enabled
}
export class OB11PluginMangerAdapter extends IOB11NetworkAdapter<PluginConfig> {
private readonly pluginPath: string;
private readonly configPath: string;
private loadedPlugins: Map<string, LoadedPlugin> = new Map();
declare config: PluginConfig;
override get isActive (): boolean {
return this.isEnable && this.loadedPlugins.size > 0;
}
constructor (
name: string,
core: NapCatCore,
obContext: NapCatOneBot11Adapter,
actions: ActionMap
name: string, core: NapCatCore, obContext: NapCatOneBot11Adapter, actions: ActionMap
) {
const config = {
name,
@@ -84,60 +49,24 @@ export class OB11PluginMangerAdapter extends IOB11NetworkAdapter<PluginConfig> {
};
super(name, config, core, obContext, actions);
this.pluginPath = this.core.context.pathWrapper.pluginPath;
this.configPath = path.join(this.core.context.pathWrapper.configPath, 'plugins.json');
}
private loadPluginConfig (): PluginStatusConfig {
if (fs.existsSync(this.configPath)) {
try {
return JSON.parse(fs.readFileSync(this.configPath, 'utf-8'));
} catch (e) {
this.logger.logWarn('[Plugin Adapter] Error parsing plugins.json', e);
}
}
return {};
}
private savePluginConfig (config: PluginStatusConfig) {
try {
fs.writeFileSync(this.configPath, JSON.stringify(config, null, 2), 'utf-8');
} catch (e) {
this.logger.logError('[Plugin Adapter] Error saving plugins.json', e);
}
}
/**
* 扫描并加载插件
*/
* 扫描并加载插件
*/
private async loadPlugins (): Promise<void> {
try {
// 确保插件目录存在
if (!fs.existsSync(this.pluginPath)) {
this.logger.logWarn(
`[Plugin Adapter] Plugin directory does not exist: ${this.pluginPath}`
);
this.logger.logWarn(`[Plugin Adapter] Plugin directory does not exist: ${this.pluginPath}`);
fs.mkdirSync(this.pluginPath, { recursive: true });
return;
}
const items = fs.readdirSync(this.pluginPath, { withFileTypes: true });
const pluginConfig = this.loadPluginConfig();
// 扫描文件和目录
for (const item of items) {
let pluginName = '';
if (item.isFile()) {
pluginName = path.parse(item.name).name;
} else if (item.isDirectory()) {
pluginName = item.name;
}
// Check if plugin is disabled in config
if (pluginConfig[pluginName] === false) {
this.logger.log(`[Plugin Adapter] Plugin ${pluginName} is disabled in config, skipping`);
continue;
}
if (item.isFile()) {
// 处理单文件插件
await this.loadFilePlugin(item.name);
@@ -147,18 +76,16 @@ export class OB11PluginMangerAdapter extends IOB11NetworkAdapter<PluginConfig> {
}
}
this.logger.log(
`[Plugin Adapter] Loaded ${this.loadedPlugins.size} plugins`
);
this.logger.log(`[Plugin Adapter] Loaded ${this.loadedPlugins.size} plugins`);
} catch (error) {
this.logger.logError('[Plugin Adapter] Error loading plugins:', error);
}
}
/**
* 加载单文件插件 (.mjs, .js)
*/
public async loadFilePlugin (filename: string): Promise<void> {
* 加载单文件插件 (.mjs, .js)
*/
private async loadFilePlugin (filename: string): Promise<void> {
// 只处理支持的文件类型
if (!this.isSupportedFile(filename)) {
return;
@@ -166,20 +93,11 @@ export class OB11PluginMangerAdapter extends IOB11NetworkAdapter<PluginConfig> {
const filePath = path.join(this.pluginPath, filename);
const pluginName = path.parse(filename).name;
const pluginConfig = this.loadPluginConfig();
// Check if plugin is disabled in config
if (pluginConfig[pluginName] === false) {
this.logger.log(`[Plugin Adapter] Plugin ${pluginName} is disabled by user`);
return;
}
try {
const module = await this.importModule(filePath);
if (!this.isValidPluginModule(module)) {
this.logger.logWarn(
`[Plugin Adapter] File ${filename} is not a valid plugin (missing plugin methods)`
);
this.logger.logWarn(`[Plugin Adapter] File ${filename} is not a valid plugin (missing plugin methods)`);
return;
}
@@ -192,31 +110,15 @@ export class OB11PluginMangerAdapter extends IOB11NetworkAdapter<PluginConfig> {
await this.registerPlugin(plugin);
} catch (error) {
this.logger.logError(
`[Plugin Adapter] Error loading file plugin ${filename}:`,
error
);
this.logger.logError(`[Plugin Adapter] Error loading file plugin ${filename}:`, error);
}
}
/**
* 加载目录插件
*/
public async loadDirectoryPlugin (dirname: string): Promise<void> {
* 加载目录插件
*/
private async loadDirectoryPlugin (dirname: string): Promise<void> {
const pluginDir = path.join(this.pluginPath, dirname);
const pluginConfig = this.loadPluginConfig();
// Ideally we'd get the name from package.json first, but we can use dirname as a fallback identifier initially.
// However, the list scan uses item.name (dirname) as the key. Let's stick to using dirname/filename as the config key for simplicity and consistency.
// Wait, package.json name might override. But for management, consistent ID is better.
// Let's check config after parsing package.json?
// User expects to disable 'plugin-name'. But if multiple folders have same name? Not handled.
// Let's use dirname as the key for config to be consistent with file system.
if (pluginConfig[dirname] === false) {
this.logger.log(`[Plugin Adapter] Plugin ${dirname} is disabled by user`);
return;
}
try {
// 尝试读取 package.json
@@ -228,22 +130,14 @@ export class OB11PluginMangerAdapter extends IOB11NetworkAdapter<PluginConfig> {
const packageContent = fs.readFileSync(packageJsonPath, 'utf-8');
packageJson = JSON.parse(packageContent);
} catch (error) {
this.logger.logWarn(
`[Plugin Adapter] Invalid package.json in ${dirname}:`,
error
);
this.logger.logWarn(`[Plugin Adapter] Invalid package.json in ${dirname}:`, error);
}
}
// Check if disabled by package name IF package.json exists?
// No, file system name is more reliable ID for resource management here.
// 确定入口文件
const entryFile = this.findEntryFile(pluginDir, packageJson);
if (!entryFile) {
this.logger.logWarn(
`[Plugin Adapter] No valid entry file found for plugin directory: ${dirname}`
);
this.logger.logWarn(`[Plugin Adapter] No valid entry file found for plugin directory: ${dirname}`);
return;
}
@@ -251,9 +145,7 @@ export class OB11PluginMangerAdapter extends IOB11NetworkAdapter<PluginConfig> {
const module = await this.importModule(entryPath);
if (!this.isValidPluginModule(module)) {
this.logger.logWarn(
`[Plugin Adapter] Directory ${dirname} does not contain a valid plugin`
);
this.logger.logWarn(`[Plugin Adapter] Directory ${dirname} does not contain a valid plugin`);
return;
}
@@ -268,20 +160,14 @@ export class OB11PluginMangerAdapter extends IOB11NetworkAdapter<PluginConfig> {
await this.registerPlugin(plugin);
} catch (error) {
this.logger.logError(
`[Plugin Adapter] Error loading directory plugin ${dirname}:`,
error
);
this.logger.logError(`[Plugin Adapter] Error loading directory plugin ${dirname}:`, error);
}
}
/**
* 查找插件目录的入口文件
*/
private findEntryFile (
pluginDir: string,
packageJson?: PluginPackageJson
): string | null {
* 查找插件目录的入口文件
*/
private findEntryFile (pluginDir: string, packageJson?: PluginPackageJson): string | null {
// 优先级package.json main > 默认文件名
const possibleEntries = [
packageJson?.main,
@@ -302,69 +188,53 @@ export class OB11PluginMangerAdapter extends IOB11NetworkAdapter<PluginConfig> {
}
/**
* 检查是否为支持的文件类型
*/
* 检查是否为支持的文件类型
*/
private isSupportedFile (filename: string): boolean {
const ext = path.extname(filename).toLowerCase();
return ['.mjs', '.js'].includes(ext);
}
/**
* 动态导入模块
*/
* 动态导入模块
*/
private async importModule (filePath: string): Promise<any> {
const fileUrl = `file://${filePath.replace(/\\/g, '/')}`;
// Add timestamp to force reload cache if supported or just import
// Note: dynamic import caching is tricky in ESM. Adding query param might help?
const fileUrlWithQuery = `${fileUrl}?t=${Date.now()}`;
return await import(fileUrlWithQuery);
return await import(fileUrl);
}
/**
* 检查模块是否为有效的插件模块
*/
* 检查模块是否为有效的插件模块
*/
private isValidPluginModule (module: any): module is PluginModule {
return module && typeof module.plugin_init === 'function';
}
/**
* 注册插件
*/
* 注册插件
*/
private async registerPlugin (plugin: LoadedPlugin): Promise<void> {
// 检查名称冲突
if (this.loadedPlugins.has(plugin.name)) {
this.logger.logWarn(
`[Plugin Adapter] Plugin name conflict: ${plugin.name}, skipping...`
);
this.logger.logWarn(`[Plugin Adapter] Plugin name conflict: ${plugin.name}, skipping...`);
return;
}
this.loadedPlugins.set(plugin.name, plugin);
this.logger.log(
`[Plugin Adapter] Registered plugin: ${plugin.name}${plugin.version ? ` v${plugin.version}` : ''
}`
);
this.logger.log(`[Plugin Adapter] Registered plugin: ${plugin.name}${plugin.version ? ` v${plugin.version}` : ''}`);
// 调用插件初始化方法(必须存在)
try {
await plugin.module.plugin_init(
this.core,
this.obContext,
this.actions,
this
);
await plugin.module.plugin_init(this.core, this.obContext, this.actions, this);
this.logger.log(`[Plugin Adapter] Initialized plugin: ${plugin.name}`);
} catch (error) {
this.logger.logError(
`[Plugin Adapter] Error initializing plugin ${plugin.name}:`,
error
);
this.logger.logError(`[Plugin Adapter] Error initializing plugin ${plugin.name}:`, error);
}
}
/**
* 卸载插件
*/
* 卸载插件
*/
private async unloadPlugin (pluginName: string): Promise<void> {
const plugin = this.loadedPlugins.get(pluginName);
if (!plugin) {
@@ -374,18 +244,10 @@ export class OB11PluginMangerAdapter extends IOB11NetworkAdapter<PluginConfig> {
// 调用插件清理方法
if (typeof plugin.module.plugin_cleanup === 'function') {
try {
await plugin.module.plugin_cleanup(
this.core,
this.obContext,
this.actions,
this
);
await plugin.module.plugin_cleanup(this.core, this.obContext, this.actions, this);
this.logger.log(`[Plugin Adapter] Cleaned up plugin: ${pluginName}`);
} catch (error) {
this.logger.logError(
`[Plugin Adapter] Error cleaning up plugin ${pluginName}:`,
error
);
this.logger.logError(`[Plugin Adapter] Error cleaning up plugin ${pluginName}:`, error);
}
}
@@ -393,69 +255,6 @@ export class OB11PluginMangerAdapter extends IOB11NetworkAdapter<PluginConfig> {
this.logger.log(`[Plugin Adapter] Unloaded plugin: ${pluginName}`);
}
public async unregisterPlugin (pluginName: string): Promise<void> {
return this.unloadPlugin(pluginName);
}
public getPluginPath (): string {
return this.pluginPath;
}
public getPluginConfig (): PluginStatusConfig {
return this.loadPluginConfig();
}
public setPluginStatus (pluginName: string, enable: boolean): void {
const config = this.loadPluginConfig();
config[pluginName] = enable;
this.savePluginConfig(config);
// If disabling, unload immediately if loaded
if (!enable) {
// Note: pluginName passed here might be the package name or the filename/dirname
// But our registerPlugin uses plugin.name which comes from package.json or dirname.
// This mismatch is tricky.
// Ideally, we should use a consistent ID.
// Let's assume pluginName passed here effectively matches the ID used in loadedPlugins.
// But wait, loadDirectoryPlugin logic: name = packageJson.name || dirname.
// config key = dirname.
// If packageJson.name != dirname, we have a problem.
// To fix this properly:
// 1. We need to know which LoadedPlugin corresponds to the enabled/disabled item.
// 2. Or we iterate loadedPlugins and find match.
for (const [_, loaded] of this.loadedPlugins.entries()) {
const dirOrFile = path.basename(loaded.pluginPath === this.pluginPath ? loaded.entryPath : loaded.pluginPath);
const ext = path.extname(dirOrFile);
const simpleName = ext ? path.parse(dirOrFile).name : dirOrFile; // filename without ext
// But wait, config key is the FILENAME (with ext for files?).
// In Scan loop:
// pluginName = path.parse(item.name).name (for file)
// pluginName = item.name (for dir)
// config[pluginName] check.
// So if file is "test.js", pluginName is "test". Config key "test".
// If dir is "test-plugin", pluginName is "test-plugin". Config key "test-plugin".
// loadedPlugin.name might be distinct.
// So we need to match loadedPlugin back to its fs source to unload it?
// loadedPlugin.entryPath or pluginPath helps.
// If it's a file plugin: loaded.entryPath ends with pluginName + ext.
// If it's a dir plugin: loaded.pluginPath ends with pluginName.
if (pluginName === simpleName) {
this.unloadPlugin(loaded.name).catch(e => this.logger.logError('Error unloading', e));
}
}
}
// If enabling, we need to load it.
// But we can just rely on the API handler to call loadFile/DirectoryPlugin which now checks config.
// Wait, if I call loadFilePlugin("test.js") and config says enable=true, it loads.
// API handler needs to change to pass filename/dirname.
}
async onEvent<T extends OB11EmitEventContent> (event: T) {
if (!this.isEnable) {
return;
@@ -474,44 +273,21 @@ export class OB11PluginMangerAdapter extends IOB11NetworkAdapter<PluginConfig> {
}
/**
* 调用插件的事件处理方法
*/
private async callPluginEventHandler (
plugin: LoadedPlugin,
event: OB11EmitEventContent
): Promise<void> {
* 调用插件的事件处理方法
*/
private async callPluginEventHandler (plugin: LoadedPlugin, event: OB11EmitEventContent): Promise<void> {
try {
// 优先使用 plugin_onevent 方法
if (typeof plugin.module.plugin_onevent === 'function') {
await plugin.module.plugin_onevent(
this.name,
this.core,
this.obContext,
event,
this.actions,
this
);
await plugin.module.plugin_onevent(this.name, this.core, this.obContext, event, this.actions, this);
}
// 如果是消息事件并且插件有 plugin_onmessage 方法,也调用
if (
(event as any).message_type &&
typeof plugin.module.plugin_onmessage === 'function'
) {
await plugin.module.plugin_onmessage(
this.name,
this.core,
this.obContext,
event as OB11Message,
this.actions,
this
);
if ((event as any).message_type && typeof plugin.module.plugin_onmessage === 'function') {
await plugin.module.plugin_onmessage(this.name, this.core, this.obContext, event as OB11Message, this.actions, this);
}
} catch (error) {
this.logger.logError(
`[Plugin Adapter] Error calling plugin ${plugin.name} event handler:`,
error
);
this.logger.logError(`[Plugin Adapter] Error calling plugin ${plugin.name} event handler:`, error);
}
}
@@ -526,9 +302,7 @@ export class OB11PluginMangerAdapter extends IOB11NetworkAdapter<PluginConfig> {
// 加载所有插件
await this.loadPlugins();
this.logger.log(
`[Plugin Adapter] Plugin adapter opened with ${this.loadedPlugins.size} plugins loaded`
);
this.logger.log(`[Plugin Adapter] Plugin adapter opened with ${this.loadedPlugins.size} plugins loaded`);
}
async close () {
@@ -560,22 +334,22 @@ export class OB11PluginMangerAdapter extends IOB11NetworkAdapter<PluginConfig> {
}
/**
* 获取已加载的插件列表
*/
* 获取已加载的插件列表
*/
public getLoadedPlugins (): LoadedPlugin[] {
return Array.from(this.loadedPlugins.values());
}
/**
* 获取插件信息
*/
* 获取插件信息
*/
public getPluginInfo (pluginName: string): LoadedPlugin | undefined {
return this.loadedPlugins.get(pluginName);
}
/**
* 重载指定插件
*/
* 重载指定插件
*/
public async reloadPlugin (pluginName: string): Promise<boolean> {
const plugin = this.loadedPlugins.get(pluginName);
if (!plugin) {
@@ -588,10 +362,8 @@ export class OB11PluginMangerAdapter extends IOB11NetworkAdapter<PluginConfig> {
await this.unloadPlugin(pluginName);
// 重新加载插件
// Use logic to re-determine if it is directory or file based on original paths
// Note: we can't fully trust fs status if it's gone.
const isDirectory =
plugin.pluginPath !== this.pluginPath; // Simple check: if path is nested, it's a dir plugin
const isDirectory = fs.statSync(plugin.pluginPath).isDirectory() &&
plugin.pluginPath !== this.pluginPath;
if (isDirectory) {
const dirname = path.basename(plugin.pluginPath);
@@ -601,15 +373,10 @@ export class OB11PluginMangerAdapter extends IOB11NetworkAdapter<PluginConfig> {
await this.loadFilePlugin(filename);
}
this.logger.log(
`[Plugin Adapter] Plugin ${pluginName} reloaded successfully`
);
this.logger.log(`[Plugin Adapter] Plugin ${pluginName} reloaded successfully`);
return true;
} catch (error) {
this.logger.logError(
`[Plugin Adapter] Error reloading plugin ${pluginName}:`,
error
);
this.logger.logError(`[Plugin Adapter] Error reloading plugin ${pluginName}:`, error);
return false;
}
}

View File

@@ -159,8 +159,7 @@ export interface OB11MessageAt {
export interface OB11MessageReply {
type: OB11MessageDataType.reply;
data: {
id?: string; // msg_id 的短ID映射
seq?: number; // msg_seq优先使用
id: string;
};
}

View File

@@ -1,84 +0,0 @@
import type { ActionMap } from 'napcat-onebot/action';
import { EventType } from 'napcat-onebot/event/OneBotEvent';
import type { PluginModule } from 'napcat-onebot/network/plugin';
import type { OB11Message, OB11PostSendMsg } from 'napcat-onebot/types/message';
let actions: ActionMap | undefined = undefined;
/**
* 插件初始化
*/
const plugin_init: PluginModule['plugin_init'] = async (_core, _obContext, _actions, _instance) => {
console.log('[Plugin: builtin] NapCat 内置插件已初始化');
actions = _actions;
};
/**
* 消息处理
* 当收到包含 #napcat 的消息时,回复版本信息
*/
const plugin_onmessage: PluginModule['plugin_onmessage'] = async (adapter, _core, _obCtx, event, _actions, instance) => {
if (event.post_type !== EventType.MESSAGE || !event.raw_message.startsWith('#napcat')) {
return;
}
try {
const versionInfo = await getVersionInfo(adapter, instance.config);
if (!versionInfo) return;
const message = formatVersionMessage(versionInfo);
await sendMessage(event, message, adapter, instance.config);
console.log('[Plugin: builtin] 已回复版本信息');
} catch (error) {
console.error('[Plugin: builtin] 处理消息时发生错误:', error);
}
};
/**
* 获取版本信息(完美的类型推导,无需 as 断言)
*/
async function getVersionInfo (adapter: string, config: any) {
if (!actions) return null;
try {
const data = await actions.call('get_version_info', void 0, adapter, config);
return {
appName: data.app_name,
appVersion: data.app_version,
protocolVersion: data.protocol_version,
};
} catch (error) {
console.error('[Plugin: builtin] 获取版本信息失败:', error);
return null;
}
}
/**
* 格式化版本信息消息
*/
function formatVersionMessage (info: { appName: string; appVersion: string; protocolVersion: string; }) {
return `NapCat 信息\n版本: ${info.appVersion}\n平台: ${process.platform}${process.arch === 'x64' ? ' (64-bit)' : ''}`;
}
/**
* 发送消息(完美的类型推导)
*/
async function sendMessage (event: OB11Message, message: string, adapter: string, config: any) {
if (!actions) return;
const params: OB11PostSendMsg = {
message,
message_type: event.message_type,
...(event.message_type === 'group' && event.group_id ? { group_id: String(event.group_id) } : {}),
...(event.message_type === 'private' && event.user_id ? { user_id: String(event.user_id) } : {}),
};
try {
await actions.call('send_msg', params, adapter, config);
} catch (error) {
console.error('[Plugin: builtin] 发送消息失败:', error);
}
}
export { plugin_init, plugin_onmessage };

View File

@@ -1,17 +0,0 @@
{
"name": "napcat-plugin-builtin",
"version": "1.0.0",
"type": "module",
"main": "index.mjs",
"description": "NapCat 内置插件",
"author": "NapNeko",
"dependencies": {
"napcat-onebot": "workspace:*"
},
"devDependencies": {
"@types/node": "^22.0.1"
},
"scripts": {
"build": "vite build"
}
}

View File

@@ -1,11 +0,0 @@
{
"extends": "../../tsconfig.base.json",
"include": [
"*.ts",
"**/*.ts"
],
"exclude": [
"node_modules",
"dist"
]
}

View File

@@ -1,77 +0,0 @@
import { defineConfig } from 'vite';
import { resolve } from 'path';
import nodeResolve from '@rollup/plugin-node-resolve';
import { builtinModules } from 'module';
import fs from 'fs';
const nodeModules = [...builtinModules, builtinModules.map((m) => `node:${m}`)].flat();
// 构建后拷贝插件
function copyToShellPlugin () {
return {
name: 'copy-to-shell',
closeBundle () {
try {
const sourceDir = resolve(__dirname, 'dist');
const targetDir = resolve(__dirname, '../napcat-shell/dist/plugins/builtin');
const packageJsonSource = resolve(__dirname, 'package.json');
// 确保目标目录存在
if (!fs.existsSync(targetDir)) {
fs.mkdirSync(targetDir, { recursive: true });
console.log(`[copy-to-shell] Created directory: ${targetDir}`);
}
// 拷贝 dist 目录下的所有文件
const files = fs.readdirSync(sourceDir);
let copiedCount = 0;
files.forEach(file => {
const sourcePath = resolve(sourceDir, file);
const targetPath = resolve(targetDir, file);
if (fs.statSync(sourcePath).isFile()) {
fs.copyFileSync(sourcePath, targetPath);
copiedCount++;
}
});
// 拷贝 package.json
if (fs.existsSync(packageJsonSource)) {
const packageJsonTarget = resolve(targetDir, 'package.json');
fs.copyFileSync(packageJsonSource, packageJsonTarget);
copiedCount++;
}
console.log(`[copy-to-shell] Successfully copied ${copiedCount} file(s) to ${targetDir}`);
} catch (error) {
console.error('[copy-to-shell] Failed to copy files:', error);
throw error;
}
},
};
}
export default defineConfig({
resolve: {
conditions: ['node', 'default'],
alias: {
'@/napcat-core': resolve(__dirname, '../napcat-core'),
'@': resolve(__dirname, '../'),
},
},
build: {
sourcemap: false,
target: 'esnext',
minify: false,
lib: {
entry: 'index.ts',
formats: ['es'],
fileName: () => 'index.mjs',
},
rollupOptions: {
external: [...nodeModules],
},
},
plugins: [nodeResolve(), copyToShellPlugin()],
});

View File

@@ -3,5 +3,5 @@ REM 快速登录示例脚本
REM -q 参数是可选的,不传则使用二维码登录
REM
REM 使用方法(删掉对应系统那行的 REM
REM ./launcher-user.bat 123456
REM ./launcher-win10-user.bat 123456
REM ./launcher.bat -q 123456
REM ./launcher-win10.bat -q 123456

View File

@@ -26,7 +26,6 @@ const logger = new LogWrapper(pathWrapper.logsPath);
let processManager: IProcessManager | null = null;
let currentWorker: IWorkerProcess | null = null;
let isElectron = false;
let isRestarting = false;
/**
* 获取进程类型名称(用于日志)
@@ -73,12 +72,10 @@ function forceKillProcess (pid: number): void {
*/
export async function restartWorker (): Promise<void> {
logger.log('[NapCat] [Process] 正在重启Worker进程...');
isRestarting = true;
if (!currentWorker) {
logger.logWarn('[NapCat] [Process] 没有运行中的Worker进程');
await startWorker(false);
isRestarting = false;
await startWorker();
return;
}
@@ -134,17 +131,15 @@ export async function restartWorker (): Promise<void> {
logger.log('[NapCat] [Process] Worker进程已关闭等待 3 秒后启动新进程...');
await new Promise(resolve => setTimeout(resolve, 3000));
// 5. 启动新进程(重启模式不传递快速登录参数)
await startWorker(false);
isRestarting = false;
// 5. 启动新进程
await startWorker();
logger.log('[NapCat] [Process] Worker进程重启完成');
}
/**
* 启动 Worker 进程
* @param passQuickLogin 是否传递快速登录参数,默认为 true重启时为 false
*/
async function startWorker (passQuickLogin: boolean = true): Promise<void> {
async function startWorker (): Promise<void> {
if (!processManager) {
throw new Error('进程管理器未初始化');
}
@@ -152,21 +147,7 @@ async function startWorker (passQuickLogin: boolean = true): Promise<void> {
const workerScript = getWorkerScriptPath();
const processType = getProcessTypeName();
// 只在首次启动时传递 -q 或 --qq 参数给 worker 进程
const workerArgs: string[] = [];
if (passQuickLogin) {
const args = process.argv.slice(2);
const qIndex = args.findIndex(arg => arg === '-q' || arg === '--qq');
if (qIndex !== -1 && qIndex + 1 < args.length) {
const qFlag = args[qIndex];
const qValue = args[qIndex + 1];
if (qFlag && qValue) {
workerArgs.push(qFlag, qValue);
}
}
}
const child = processManager.createWorker(workerScript, workerArgs, {
const child = processManager.createWorker(workerScript, [], {
env: {
...process.env,
NAPCAT_WORKER_PROCESS: '1',
@@ -211,13 +192,6 @@ async function startWorker (passQuickLogin: boolean = true): Promise<void> {
} else {
logger.log(`[NapCat] [${processType}] Worker进程正常退出`);
}
// 如果不是由于主动重启引起的退出,尝试自动重新拉起(保留快速登录参数)
if (!isRestarting) {
logger.logWarn(`[NapCat] [${processType}] Worker进程意外退出正在尝试重新拉起...`);
startWorker(true).catch(e => {
logger.logError(`[NapCat] [${processType}] 重新拉起Worker进程失败:`, e);
});
}
});
child.on('spawn', () => {

View File

@@ -1,225 +0,0 @@
import { RequestHandler } from 'express';
import { WebUiDataRuntime } from '@/napcat-webui-backend/src/helper/Data';
import { sendError, sendSuccess } from '@/napcat-webui-backend/src/utils/response';
import { NapCatOneBot11Adapter } from '@/napcat-onebot/index';
import { OB11PluginMangerAdapter } from '@/napcat-onebot/network/plugin-manger';
import path from 'path';
import fs from 'fs';
// Helper to get the plugin manager adapter
const getPluginManager = (): OB11PluginMangerAdapter | null => {
const ob11 = WebUiDataRuntime.getOneBotContext() as NapCatOneBot11Adapter;
if (!ob11) return null;
return ob11.networkManager.findSomeAdapter('plugin_manager') as OB11PluginMangerAdapter;
};
export const GetPluginListHandler: RequestHandler = async (_req, res) => {
const pluginManager = getPluginManager();
if (!pluginManager) {
return sendError(res, 'Plugin Manager not found');
}
// 辅助函数:根据文件名/路径生成唯一ID作为配置键
const getPluginId = (fsName: string, isFile: boolean): string => {
if (isFile) {
return path.parse(fsName).name;
}
return fsName;
};
const loadedPlugins = pluginManager.getLoadedPlugins();
const loadedPluginMap = new Map<string, any>(); // Map ID -> Loaded Info
// 1. 整理已加载的插件
for (const p of loadedPlugins) {
// 计算 ID需要回溯到加载时的入口信息
// 对于已加载的插件,我们通过判断 pluginPath 是否等于根 pluginPath 来判断它是单文件还是目录
const isFilePlugin = p.pluginPath === pluginManager.getPluginPath();
const fsName = isFilePlugin ? path.basename(p.entryPath) : path.basename(p.pluginPath);
const id = getPluginId(fsName, isFilePlugin);
loadedPluginMap.set(id, {
name: p.packageJson?.name || p.name, // 优先使用 package.json 的 name
id: id,
version: p.version || '0.0.0',
description: p.packageJson?.description || '',
author: p.packageJson?.author || '',
status: 'active',
filename: fsName, // 真实文件/目录名
loadedName: p.name // 运行时注册的名称,用于重载/卸载
});
}
const pluginPath = pluginManager.getPluginPath();
const pluginConfig = pluginManager.getPluginConfig();
const allPlugins: any[] = [];
// 2. 扫描文件系统,合并状态
if (fs.existsSync(pluginPath)) {
const items = fs.readdirSync(pluginPath, { withFileTypes: true });
for (const item of items) {
let id = '';
if (item.isFile()) {
if (!['.js', '.mjs'].includes(path.extname(item.name))) continue;
id = getPluginId(item.name, true);
} else if (item.isDirectory()) {
id = getPluginId(item.name, false);
} else {
continue;
}
const isActiveConfig = pluginConfig[id] !== false; // 默认为 true
if (loadedPluginMap.has(id)) {
// 已加载,使用加载的信息
const loadedInfo = loadedPluginMap.get(id);
allPlugins.push(loadedInfo);
} else {
// 未加载 (可能是被禁用,或者加载失败,或者新增未运行)
let version = '0.0.0';
let description = '';
let author = '';
// 默认显示名称为 ID (文件名/目录名)
let name = id;
try {
// 尝试读取 package.json 获取信息
if (item.isDirectory()) {
const packageJsonPath = path.join(pluginPath, item.name, 'package.json');
if (fs.existsSync(packageJsonPath)) {
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
version = pkg.version || version;
description = pkg.description || description;
author = pkg.author || author;
// 如果 package.json 有 name优先使用
name = pkg.name || name;
}
}
} catch (e) { }
allPlugins.push({
name: name,
id: id,
version,
description,
author,
// 如果配置是 false则为 disabled否则是 stopped (应启动但未启动)
status: isActiveConfig ? 'stopped' : 'disabled',
filename: item.name
});
}
}
}
return sendSuccess(res, allPlugins);
};
export const ReloadPluginHandler: RequestHandler = async (req, res) => {
const { name } = req.body;
// Note: we should probably accept ID or Name. But ReloadPlugin uses valid loaded name.
// Let's stick to name for now, but be aware of ambiguity.
if (!name) return sendError(res, 'Plugin Name is required');
const pluginManager = getPluginManager();
if (!pluginManager) {
return sendError(res, 'Plugin Manager not found');
}
const success = await pluginManager.reloadPlugin(name);
if (success) {
return sendSuccess(res, { message: 'Reloaded successfully' });
} else {
return sendError(res, 'Failed to reload plugin');
}
};
export const SetPluginStatusHandler: RequestHandler = async (req, res) => {
const { enable, filename } = req.body;
// We Use filename / id to control config
// Front-end should pass the 'filename' or 'id' as the key identifier
if (!filename) return sendError(res, 'Plugin Filename/ID is required');
const pluginManager = getPluginManager();
if (!pluginManager) {
return sendError(res, 'Plugin Manager not found');
}
// Calculate ID from filename (remove ext if file)
// Or just use the logic consistent with loadPlugins
let id = filename;
// If it has extension .js/.mjs, remove it to get the ID used in config
if (filename.endsWith('.js') || filename.endsWith('.mjs')) {
id = path.parse(filename).name;
}
try {
pluginManager.setPluginStatus(id, enable);
// If enabling, trigger load
if (enable) {
const pluginPath = pluginManager.getPluginPath();
const fullPath = path.join(pluginPath, filename);
if (fs.statSync(fullPath).isDirectory()) {
await pluginManager.loadDirectoryPlugin(filename);
} else {
await pluginManager.loadFilePlugin(filename);
}
} else {
// Disabling is handled inside setPluginStatus usually if implemented,
// OR we can explicitly unload here using the loaded name.
// The Manager's setPluginStatus implementation (if added) might logic this out.
// But our current Manager implementation just saves config.
// Wait, I updated Manager to try to unload.
// Let's rely on Manager's setPluginStatus or do it here?
// I implemented a basic unload loop in Manager.setPluginStatus.
}
return sendSuccess(res, { message: 'Status updated successfully' });
} catch (e: any) {
return sendError(res, 'Failed to update status: ' + e.message);
}
};
export const UninstallPluginHandler: RequestHandler = async (req, res) => {
const { name, filename } = req.body;
// If it's loaded, we use name. If it's disabled, we might use filename.
const pluginManager = getPluginManager();
if (!pluginManager) {
return sendError(res, 'Plugin Manager not found');
}
// Check if loaded
const plugin = pluginManager.getPluginInfo(name);
let fsPath = '';
if (plugin) {
// Active plugin
await pluginManager.unregisterPlugin(name);
if (plugin.pluginPath === pluginManager.getPluginPath()) {
fsPath = plugin.entryPath;
} else {
fsPath = plugin.pluginPath;
}
} else {
// Disabled or not loaded
if (filename) {
fsPath = path.join(pluginManager.getPluginPath(), filename);
} else {
return sendError(res, 'Plugin not found, provide filename if disabled');
}
}
try {
if (fs.existsSync(fsPath)) {
fs.rmSync(fsPath, { recursive: true, force: true });
}
return sendSuccess(res, { message: 'Uninstalled successfully' });
} catch (e: any) {
return sendError(res, 'Failed to uninstall: ' + e.message);
}
};

View File

@@ -1,11 +0,0 @@
import { Router } from 'express';
import { GetPluginListHandler, ReloadPluginHandler, SetPluginStatusHandler, UninstallPluginHandler } from '@/napcat-webui-backend/src/api/Plugin';
const router = Router();
router.get('/List', GetPluginListHandler);
router.post('/Reload', ReloadPluginHandler);
router.post('/SetStatus', SetPluginStatusHandler);
router.post('/Uninstall', UninstallPluginHandler);
export { router as PluginRouter };

View File

@@ -17,7 +17,6 @@ import { WebUIConfigRouter } from './WebUIConfig';
import { UpdateNapCatRouter } from './UpdateNapCat';
import DebugRouter from '@/napcat-webui-backend/src/api/Debug';
import { ProcessRouter } from './Process';
import { PluginRouter } from './Plugin';
const router = Router();
@@ -48,7 +47,5 @@ router.use('/UpdateNapCat', UpdateNapCatRouter);
router.use('/Debug', DebugRouter);
// router:进程管理相关路由
router.use('/Process', ProcessRouter);
// router:插件管理相关路由
router.use('/Plugin', PluginRouter);
export { router as ALLRouter };

View File

@@ -25,7 +25,6 @@ const FileManagerPage = lazy(() => import('@/pages/dashboard/file_manager'));
const LogsPage = lazy(() => import('@/pages/dashboard/logs'));
const NetworkPage = lazy(() => import('@/pages/dashboard/network'));
const TerminalPage = lazy(() => import('@/pages/dashboard/terminal'));
const PluginPage = lazy(() => import('@/pages/dashboard/plugin'));
function App () {
return (
@@ -43,7 +42,7 @@ function App () {
);
}
function AuthChecker ({ children }: { children: React.ReactNode; }) {
function AuthChecker ({ children }: { children: React.ReactNode }) {
const { isAuth } = useAuth();
const navigate = useNavigate();
@@ -77,7 +76,6 @@ function AppRoutes () {
</Route>
<Route path='file_manager' element={<FileManagerPage />} />
<Route path='terminal' element={<TerminalPage />} />
<Route path='plugins' element={<PluginPage />} />
<Route path='about' element={<AboutPage />} />
</Route>
<Route path='/qq_login' element={<QQLoginPage />} />

View File

@@ -93,7 +93,7 @@ const NetworkDisplayCard = <T extends keyof NetworkType> ({
onPress={handleEnableDebug}
isDisabled={editing}
>
{debug ? '默认' : '调试'}
{debug ? '关闭调试' : '开启调试'}
</Button>
<Button
fullWidth

View File

@@ -1,127 +0,0 @@
import { Button } from '@heroui/button';
import { Switch } from '@heroui/switch';
import { Chip } from '@heroui/chip';
import { useState } from 'react';
import { MdDeleteForever, MdPublishedWithChanges } from 'react-icons/md';
import DisplayCardContainer from './container';
import { PluginItem } from '@/controllers/plugin_manager';
export interface PluginDisplayCardProps {
data: PluginItem;
onReload: () => Promise<void>;
onToggleStatus: () => Promise<void>;
onUninstall: () => Promise<void>;
}
const PluginDisplayCard: React.FC<PluginDisplayCardProps> = ({
data,
onReload,
onToggleStatus,
onUninstall,
}) => {
const { name, version, author, description, status } = data;
const isEnabled = status !== 'disabled';
const [processing, setProcessing] = useState(false);
const handleToggle = () => {
setProcessing(true);
onToggleStatus().finally(() => setProcessing(false));
};
const handleReload = () => {
setProcessing(true);
onReload().finally(() => setProcessing(false));
};
const handleUninstall = () => {
setProcessing(true);
onUninstall().finally(() => setProcessing(false));
};
return (
<DisplayCardContainer
className='w-full max-w-[420px]'
action={
<div className='flex gap-2 w-full'>
<Button
fullWidth
radius='full'
size='sm'
variant='flat'
className='flex-1 bg-default-100 dark:bg-default-50 text-default-600 font-medium hover:bg-primary/20 hover:text-primary transition-colors'
startContent={<MdPublishedWithChanges size={16} />}
onPress={handleReload}
isDisabled={!isEnabled || processing}
>
</Button>
<Button
fullWidth
radius='full'
size='sm'
variant='flat'
className='flex-1 bg-default-100 dark:bg-default-50 text-default-600 font-medium hover:bg-danger/20 hover:text-danger transition-colors'
startContent={<MdDeleteForever size={16} />}
onPress={handleUninstall}
isDisabled={processing}
>
</Button>
</div>
}
enableSwitch={
<Switch
isDisabled={processing}
isSelected={isEnabled}
onChange={handleToggle}
classNames={{
wrapper: 'group-data-[selected=true]:bg-primary-400',
}}
/>
}
title={name}
tag={
<Chip
className="ml-auto"
color={status === 'active' ? 'success' : status === 'stopped' ? 'warning' : 'default'}
size="sm"
variant="flat"
>
{status === 'active' ? '运行中' : status === 'stopped' ? '已停止' : '已禁用'}
</Chip>
}
>
<div className='grid grid-cols-2 gap-3'>
<div className='flex flex-col gap-1 p-3 bg-default-100/50 dark:bg-white/10 rounded-xl border border-transparent hover:border-default-200 transition-colors'>
<span className='text-xs text-default-500 dark:text-white/50 font-medium tracking-wide'>
</span>
<div className='text-sm font-medium text-default-700 dark:text-white/90 truncate'>
{version}
</div>
</div>
<div className='flex flex-col gap-1 p-3 bg-default-100/50 dark:bg-white/10 rounded-xl border border-transparent hover:border-default-200 transition-colors'>
<span className='text-xs text-default-500 dark:text-white/50 font-medium tracking-wide'>
</span>
<div className='text-sm font-medium text-default-700 dark:text-white/90 truncate'>
{author || '未知'}
</div>
</div>
<div className='col-span-2 flex flex-col gap-1 p-3 bg-default-100/50 dark:bg-white/10 rounded-xl border border-transparent hover:border-default-200 transition-colors'>
<span className='text-xs text-default-500 dark:text-white/50 font-medium tracking-wide'>
</span>
<div className='text-sm font-medium text-default-700 dark:text-white/90 break-words line-clamp-2'>
{description || '暂无描述'}
</div>
</div>
</div>
</DisplayCardContainer>
);
};
export default PluginDisplayCard;

View File

@@ -8,7 +8,6 @@ import {
LuSignal,
LuTerminal,
LuZap,
LuPackage,
} from 'react-icons/lu';
export type SiteConfig = typeof siteConfig;
@@ -60,11 +59,6 @@ export const siteConfig = {
icon: <LuFolderOpen className='w-5 h-5' />,
href: '/file_manager',
},
{
label: '插件管理',
icon: <LuPackage className='w-5 h-5' />,
href: '/plugins',
},
{
label: '系统终端',
icon: <LuTerminal className='w-5 h-5' />,

View File

@@ -61,8 +61,7 @@ const messageNode = z.union([
.object({
type: z.literal('reply'),
data: z.object({
id: z.number().optional(),
seq: z.number().optional(),
id: z.number(),
}),
})
.describe('回复消息'),

View File

@@ -1,35 +0,0 @@
import { serverRequest } from '@/utils/request';
export interface PluginItem {
name: string;
version: string;
description: string;
author: string;
status: 'active' | 'disabled' | 'stopped';
filename?: string;
}
export interface ServerResponse<T> {
code: number;
message: string;
data: T;
}
export default class PluginManager {
public static async getPluginList () {
const { data } = await serverRequest.get<ServerResponse<PluginItem[]>>('/Plugin/List');
return data.data;
}
public static async reloadPlugin (name: string) {
await serverRequest.post<ServerResponse<void>>('/Plugin/Reload', { name });
}
public static async setPluginStatus (name: string, enable: boolean, filename?: string) {
await serverRequest.post<ServerResponse<void>>('/Plugin/SetStatus', { name, enable, filename });
}
public static async uninstallPlugin (name: string, filename?: string) {
await serverRequest.post<ServerResponse<void>>('/Plugin/Uninstall', { name, filename });
}
}

View File

@@ -1,115 +0,0 @@
import { Button } from '@heroui/button';
import { useEffect, useState } from 'react';
import toast from 'react-hot-toast';
import { IoMdRefresh } from 'react-icons/io';
import PageLoading from '@/components/page_loading';
import PluginDisplayCard from '@/components/display_card/plugin_card';
import PluginManager, { PluginItem } from '@/controllers/plugin_manager';
import useDialog from '@/hooks/use-dialog';
export default function PluginPage () {
const [plugins, setPlugins] = useState<PluginItem[]>([]);
const [loading, setLoading] = useState(false);
const dialog = useDialog();
const loadPlugins = async () => {
setLoading(true);
try {
const data = await PluginManager.getPluginList();
setPlugins(data);
} catch (e: any) {
toast.error(e.message);
} finally {
setLoading(false);
}
};
useEffect(() => {
loadPlugins();
}, []);
const handleReload = async (name: string) => {
const loadingToast = toast.loading('重载中...');
try {
await PluginManager.reloadPlugin(name);
toast.success('重载成功', { id: loadingToast });
loadPlugins();
} catch (e: any) {
toast.error(e.message, { id: loadingToast });
}
};
const handleToggle = async (plugin: PluginItem) => {
const isEnable = plugin.status !== 'active';
const actionText = isEnable ? '启用' : '禁用';
const loadingToast = toast.loading(`${actionText}中...`);
try {
await PluginManager.setPluginStatus(plugin.name, isEnable, plugin.filename);
toast.success(`${actionText}成功`, { id: loadingToast });
loadPlugins();
} catch (e: any) {
toast.error(e.message, { id: loadingToast });
}
};
const handleUninstall = async (plugin: PluginItem) => {
return new Promise<void>((resolve, reject) => {
dialog.confirm({
title: '卸载插件',
content: `确定要卸载插件「${plugin.name}」吗? 此操作不可恢复。`,
onConfirm: async () => {
const loadingToast = toast.loading('卸载中...');
try {
await PluginManager.uninstallPlugin(plugin.name, plugin.filename);
toast.success('卸载成功', { id: loadingToast });
loadPlugins();
resolve();
} catch (e: any) {
toast.error(e.message, { id: loadingToast });
reject(e);
}
},
onCancel: () => {
resolve();
}
});
});
};
return (
<>
<title> - NapCat WebUI</title>
<div className='p-2 md:p-4 relative'>
<PageLoading loading={loading} />
<div className='flex mb-6 items-center gap-4'>
<h1 className="text-2xl font-bold"></h1>
<Button
isIconOnly
className="bg-default-100/50 hover:bg-default-200/50 text-default-700 backdrop-blur-md"
radius='full'
onPress={loadPlugins}
>
<IoMdRefresh size={24} />
</Button>
</div>
{plugins.length === 0 ? (
<div className="text-default-400"></div>
) : (
<div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 justify-start items-stretch gap-x-2 gap-y-4'>
{plugins.map(plugin => (
<PluginDisplayCard
key={plugin.name}
data={plugin}
onReload={() => handleReload(plugin.name)}
onToggleStatus={() => handleToggle(plugin)}
onUninstall={() => handleUninstall(plugin)}
/>
))}
</div>
)}
</div>
</>
);
}

View File

@@ -24,197 +24,196 @@ export type OB11SegmentType =
| 'file';
export interface Segment {
type: OB11SegmentType;
type: OB11SegmentType
}
/** 纯文本 */
export interface TextSegment extends Segment {
type: 'text';
type: 'text'
data: {
text: string;
};
text: string
}
}
/** QQ表情 */
export interface FaceSegment extends Segment {
type: 'face';
type: 'face'
data: {
id: string;
};
id: string
}
}
/** 图片消息段 */
export interface ImageSegment extends Segment {
type: 'image';
type: 'image'
data: {
file: string;
type?: 'flash';
url?: string;
cache?: 0 | 1;
proxy?: 0 | 1;
timeout?: number;
};
file: string
type?: 'flash'
url?: string
cache?: 0 | 1
proxy?: 0 | 1
timeout?: number
}
}
/** 语音消息段 */
export interface RecordSegment extends Segment {
type: 'record';
type: 'record'
data: {
file: string;
magic?: 0 | 1;
url?: string;
cache?: 0 | 1;
proxy?: 0 | 1;
timeout?: number;
};
file: string
magic?: 0 | 1
url?: string
cache?: 0 | 1
proxy?: 0 | 1
timeout?: number
}
}
/** 短视频消息段 */
export interface VideoSegment extends Segment {
type: 'video';
type: 'video'
data: {
file: string;
url?: string;
cache?: 0 | 1;
proxy?: 0 | 1;
timeout?: number;
};
file: string
url?: string
cache?: 0 | 1
proxy?: 0 | 1
timeout?: number
}
}
/** @某人消息段 */
export interface AtSegment extends Segment {
type: 'at';
type: 'at'
data: {
qq: string | 'all';
name?: string;
};
qq: string | 'all'
name?: string
}
}
/** 猜拳魔法表情消息段 */
export interface RpsSegment extends Segment {
type: 'rps';
type: 'rps'
}
/** 掷骰子魔法表情消息段 */
export interface DiceSegment extends Segment {
type: 'dice';
type: 'dice'
}
/** 窗口抖动(戳一戳)消息段 */
export interface ShakeSegment extends Segment {
type: 'shake';
data: object;
type: 'shake'
data: object
}
/** 戳一戳消息段 */
export interface PokeSegment extends Segment {
type: 'poke';
type: 'poke'
data: {
type: string;
id: string;
name?: string;
};
type: string
id: string
name?: string
}
}
/** 匿名发消息消息段 */
export interface AnonymousSegment extends Segment {
type: 'anonymous';
type: 'anonymous'
data: {
ignore?: 0 | 1;
};
ignore?: 0 | 1
}
}
/** 链接分享消息段 */
export interface ShareSegment extends Segment {
type: 'share';
type: 'share'
data: {
url: string;
title: string;
content?: string;
image?: string;
};
url: string
title: string
content?: string
image?: string
}
}
/** 推荐好友/群消息段 */
export interface ContactSegment extends Segment {
type: 'contact';
type: 'contact'
data: {
type: 'qq' | 'group';
id: string;
};
type: 'qq' | 'group'
id: string
}
}
/** 位置消息段 */
export interface LocationSegment extends Segment {
type: 'location';
type: 'location'
data: {
lat: string;
lon: string;
title?: string;
content?: string;
};
lat: string
lon: string
title?: string
content?: string
}
}
/** 音乐分享消息段 */
export interface MusicSegment extends Segment {
type: 'music';
type: 'music'
data: {
type: 'qq' | '163' | 'xm';
id: string;
};
type: 'qq' | '163' | 'xm'
id: string
}
}
/** 音乐自定义分享消息段 */
export interface CustomMusicSegment extends Segment {
type: 'music';
type: 'music'
data: {
type: 'custom';
url: string;
audio: string;
title: string;
content?: string;
image?: string;
};
type: 'custom'
url: string
audio: string
title: string
content?: string
image?: string
}
}
/** 回复消息段 */
export interface ReplySegment extends Segment {
type: 'reply';
type: 'reply'
data: {
id?: string; // msg_id 的短ID映射
seq?: number; // msg_seq优先使用
};
id: string
}
}
export interface FileSegment extends Segment {
type: 'file';
type: 'file'
data: {
file: string;
};
file: string
}
}
/** 合并转发消息段 */
export interface ForwardSegment extends Segment {
type: 'forward';
type: 'forward'
data: {
id: string;
};
id: string
}
}
/** XML消息段 */
export interface XmlSegment extends Segment {
type: 'xml';
type: 'xml'
data: {
data: string;
};
data: string
}
}
/** JSON消息段 */
export interface JsonSegment extends Segment {
type: 'json';
type: 'json'
data: {
data: string;
};
data: string
}
}
/** OneBot11消息段 */
@@ -243,23 +242,23 @@ export type OB11SegmentBase =
/** 合并转发已有消息节点消息段 */
export interface DirectNodeSegment extends Segment {
type: 'node';
type: 'node'
data: {
id: string;
};
id: string
}
}
/** 合并转发自定义节点消息段 */
export interface CustomNodeSegments extends Segment {
type: 'node';
type: 'node'
data: {
user_id: string;
nickname: string;
content: OB11Segment[];
prompt?: string;
summary?: string;
source?: string;
};
user_id: string
nickname: string
content: OB11Segment[]
prompt?: string
summary?: string
source?: string
}
}
/** 合并转发消息段 */

10
pnpm-lock.yaml generated
View File

@@ -220,16 +220,6 @@ importers:
specifier: ^22.0.1
version: 22.19.1
packages/napcat-plugin-builtin:
dependencies:
napcat-onebot:
specifier: workspace:*
version: link:../napcat-onebot
devDependencies:
'@types/node':
specifier: ^22.0.1
version: 22.19.1
packages/napcat-protobuf:
dependencies:
'@protobuf-ts/runtime':