PluginContext is the only interface plugins use to interact with xbot. It provides controlled, permission-filtered access to xbot’s capabilities.
PluginContext is a composite interface combining multiple sub-interfaces:
type PluginContext interface {
ToolRegistrar // Register tools and middleware
HookSubscriber // Subscribe to lifecycle hooks
StorageProvider // Per-plugin KV storage
SessionMetadata // Read-only session info
EventBusPublisher // Plugin-to-plugin events
UIContributor // Widgets, themes, overlays
CronScheduler // Schedule cron jobs
// ... plus configuration and channel provider methods
}
Access is filtered by declared permissions — plugins can only use what they declare in plugin.json.
Register tools for the LLM to use:
type ToolRegistrar interface {
RegisterTool(tool PluginTool) error
RegisterTools(tools ...PluginTool) error
UseMiddleware(middleware PluginMiddleware) error
}
Permission required: tools.register
func (p *MyPlugin) Activate(ctx plugin.PluginContext) error {
tool := plugin.ToolFromFunc("greet", "Greet someone", func(ctx context.Context, input string) (string, error) {
return "Hello!", nil
})
return ctx.RegisterTool(tool)
}
Subscribe to lifecycle hooks:
type HookSubscriber interface {
OnPreToolUse(matcher string, handler HookHandler) error
OnPostToolUse(matcher string, handler HookHandler) error
OnUserPrompt(handler HookHandler) error
OnAgentStop(handler HookHandler) error
OnSessionStart(handler HookHandler) error
OnSessionEnd(handler HookHandler) error
OnEvent(event HookEvent, matcher string, handler HookHandler) error
OnAllToolUse(handler HookHandler) error
OnError(handler HookHandler) error
}
Permission required: hooks.register
func (p *MyPlugin) Activate(ctx plugin.PluginContext) error {
return ctx.OnPreToolUse("Shell", func(ctx context.Context, payload *plugin.HookPayload) (*plugin.HookResult, error) {
// Intercept Shell tool calls before execution
return &plugin.HookResult{Decision: plugin.DecisionAllow}, nil
})
}
Per-plugin persistent key-value storage:
type StorageProvider interface {
Storage() StorageAccessor
StorageInt(key string) (int64, bool)
StorageBool(key string) (bool, bool)
StorageJSON(key string, value any) error
StorageGetJSON(key string, target any) error
}
Permission required: storage
Storage location: ~/.xbot/plugins/<id>/data/storage.json
func (p *MyPlugin) Activate(ctx plugin.PluginContext) error {
// Store a value
ctx.Storage().Set("counter", "42")
// Retrieve typed values
count, ok := ctx.StorageInt("counter") // int64(42), true
// Store JSON
ctx.StorageJSON("config", map[string]any{"theme": "dark"})
// Retrieve JSON
var cfg map[string]any
ctx.StorageGetJSON("config", &cfg)
return nil
}
Read-only session information:
type SessionMetadata interface {
PluginID() string
WorkingDir() string
Channel() string // "cli", "web", "feishu", etc.
ChatID() string
TenantID() int64
Logger() Logger
}
Available to all plugins (no permission required).
func (p *MyPlugin) Activate(ctx plugin.PluginContext) error {
logger := ctx.Logger()
logger.Info("Plugin activated",
plugin.Field{Key: "workDir", Value: ctx.WorkingDir()},
plugin.Field{Key: "channel", Value: ctx.Channel()},
)
return nil
}
Plugin-to-plugin pub/sub communication:
type EventBusPublisher interface {
Subscribe(topic string, handler PluginEventHandler) error
Publish(topic string, data any) error
}
Permissions required: bus.read (subscribe), bus.write (publish), bus.plugin (both)
func (p *MyPlugin) Activate(ctx plugin.PluginContext) error {
// Subscribe to events from other plugins
ctx.Subscribe("xbot.git-fancy:commit", func(ctx context.Context, topic string, data any) error {
logger := ctx.Logger()
logger.Info("Received commit event")
return nil
})
// Publish events to other plugins
ctx.Publish("xbot.my-plugin:ready", map[string]any{"version": "1.0.0"})
return nil
}
Register UI widgets, themes, and overlays:
type UIContributor interface {
ContributeUI(widgetID, zone string, widget UIWidget, priority int) error
UpdateWidget(widgetID string) error
SetWidgetRegistry(wr *WidgetRegistry)
ContributeTheme(id string, themeData []byte) error
RegisterOverlay(id string, provider OverlayProvider) error
ShowOverlay(id string) error
HideOverlay() error
RegisterWebActionHandler(widgetID string, handler WebActionHandler) error
}
Permission required: ui.contribute (widgets), ui.themes (themes)
func (p *MyPlugin) Activate(ctx plugin.PluginContext) error {
widget := &MyWidget{}
return ctx.ContributeUI("my-widget", "statusBarRight", widget, 100)
}
Schedule cron jobs:
type CronScheduler interface {
ScheduleCron(spec CronContribution) (string, error)
}
Permission required: cron
func (p *MyPlugin) Activate(ctx plugin.PluginContext) error {
_, err := ctx.ScheduleCron(plugin.CronContribution{
Message: "Check for updates",
EverySeconds: 3600,
})
return err
}
Read and update the plugin’s own user configuration, and subscribe to live changes:
// PluginContext exposes three configuration methods:
Config() (map[string]any, error) // merged config: manifest defaults + user overrides
SetConfig(key string, value any) error // persist a single key (~/.xbot/plugins/<id>/config.json)
OnConfigChanged(cb func(map[string]any)) error // subscribe to changes (hot reload)
Permission required: config
Declare configurable settings in plugin.json under contributes.configuration:
{
"contributes": {
"configuration": {
"title": "My Plugin Settings",
"properties": {
"mode": {
"type": "select",
"label": "运行模式",
"description": "Choose how the plugin behaves",
"default": "auto",
"options": [
{ "label": "Auto", "value": "auto" },
{ "label": "Manual", "value": "manual" }
]
},
"level": { "type": "number", "label": "Level", "default": 5, "minimum": 1, "maximum": 100 }
}
}
}
}
Supported property types: boolean, string, number, select, multiselect. Each property may also declare label, description, default, options (for select/multiselect), section (grouping), secret (masked input), placeholder, required, minimum/maximum.
The Web UI renders these into a settings form (Settings, Plugins category) automatically from the schema. Users can edit them there, and changes are hot-reloaded: OnConfigChanged fires with the new merged config within the running plugin (no reload needed).
func (p *MyPlugin) Activate(ctx plugin.PluginContext) error {
cfg, err := ctx.Config()
if err != nil {
return err
}
mode, _ := cfg["mode"].(string)
// Apply live changes without restarting:
if err := ctx.OnConfigChanged(func(merged map[string]any) {
// e.g. update an internal field, re-run a widget, etc.
}); err != nil {
return err
}
return nil
}
Configuration is stored at ~/.xbot/plugins/<id>/config.json (global, shared by all users).
Every PluginContext method call is checked against the plugin’s declared permissions. If a plugin tries to use a capability it didn’t declare, the call returns an error:
// plugin.json declares: ["tools.register"]
// This works:
ctx.RegisterTool(tool)
// This returns an error:
ctx.Subscribe("topic", handler) // bus.read not declared
The plugin/sdk.go file provides convenience functions:
// Create a simple tool from a function
tool := plugin.ToolFromFunc("name", "desc", func(ctx context.Context, input string) (string, error) {
return "result", nil
})
// Create a tool with JSON input
tool := plugin.ToolFromJSONFunc("name", "desc", params, func(ctx context.Context, input json.RawMessage) (any, error) {
return result, nil
})
// Pre-built hook handlers
plugin.DenyHook("blocked") // Always deny
plugin.AllowHook() // Always allow
plugin.LogHook(logger, "msg") // Log and allow