Give your plugin user-editable settings with declarative schema, defaults, persistence, and change notifications. The config store is plugin/config.go; the reference implementation is xbot.git-fancy (plugins/xbot-git-fancy/plugin.json), whose “默认 Commit 条数” and “显示 Diff 变更统计” settings appear in the web UI.
{
"id": "xbot.git-fancy",
"contributes": {
"configuration": {
"title": "Git Fancy",
"properties": {
"defaultLogLimit": {
"type": "number",
"label": "默认 Commit 条数",
"description": "Git 日志面板默认加载的 commit 数量(log limit 默认值)",
"default": 10,
"minimum": 1,
"maximum": 100
},
"showDiffStats": {
"type": "boolean",
"label": "显示 Diff 变更统计",
"description": "commit 详情中显示 numstat 变更统计(++/-- 行数)",
"default": true
}
}
}
}
}
ConfigurationContribution (plugin/plugin.go) supports title, properties (each a ConfigProperty with type: number/boolean/string/toggle/text, label, description, default, minimum, maximum), and order. The schema is also derivable from web.contributes (configSchemaFromWebContribs).
PluginContext.Config() returns manifest defaults overlaid with user values:
func (p *Plugin) Activate(ctx plugin.PluginContext) error {
cfg, err := ctx.Config()
if err != nil {
return err
}
limit := 10
if v, ok := cfg["defaultLogLimit"].(float64); ok {
limit = int(v)
}
showStats := true
if v, ok := cfg["showDiffStats"].(bool); ok {
showStats = v
}
// ...
}
Script plugins get the same data without an RPC — XBOT_PLUGIN_CONFIG env var carries the merged config as JSON (plugin/script_runtime.go pluginConfigJSON).
ctx.OnConfigChanged(func(config map[string]any) {
ctx.Logger().Infof("config changed: %v", config)
// re-read values, refresh widgets, etc.
})
The subscription is released automatically on deactivate.
ctx.SetConfig("defaultLogLimit", 20) // persists + notifies subscribers
- Storage location:
~/.xbot/plugins/<id>/config.json(PluginConfigStore.configPath). - Merging:
Load(pluginID)returns user config;GetDefaultConfig(manifest)extracts defaults fromcontributes.configuration; the merged view is whatConfig()returns. An in-memory cache is invalidated byUpdate(InvalidateCache). - Notification:
PluginConfigStore.Subscribe(pluginID, cb)fires onUpdate—notifyChangedispatches to all subscribers. - Web UI + RPC:
plugin.get_config/plugin.set_configRPC methods (typed inBackendRPC,web/src/plugin-api/rpc.ts) drive the frontend settings form;plugin.set_configreturns{status, key}.
The plugin system composes three layers:
| Layer | Source | Precedence |
|---|---|---|
| User config | config.json (written by SetConfig / web UI) | highest |
| Manifest defaults | contributes.configuration.properties[].default | middle |
| Zero values | code fallbacks | lowest |
Config() always returns the merged map — never a partial view.
ExportConfig/ImportConfig(plugin/export.go) carry plugin config across machines;ImportConfigrestores configs only for plugins that exist locally (missing ones are skipped with a warning).- Values from JSON are
float64/bool/string— assert types defensively (thegit-fancypattern above). - Changing the schema shape of an existing key breaks users with saved configs — prefer adding new keys and migrating old ones in
Activate(seeplugin/migration.goRegisterMigrationfor the storage-migration machinery).