Overview
NapCat’s plugin system allows you to extend the bot’s functionality through a modular architecture. Plugins can handle messages, events, register HTTP APIs, and interact with the core system.Plugin Structure
A NapCat plugin consists of:my-plugin/
├── package.json # Plugin metadata
├── index.ts # Main entry point
├── webui/ # WebUI assets (optional)
│ ├── dashboard.html
│ └── static/
└── config.json # Plugin configuration
package.json
Define your plugin metadata:{
"name": "my-napcat-plugin",
"plugin": "my-plugin",
"version": "1.0.0",
"description": "My awesome NapCat plugin",
"main": "index.ts",
"author": "Your Name",
"homepage": "https://github.com/yourusername/my-plugin",
"icon": "icon.png"
}
Plugin Module Interface
Fromtypes.ts:304-340:
export interface PluginModule {
// Required: Initialize plugin
plugin_init: (ctx: NapCatPluginContext) => void | Promise<void>;
// Optional: Handle messages
plugin_onmessage?: (ctx: NapCatPluginContext, event: OB11Message) => void | Promise<void>;
// Optional: Handle all events
plugin_onevent?: (ctx: NapCatPluginContext, event: OB11EmitEventContent) => void | Promise<void>;
// Optional: Cleanup on plugin unload
plugin_cleanup?: (ctx: NapCatPluginContext) => void | Promise<void>;
// Optional: Configuration schema
plugin_config_schema?: PluginConfigSchema;
plugin_config_ui?: PluginConfigSchema;
// Optional: Get/Set config
plugin_get_config?: (ctx: NapCatPluginContext) => unknown | Promise<unknown>;
plugin_set_config?: (ctx: NapCatPluginContext, config: unknown) => void | Promise<void>;
// Optional: Reactive config controller
plugin_config_controller?: (ctx: NapCatPluginContext, ui: PluginConfigUIController, initialConfig: Record<string, unknown>) => void | (() => void) | Promise<void | (() => void)>;
// Optional: Handle config changes
plugin_on_config_change?: (ctx: NapCatPluginContext, ui: PluginConfigUIController, key: string, value: unknown, currentConfig: Record<string, unknown>) => void | Promise<void>;
}
Plugin Context
The plugin context provides access to core functionality:interface NapCatPluginContext {
core: NapCatCore; // Core NapCat instance
oneBot: NapCatOneBot11Adapter; // OneBot adapter
actions: ActionMap; // Available actions
pluginName: string; // Your plugin ID
pluginPath: string; // Plugin directory path
configPath: string; // Config file path
dataPath: string; // Data directory path
NapCatConfig: NapCatConfigClass; // Config builder
adapterName: string; // Adapter name
pluginManager: IPluginManager; // Plugin manager
logger: PluginLogger; // Plugin logger
router: PluginRouterRegistry; // HTTP router
getPluginExports: <T>(pluginId: string) => T | undefined; // Access other plugins
}
Basic Plugin Example
Fromindex.ts:31-212:
Initialize Plugin
index.ts
const startTime: number = Date.now();
let logger: PluginLogger | null = null;
interface MyPluginConfig {
prefix: string;
enableReply: boolean;
description: string;
}
let currentConfig: MyPluginConfig = {
prefix: '#mybot',
enableReply: true,
description: 'My plugin description',
};
export const plugin_init: PluginModule['plugin_init'] = async (ctx) => {
logger = ctx.logger;
logger.info('My plugin initialized');
// Load saved configuration
try {
if (fs.existsSync(ctx.configPath)) {
const savedConfig = JSON.parse(fs.readFileSync(ctx.configPath, 'utf-8'));
Object.assign(currentConfig, savedConfig);
}
} catch (e) {
logger?.warn('Failed to load config', e);
}
};
Handle Messages
export const plugin_onmessage: PluginModule['plugin_onmessage'] = async (ctx, event) => {
if (!currentConfig.enableReply) return;
const prefix = currentConfig.prefix;
if (event.post_type !== 'message' || !event.raw_message.startsWith(prefix)) {
return;
}
logger?.info('Processing command:', event.raw_message);
try {
// Get bot version
const versionInfo = await ctx.actions.call('get_version_info', undefined, ctx.adapterName, ctx.pluginManager.config);
const message = `Bot Version: ${versionInfo.app_version}\nUptime: ${formatUptime(Date.now() - startTime)}`;
// Send reply
await ctx.actions.call('send_msg', {
message,
message_type: event.message_type,
...(event.message_type === 'group' ? { group_id: String(event.group_id) } : {}),
...(event.message_type === 'private' ? { user_id: String(event.user_id) } : {}),
}, ctx.adapterName, ctx.pluginManager.config);
logger?.info('Reply sent successfully');
} catch (error) {
logger?.error('Error handling message:', error);
}
};
function formatUptime(ms: number): string {
const seconds = Math.floor(ms / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
if (days > 0) return `${days}天 ${hours % 24}小时`;
if (hours > 0) return `${hours}小时 ${minutes % 60}分钟`;
if (minutes > 0) return `${minutes}分钟`;
return `${seconds}秒`;
}
Handle All Events
export const plugin_onevent: PluginModule['plugin_onevent'] = async (ctx, event) => {
// Handle different event types
if (event.post_type === 'notice') {
if (event.notice_type === 'group_increase') {
logger?.info(`New member joined group: ${event.group_id}`);
} else if (event.notice_type === 'group_decrease') {
logger?.info(`Member left group: ${event.group_id}`);
}
} else if (event.post_type === 'request') {
if (event.request_type === 'friend') {
logger?.info(`Friend request from: ${event.user_id}`);
}
}
};
Configuration System
Define Configuration Schema
export const plugin_config_ui: PluginConfigSchema = [
{
key: 'prefix',
type: 'text',
label: 'Command Prefix',
default: '#mybot',
description: 'The prefix for bot commands',
},
{
key: 'enableReply',
type: 'boolean',
label: 'Enable Auto Reply',
default: true,
description: 'Enable or disable automatic replies',
},
{
key: 'theme',
type: 'select',
label: 'Theme',
options: [
{ label: 'Light', value: 'light' },
{ label: 'Dark', value: 'dark' },
],
default: 'light',
},
{
key: 'features',
type: 'multi-select',
label: 'Enabled Features',
options: [
{ label: 'Feature 1', value: 'feat1' },
{ label: 'Feature 2', value: 'feat2' },
],
default: ['feat1'],
},
];
Get and Set Configuration
export const plugin_get_config: PluginModule['plugin_get_config'] = async (ctx) => {
return currentConfig;
};
export const plugin_set_config: PluginModule['plugin_set_config'] = async (ctx, config) => {
currentConfig = config as MyPluginConfig;
// Save to disk
try {
const configDir = path.dirname(ctx.configPath);
if (!fs.existsSync(configDir)) {
fs.mkdirSync(configDir, { recursive: true });
}
fs.writeFileSync(ctx.configPath, JSON.stringify(config, null, 2), 'utf-8');
} catch (e) {
logger?.error('Failed to save config', e);
throw e;
}
};
Reactive Configuration
Fromindex.ts:239-262:
export const plugin_config_controller: PluginModule['plugin_config_controller'] = async (ctx, ui, initialConfig) => {
logger?.info('Config controller initialized', initialConfig);
// Handle initial config
if (initialConfig['apiUrl']) {
await loadDynamicOptions(ui, initialConfig['apiUrl'] as string);
}
// Return cleanup function
return () => {
logger?.info('Config controller cleaned up');
};
};
export const plugin_on_config_change: PluginModule['plugin_on_config_change'] = async (ctx, ui, key, value, currentConfig) => {
logger?.info(`Config changed: ${key} = ${value}`);
if (key === 'apiUrl') {
// Update UI based on new value
await loadDynamicOptions(ui, value as string);
}
};
async function loadDynamicOptions(ui: PluginConfigUIController, apiUrl: string) {
if (!apiUrl) {
ui.removeField('apiEndpoints');
return;
}
// Add or update dynamic field
ui.addField({
key: 'apiEndpoints',
type: 'multi-select',
label: 'API Endpoints',
description: `Loaded from ${apiUrl}`,
options: [
{ label: `${apiUrl}/users`, value: '/users' },
{ label: `${apiUrl}/posts`, value: '/posts' },
],
default: [],
}, 'apiUrl');
}
WebUI Integration
Register HTTP Routes
Fromindex.ts:92-157:
// Routes mounted at /api/Plugin/ext/{pluginId}/
ctx.router.get('/status', (req, res) => {
res.json({
code: 0,
data: {
pluginName: ctx.pluginName,
uptime: Date.now() - startTime,
config: currentConfig,
},
});
});
ctx.router.post('/config', (req, res) => {
try {
const newConfig = req.body as Partial<MyPluginConfig>;
Object.assign(currentConfig, newConfig);
// Save config
fs.writeFileSync(ctx.configPath, JSON.stringify(currentConfig, null, 2));
res.json({ code: 0, message: 'Config saved successfully' });
} catch (e: any) {
res.status(500).json({ code: -1, message: e.message });
}
});
Serve Static Files
// Static files from directory (no auth required)
// Accessible at /plugin/{pluginId}/files/static/
ctx.router.static('/static', 'webui');
// Dynamic in-memory files (no auth required)
// Accessible at /plugin/{pluginId}/mem/dynamic/
ctx.router.staticOnMem('/dynamic', [
{
path: '/info.json',
contentType: 'application/json',
content: () => JSON.stringify({
pluginName: ctx.pluginName,
generatedAt: new Date().toISOString(),
uptime: Date.now() - startTime,
}, null, 2),
},
]);
Register Plugin Pages
ctx.router.page({
path: 'dashboard',
title: 'Plugin Dashboard',
icon: '📊',
htmlFile: 'webui/dashboard.html',
description: 'View plugin status and configuration',
});
Plugin Communication
Call Other Plugins
Fromindex.ts:160-195:
ctx.router.get('/call-plugin/:pluginId', (req, res) => {
const { pluginId } = req.params;
if (!pluginId) {
res.status(400).json({ code: -1, message: 'Plugin ID is required' });
return;
}
// Get other plugin's exports
const targetPlugin = ctx.getPluginExports<PluginModule>(pluginId);
if (!targetPlugin) {
res.status(404).json({ code: -1, message: `Plugin '${pluginId}' not found` });
return;
}
// Check available methods
res.json({
code: 0,
data: {
pluginId,
hasInit: typeof targetPlugin.plugin_init === 'function',
hasOnMessage: typeof targetPlugin.plugin_onmessage === 'function',
hasOnEvent: typeof targetPlugin.plugin_onevent === 'function',
},
});
});
Advanced Features
Access Core APIs
// Send messages
await ctx.actions.call('send_msg', {
message: 'Hello!',
message_type: 'group',
group_id: '123456',
}, ctx.adapterName, ctx.pluginManager.config);
// Get group list
const groups = await ctx.actions.call('get_group_list', undefined, ctx.adapterName, ctx.pluginManager.config);
// Get user info
const userInfo = await ctx.actions.call('get_stranger_info', {
user_id: '123456',
}, ctx.adapterName, ctx.pluginManager.config);
Use Plugin Logger
ctx.logger.info('Informational message');
ctx.logger.warn('Warning message');
ctx.logger.error('Error message', errorObject);
ctx.logger.debug('Debug information');
Store Plugin Data
const dataFile = path.join(ctx.dataPath, 'plugin-data.json');
// Save data
fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
// Load data
if (fs.existsSync(dataFile)) {
const data = JSON.parse(fs.readFileSync(dataFile, 'utf-8'));
}
Complete Plugin Example
index.ts
import type { PluginModule, PluginLogger, PluginConfigSchema } from 'napcat-types/napcat-onebot/network/plugin-manger';
import type { OB11Message } from 'napcat-types/napcat-onebot/types';
import fs from 'fs';
import path from 'path';
let logger: PluginLogger | null = null;
const commandHistory: Map<string, number> = new Map();
interface Config {
prefix: string;
cooldown: number;
enableLogging: boolean;
}
let config: Config = {
prefix: '!',
cooldown: 5000,
enableLogging: true,
};
export const plugin_config_ui: PluginConfigSchema = [
{
key: 'prefix',
type: 'text',
label: 'Command Prefix',
default: '!',
},
{
key: 'cooldown',
type: 'number',
label: 'Cooldown (ms)',
default: 5000,
},
{
key: 'enableLogging',
type: 'boolean',
label: 'Enable Logging',
default: true,
},
];
export const plugin_init: PluginModule['plugin_init'] = async (ctx) => {
logger = ctx.logger;
logger.info('Echo plugin initialized');
// Load config
try {
if (fs.existsSync(ctx.configPath)) {
const saved = JSON.parse(fs.readFileSync(ctx.configPath, 'utf-8'));
Object.assign(config, saved);
}
} catch (e) {
logger.warn('Failed to load config');
}
// Register API endpoint
ctx.router.get('/stats', (req, res) => {
res.json({
code: 0,
data: {
totalCommands: commandHistory.size,
config,
},
});
});
};
export const plugin_onmessage: PluginModule['plugin_onmessage'] = async (ctx, event) => {
const msg = event.raw_message;
if (!msg.startsWith(config.prefix)) return;
const userId = event.user_id.toString();
const now = Date.now();
const lastCmd = commandHistory.get(userId) || 0;
// Cooldown check
if (now - lastCmd < config.cooldown) {
if (config.enableLogging) {
logger?.info(`User ${userId} is on cooldown`);
}
return;
}
commandHistory.set(userId, now);
const command = msg.slice(config.prefix.length).trim();
const reply = `You said: ${command}`;
await ctx.actions.call('send_msg', {
message: reply,
message_type: event.message_type,
...(event.message_type === 'group' ? { group_id: String(event.group_id) } : {}),
...(event.message_type === 'private' ? { user_id: String(event.user_id) } : {}),
}, ctx.adapterName, ctx.pluginManager.config);
if (config.enableLogging) {
logger?.info(`Processed command from ${userId}: ${command}`);
}
};
export const plugin_get_config: PluginModule['plugin_get_config'] = async () => config;
export const plugin_set_config: PluginModule['plugin_set_config'] = async (ctx, newConfig) => {
config = newConfig as Config;
fs.writeFileSync(ctx.configPath, JSON.stringify(config, null, 2));
};
export const plugin_cleanup: PluginModule['plugin_cleanup'] = async (ctx) => {
logger?.info('Echo plugin shutting down');
commandHistory.clear();
};
Always implement proper error handling and cleanup in your plugins. Unhandled errors can crash the entire bot.
Best Practices
- Use the logger instead of
console.logfor better log management - Handle errors gracefully - wrap async operations in try-catch
- Validate configuration before using config values
- Clean up resources in
plugin_cleanup(timers, connections, files) - Use cooldowns to prevent command spam
- Store persistent data in
ctx.dataPath - Document your plugin with clear descriptions and examples
Next Steps
Packet Inspection
Monitor protocol packets
Message Handling
Learn about message APIs
