mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-02-04 06:31:13 +00:00
Add config UI and persistence to builtin plugin
Introduces a configuration UI schema and persistent config storage for the napcat-plugin-builtin. The plugin now loads and saves its configuration, supports dynamic prefix and reply toggling, and updates dependencies to napcat-types v0.0.6.
This commit is contained in:
parent
d711cdecaf
commit
d680328762
@ -107,6 +107,7 @@ export class OB11PluginMangerAdapter extends IOB11NetworkAdapter<PluginConfig> {
|
|||||||
private readonly configPath: string;
|
private readonly configPath: string;
|
||||||
private loadedPlugins: Map<string, LoadedPlugin> = new Map();
|
private loadedPlugins: Map<string, LoadedPlugin> = new Map();
|
||||||
declare config: PluginConfig;
|
declare config: PluginConfig;
|
||||||
|
public NapCatConfig = NapCatConfig;
|
||||||
|
|
||||||
override get isActive (): boolean {
|
override get isActive (): boolean {
|
||||||
return this.isEnable && this.loadedPlugins.size > 0;
|
return this.isEnable && this.loadedPlugins.size > 0;
|
||||||
|
|||||||
@ -3,8 +3,31 @@ import { EventType } from 'napcat-types/napcat-onebot/event/index';
|
|||||||
import type { PluginModule } from 'napcat-types/napcat-onebot/network/plugin-manger';
|
import type { PluginModule } from 'napcat-types/napcat-onebot/network/plugin-manger';
|
||||||
import type { OB11Message, OB11PostSendMsg } from 'napcat-types/napcat-onebot/types/index';
|
import type { OB11Message, OB11PostSendMsg } from 'napcat-types/napcat-onebot/types/index';
|
||||||
|
|
||||||
|
import fs from 'fs';
|
||||||
|
import type { PluginConfigSchema, OB11PluginMangerAdapter } from 'napcat-types/napcat-onebot/network/plugin-manger';
|
||||||
|
|
||||||
let actions: ActionMap | undefined = undefined;
|
let actions: ActionMap | undefined = undefined;
|
||||||
let startTime: number = Date.now();
|
let startTime: number = Date.now();
|
||||||
|
let platformInstance: OB11PluginMangerAdapter | undefined = undefined;
|
||||||
|
|
||||||
|
interface BuiltinPluginConfig {
|
||||||
|
prefix: string;
|
||||||
|
enableReply: boolean;
|
||||||
|
description: string;
|
||||||
|
theme?: string;
|
||||||
|
features?: string[];
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentConfig: BuiltinPluginConfig = {
|
||||||
|
prefix: '#napcat',
|
||||||
|
enableReply: true,
|
||||||
|
description: '这是一个内置插件的配置示例'
|
||||||
|
};
|
||||||
|
|
||||||
|
const PLUGIN_NAME = 'napcat-plugin-builtin';
|
||||||
|
|
||||||
|
export let plugin_config_ui: PluginConfigSchema = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 插件初始化
|
* 插件初始化
|
||||||
@ -12,6 +35,61 @@ let startTime: number = Date.now();
|
|||||||
const plugin_init: PluginModule['plugin_init'] = async (_core, _obContext, _actions, _instance) => {
|
const plugin_init: PluginModule['plugin_init'] = async (_core, _obContext, _actions, _instance) => {
|
||||||
console.log('[Plugin: builtin] NapCat 内置插件已初始化');
|
console.log('[Plugin: builtin] NapCat 内置插件已初始化');
|
||||||
actions = _actions;
|
actions = _actions;
|
||||||
|
platformInstance = _instance;
|
||||||
|
|
||||||
|
if (_instance.NapCatConfig) {
|
||||||
|
const NapCatConfig = _instance.NapCatConfig;
|
||||||
|
plugin_config_ui = NapCatConfig.combine(
|
||||||
|
NapCatConfig.html('<div style="padding: 10px; background: rgba(0,0,0,0.05); border-radius: 8px;"><h3>👋 Welcome to NapCat Builtin Plugin</h3><p>This is a demonstration of the plugin configuration interface.</p></div>'),
|
||||||
|
NapCatConfig.text('prefix', 'Command Prefix', '#napcat', 'The prefix to trigger the version info command'),
|
||||||
|
NapCatConfig.boolean('enableReply', 'Enable Reply', true, 'Switch to enable or disable the reply functionality'),
|
||||||
|
NapCatConfig.select('theme', 'Theme Selection', [
|
||||||
|
{ label: 'Light Mode', value: 'light' },
|
||||||
|
{ label: 'Dark Mode', value: 'dark' },
|
||||||
|
{ label: 'Auto', value: 'auto' }
|
||||||
|
], 'light', 'Select a theme for the response (Demo purpose only)'),
|
||||||
|
NapCatConfig.multiSelect('features', 'Enabled Features', [
|
||||||
|
{ label: 'Version Info', value: 'version' },
|
||||||
|
{ label: 'Status Report', value: 'status' },
|
||||||
|
{ label: 'Debug Log', value: 'debug' }
|
||||||
|
], ['version'], 'Select features to enable'),
|
||||||
|
NapCatConfig.text('description', 'Description', '这是一个内置插件的配置示例', 'A multi-line text area for notes')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to load config
|
||||||
|
try {
|
||||||
|
if (platformInstance && platformInstance.getPluginConfigPath) {
|
||||||
|
const configPath = platformInstance.getPluginConfigPath(PLUGIN_NAME);
|
||||||
|
if (fs.existsSync(configPath)) {
|
||||||
|
const savedConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
||||||
|
Object.assign(currentConfig, savedConfig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[Plugin: builtin] Failed to load config', e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const plugin_get_config = async () => {
|
||||||
|
return currentConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const plugin_set_config = async (config: BuiltinPluginConfig) => {
|
||||||
|
currentConfig = config;
|
||||||
|
if (platformInstance && platformInstance.getPluginConfigPath) {
|
||||||
|
try {
|
||||||
|
const configPath = platformInstance.getPluginConfigPath(PLUGIN_NAME);
|
||||||
|
const configDir = configPath.substring(0, configPath.lastIndexOf(process.platform === 'win32' ? '\\' : '/'));
|
||||||
|
if (!fs.existsSync(configDir)) {
|
||||||
|
fs.mkdirSync(configDir, { recursive: true });
|
||||||
|
}
|
||||||
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[Plugin: builtin] Failed to save config', e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -19,7 +97,13 @@ const plugin_init: PluginModule['plugin_init'] = async (_core, _obContext, _acti
|
|||||||
* 当收到包含 #napcat 的消息时,回复版本信息
|
* 当收到包含 #napcat 的消息时,回复版本信息
|
||||||
*/
|
*/
|
||||||
const plugin_onmessage: PluginModule['plugin_onmessage'] = async (adapter, _core, _obCtx, event, _actions, instance) => {
|
const plugin_onmessage: PluginModule['plugin_onmessage'] = async (adapter, _core, _obCtx, event, _actions, instance) => {
|
||||||
if (event.post_type !== EventType.MESSAGE || !event.raw_message.startsWith('#napcat')) {
|
// Use config logic
|
||||||
|
const prefix = currentConfig.prefix || '#napcat';
|
||||||
|
if (currentConfig.enableReply === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.post_type !== EventType.MESSAGE || !event.raw_message.startsWith(prefix)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
"description": "NapCat 内置插件",
|
"description": "NapCat 内置插件",
|
||||||
"author": "NapNeko",
|
"author": "NapNeko",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"napcat-types": "0.0.5"
|
"napcat-types": "0.0.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.0.1"
|
"@types/node": "^22.0.1"
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "napcat-types",
|
"name": "napcat-types",
|
||||||
"version": "0.0.5",
|
"version": "0.0.6",
|
||||||
"private": false,
|
"private": false,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"types": "./napcat-types/index.d.ts",
|
"types": "./napcat-types/index.d.ts",
|
||||||
|
|||||||
@ -118,24 +118,7 @@ export const GetPluginListHandler: RequestHandler = async (_req, res) => {
|
|||||||
return sendSuccess(res, { plugins: allPlugins, pluginManagerNotFound: false });
|
return sendSuccess(res, { plugins: allPlugins, pluginManagerNotFound: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ReloadPluginHandler: RequestHandler = async (req, res) => {
|
// ReloadPluginHandler removed
|
||||||
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, '插件管理器未加载,请检查 plugins 目录是否存在');
|
|
||||||
}
|
|
||||||
|
|
||||||
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) => {
|
export const SetPluginStatusHandler: RequestHandler = async (req, res) => {
|
||||||
const { enable, filename } = req.body;
|
const { enable, filename } = req.body;
|
||||||
@ -291,7 +274,11 @@ export const SetPluginConfigHandler: RequestHandler = async (req, res) => {
|
|||||||
fs.mkdirSync(configDir, { recursive: true });
|
fs.mkdirSync(configDir, { recursive: true });
|
||||||
}
|
}
|
||||||
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
||||||
return sendSuccess(res, { message: 'Config saved' });
|
|
||||||
|
// Auto-Reload plugin to apply changes
|
||||||
|
await pluginManager.reloadPlugin(name);
|
||||||
|
|
||||||
|
return sendSuccess(res, { message: 'Config saved and plugin reloaded' });
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
return sendError(res, 'Error saving config: ' + e.message);
|
return sendError(res, 'Error saving config: ' + e.message);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import {
|
import {
|
||||||
GetPluginListHandler,
|
GetPluginListHandler,
|
||||||
ReloadPluginHandler,
|
|
||||||
SetPluginStatusHandler,
|
SetPluginStatusHandler,
|
||||||
UninstallPluginHandler,
|
UninstallPluginHandler,
|
||||||
GetPluginConfigHandler,
|
GetPluginConfigHandler,
|
||||||
@ -17,7 +16,6 @@ import {
|
|||||||
const router: Router = Router();
|
const router: Router = Router();
|
||||||
|
|
||||||
router.get('/List', GetPluginListHandler);
|
router.get('/List', GetPluginListHandler);
|
||||||
router.post('/Reload', ReloadPluginHandler);
|
|
||||||
router.post('/SetStatus', SetPluginStatusHandler);
|
router.post('/SetStatus', SetPluginStatusHandler);
|
||||||
router.post('/Uninstall', UninstallPluginHandler);
|
router.post('/Uninstall', UninstallPluginHandler);
|
||||||
router.get('/Config', GetPluginConfigHandler);
|
router.get('/Config', GetPluginConfigHandler);
|
||||||
|
|||||||
@ -3,14 +3,13 @@ import { Switch } from '@heroui/switch';
|
|||||||
import { Chip } from '@heroui/chip';
|
import { Chip } from '@heroui/chip';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { MdDeleteForever, MdPublishedWithChanges, MdSettings } from 'react-icons/md';
|
import { MdDeleteForever, MdSettings } from 'react-icons/md';
|
||||||
|
|
||||||
import DisplayCardContainer from './container';
|
import DisplayCardContainer from './container';
|
||||||
import { PluginItem } from '@/controllers/plugin_manager';
|
import { PluginItem } from '@/controllers/plugin_manager';
|
||||||
|
|
||||||
export interface PluginDisplayCardProps {
|
export interface PluginDisplayCardProps {
|
||||||
data: PluginItem;
|
data: PluginItem;
|
||||||
onReload: () => Promise<void>;
|
|
||||||
onToggleStatus: () => Promise<void>;
|
onToggleStatus: () => Promise<void>;
|
||||||
onUninstall: () => Promise<void>;
|
onUninstall: () => Promise<void>;
|
||||||
onConfig?: () => void;
|
onConfig?: () => void;
|
||||||
@ -19,7 +18,6 @@ export interface PluginDisplayCardProps {
|
|||||||
|
|
||||||
const PluginDisplayCard: React.FC<PluginDisplayCardProps> = ({
|
const PluginDisplayCard: React.FC<PluginDisplayCardProps> = ({
|
||||||
data,
|
data,
|
||||||
onReload,
|
|
||||||
onToggleStatus,
|
onToggleStatus,
|
||||||
onUninstall,
|
onUninstall,
|
||||||
onConfig,
|
onConfig,
|
||||||
@ -34,11 +32,6 @@ const PluginDisplayCard: React.FC<PluginDisplayCardProps> = ({
|
|||||||
onToggleStatus().finally(() => setProcessing(false));
|
onToggleStatus().finally(() => setProcessing(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleReload = () => {
|
|
||||||
setProcessing(true);
|
|
||||||
onReload().finally(() => setProcessing(false));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUninstall = () => {
|
const handleUninstall = () => {
|
||||||
setProcessing(true);
|
setProcessing(true);
|
||||||
onUninstall().finally(() => setProcessing(false));
|
onUninstall().finally(() => setProcessing(false));
|
||||||
@ -50,19 +43,6 @@ const PluginDisplayCard: React.FC<PluginDisplayCardProps> = ({
|
|||||||
action={
|
action={
|
||||||
<div className='flex flex-col gap-2 w-full'>
|
<div className='flex flex-col gap-2 w-full'>
|
||||||
<div className='flex gap-2 w-full'>
|
<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
|
<Button
|
||||||
fullWidth
|
fullWidth
|
||||||
radius='full'
|
radius='full'
|
||||||
|
|||||||
@ -43,9 +43,7 @@ export default class PluginManager {
|
|||||||
return data.data;
|
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) {
|
public static async setPluginStatus (name: string, enable: boolean, filename?: string) {
|
||||||
await serverRequest.post<ServerResponse<void>>('/Plugin/SetStatus', { name, enable, filename });
|
await serverRequest.post<ServerResponse<void>>('/Plugin/SetStatus', { name, enable, filename });
|
||||||
|
|||||||
@ -42,16 +42,7 @@ export default function PluginPage () {
|
|||||||
loadPlugins();
|
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 handleToggle = async (plugin: PluginItem) => {
|
||||||
const isEnable = plugin.status !== 'active';
|
const isEnable = plugin.status !== 'active';
|
||||||
@ -156,7 +147,6 @@ export default function PluginPage () {
|
|||||||
<PluginDisplayCard
|
<PluginDisplayCard
|
||||||
key={plugin.name}
|
key={plugin.name}
|
||||||
data={plugin}
|
data={plugin}
|
||||||
onReload={() => handleReload(plugin.name)}
|
|
||||||
onToggleStatus={() => handleToggle(plugin)}
|
onToggleStatus={() => handleToggle(plugin)}
|
||||||
onUninstall={() => handleUninstall(plugin)}
|
onUninstall={() => handleUninstall(plugin)}
|
||||||
onConfig={() => {
|
onConfig={() => {
|
||||||
|
|||||||
@ -223,8 +223,8 @@ importers:
|
|||||||
packages/napcat-plugin-builtin:
|
packages/napcat-plugin-builtin:
|
||||||
dependencies:
|
dependencies:
|
||||||
napcat-types:
|
napcat-types:
|
||||||
specifier: 0.0.5
|
specifier: 0.0.6
|
||||||
version: 0.0.5
|
version: 0.0.6
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^22.0.1
|
specifier: ^22.0.1
|
||||||
@ -5448,8 +5448,8 @@ packages:
|
|||||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
napcat-types@0.0.5:
|
napcat-types@0.0.6:
|
||||||
resolution: {integrity: sha512-ihbIdCAsqx4wdiSaKfGgsbC5nn3rjk/xbPO1lqr+3GzU+w/oeWsP0XzdF44Z3Uz6ODOijsoR1VR15HXuP8ukcQ==}
|
resolution: {integrity: sha512-KyIEr/uFC8w1bGF2Oyvk+2Kdr6ENklWK9bHwrGGbAKnUUJ4GRhsUYQdX/dDhhiZrLFWisYslQyLFD6530YtTlg==}
|
||||||
|
|
||||||
napcat.protobuf@1.1.4:
|
napcat.protobuf@1.1.4:
|
||||||
resolution: {integrity: sha512-z7XtLSBJ/PxmYb0VD/w+eYr/X3LyGz+SZ2QejFTOczwt6zWNxy2yV1mTMTvJoc3BWkI3ESVFRxkuT6+pj1tb1Q==}
|
resolution: {integrity: sha512-z7XtLSBJ/PxmYb0VD/w+eYr/X3LyGz+SZ2QejFTOczwt6zWNxy2yV1mTMTvJoc3BWkI3ESVFRxkuT6+pj1tb1Q==}
|
||||||
@ -12819,7 +12819,7 @@ snapshots:
|
|||||||
|
|
||||||
nanoid@3.3.11: {}
|
nanoid@3.3.11: {}
|
||||||
|
|
||||||
napcat-types@0.0.5:
|
napcat-types@0.0.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@sinclair/typebox': 0.34.41
|
'@sinclair/typebox': 0.34.41
|
||||||
'@types/cors': 2.8.19
|
'@types/cors': 2.8.19
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user