22 KiB
中文 | English
HomeAgent Plugin Development Guide
Overview
All external interaction capabilities of HomeAgent comes from plugins. Plugins interact with the kernel through PluginSDK (Go API).
SDK Repository: Plugin development tools, template code, and example plugins are hosted in the homeagent-sdk repository.
git clone https://gitcode.com/JianFeeeee/homeagent-sdk.git
cd homeagent-sdk
Each plugin implements a three-method interface:
type Plugin interface {
Name() string
Start(sdk *PluginSDK) error
Stop() error
}
Three Development Methods
| Method | Use Case | Complexity |
|---|---|---|
| Dynamic .so/.dll plugin (recommended) | Independently distributed third-party plugins | Medium, generated using plugindev toolchain |
| Built-in plugin | Released with HomeAgent | Simple, requires merging into main repo |
| Lua script plugin | Lightweight rapid prototyping | Simple, generated using plugindev init --lua |
1. Quick Start: Using the plugindev Toolchain
plugindev is the unified plugin development toolchain provided in the SDK repository, supporting both Go and Lua plugin types.
Installation
cd homeagent-sdk/tools/plugindev
go build -o plugindev
# Add plugindev to PATH or use directly
SDK Version Management
plugindev sdk manages local SDK versions:
plugindev sdk list # list installed SDK versions
plugindev sdk current # show current SDK version
plugindev sdk latest # show latest available version
plugindev sdk install v0.8.0 # install a specific version
plugindev sdk use v0.8.0 # switch to a version
plugindev sdk path # show current SDK path
SDK is stored at ~/.homeagent/plugindev/sdk/<version>/; plugindev init reads the current SDK version for go.mod.
Creating a Go Plugin
plugindev init myplugin
cd myplugin
# Edit plugin code
vim plugin.go
# Build and package
plugindev build
# Output: dist/myplugin_linux_amd64.hmap (or windows_amd64)
Creating a Lua Plugin
plugindev init myluaplugin --lua
cd myluaplugin
# Edit plugin code
vim main.lua
# Local test
lua main.lua
# Build and package
plugindev build
# Output: dist/myluaplugin_lua.hmap
Template Project Structure
Go plugin:
myplugin/
├── plg.json — Plugin metadata (name, version, entry, target platforms)
├── plugin.go — Plugin implementation (Plugin interface + NewPlugin export)
├── go.mod — Go module definition
├── README.md — Documentation
└── thirdpart/ — Optional external source code directory
C ABI bridge files (z_bridge_gen.go + z_entry.c) are auto-generated at build time.
Lua plugin:
myluaplugin/
├── plg.json — Plugin metadata (entry: "main.lua", targets: "lua")
├── main.lua — Plugin implementation (Lua version of Plugin interface)
├── sdk.lua — SDK mock layer (supports `lua main.lua` standalone testing)
└── README.md — Documentation
Build & Package
plugindev build automatically handles compilation and packaging:
cd myplugin
plugindev build
Execution process:
- Reads
plg.jsontargetsfield to determine target platforms - Auto-generates C ABI bridge code (
z_bridge_gen.go+z_entry.c) - Go plugin: Runs
go build -buildmode=c-shared(produces.so/.dylib/.dll) - Lua plugin: Packages source code directly, no compilation needed
- Generates
plugin.jsonoutput manifest - Packages as
.hmapdistribution (zip format, containingplugin.json+ binary)
plg.json (project config) vs plugin.json (output manifest)
| File | Purpose | Key fields |
|---|---|---|
plg.json |
Project metadata, maintained by developer | targets — build targets (e.g. "linux/amd64,windows/amd64") |
plugin.json |
Build artifact manifest, auto-generated | entry — entry filename; platforms — declared platforms |
Each target produces a separate .hmap; binary name by platform:
| Platform | Binary |
|---|---|
| Linux | plugin.so |
| macOS | plugin.dylib |
| Windows | plugin.dll |
Multi-platform bundle: --bundle
plugindev build --bundle
Builds linux/amd64 + darwin/amd64 + windows/amd64 in one pass, producing a single .hmap
with all platform binaries. The output manifest includes a platforms field.
The kernel auto-selects the correct binary during installation.
Output in dist/ directory:
dist/
├── myplugin_linux_amd64.hmap # Single platform: Linux
├── myplugin_windows_amd64.hmap # Single platform: Windows
├── myplugin_darwin_amd64.hmap # Single platform: macOS
├── myplugin_bundle.hmap # Multi-platform bundle
└── myplugin_lua.hmap # Lua plugin
Deployment
Install via PluginMgr HTTP API (three methods):
# 1. Install from URL (auto-cleanup)
curl -X POST http://127.0.0.1:9876/plugins \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/myplugin.hmap"}'
# 2. Install from local path (keeps source file)
curl -X POST http://127.0.0.1:9876/plugins \
-H "Content-Type: application/json" \
-d '{"path": "/path/to/myplugin.hmap"}'
# 3. Upload binary directly
curl -X POST http://127.0.0.1:9876/plugins \
--data-binary @dist/myplugin.hmap
Reload plugins via /api/v1/plugins/reload or restart the kernel to activate.
Or upload via WebUI plugin management page.
2. Go Plugin Development in Detail
Plugin Interface
package main
import "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
type Plugin struct {
name string
sdk *sdk.PluginSDK
}
func (p *Plugin) Name() string { return p.name }
func (p *Plugin) Start(s *sdk.PluginSDK) error {
p.sdk = s
// Register config items, tools, stage hooks, etc.
return nil
}
func (p *Plugin) Stop() error {
// Clean up resources
return nil
}
// NewPluginFactory creates plugin instance (called by main.go or Windows bridge)
func NewPluginFactory(name string, config map[string]interface{}) (sdk.Plugin, error) {
return &Plugin{name: name}, nil
}
Entry Point
plugindev init generates plugin.go with the NewPlugin export function directly,
which is the entry point when the kernel loads the plugin:
func NewPlugin(name string, config map[string]interface{}) (sdk.Plugin, error) {
return &Plugin{name: name}, nil
}
At build time, plugindev build auto-generates C ABI bridge code (z_bridge_gen.go + z_entry.c),
shared by both Windows DLL and Linux/macOS .so builds. No manual bridge code needed.
PluginSDK Core API
Tool Registration — Make your capabilities callable by LLM
s.RegisterTool("weather_query", sdk.ToolDef{
Name: "weather_query",
Description: "Query weather for a specified city",
NoMemory: false, // false=output participates in memory, true=skip
// Cleaner: func(output string) string { // Optional: clean output before vector/jieba/distill
// return extractJSON(output, "content")
// },
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"city": map[string]interface{}{
"type": "string",
"description": "City name, e.g. Beijing",
},
},
"required": []string{"city"},
},
}, func(args map[string]interface{}) (interface{}, error) {
city, _ := args["city"].(string)
return map[string]interface{}{
"city": city,
"temp": 25,
"weather": "Sunny",
}, nil
})
NoMemory and Cleaner
NoMemory and Cleaner are optional fields on ToolDef that control how tool output participates in the memory computation layer (vectorization, jieba tokenization, distillation):
-
NoMemory(defaultfalse): Whentrue, the tool's output is excluded from all memory computation (vector, tokenization, distillation), but the original text is preserved in Context and Document. LLM attention is unaffected. Use cases:cmd_run(unpredictable noise in command output), pure operation tools like file upload/delete. -
Cleaner(optional): A functionfunc(output string) string. When set, the tool output is filtered through this function before participating in vectorization/jieba/distillation. Typical use: stripping SQL prefixes, extracting acontentfield from JSON. The original output is never modified — Cleaner only affects the computation layer input.
Decision matrix:
Tool output → valuable for LLM attention?
├── No → NoMemory=true (output preserved, skipped in computation)
└── Yes → Contains cleanable noise?
├── Yes → Cleaner filters before computation
└── No → Normal memory, no extra handling
Note
:
Cleaneris a Gofunctype (json:"-"), cannot cross C ABI boundaries. Not available for Lua plugins or remote plugins.
Stage Hooks — Intervene in message processing flow
7 stages:
| Stage | Timing | Purpose |
|---|---|---|
on_input |
Message just arrived at Agent | Blacklist, rate-limit, short-circuit |
pre_action |
About to call LLM | Inject context |
post_action |
LLM returned results | Modify output/tool list |
before_toolcall |
Before tool execution | Audit, reject, modify params |
after_toolcall |
After tool execution | Desensitize, rewrite results |
before_output |
Before output | Format adaptation, leak cleanup |
after_output |
After output | Statistics/logging |
// Global: receive all stage events
s.RegisterStage(sdk.StagePreAction, func(ctx *sdk.StageContext) error {
ctx.Lock()
ctx.ContextMsgs = append(ctx.ContextMsgs, map[string]interface{}{
"role": "system",
"content": "Injected context content",
})
ctx.Unlock()
return nil
})
// Own tools only: only before_toolcall/after_toolcall for this plugin's tools
s.RegisterStage(sdk.StageBeforeToolcall, myHandler, sdk.StageScopeOwnTools)
Configuration Management
// Register config definition
s.Settings().RegisterDef(sdk.ConfigDef{
Key: "plugin.myplugin.api_key",
Default: "",
Type: "string",
DisplayName: "API Key",
Description: "API key",
Category: "myplugin",
})
// Read/write config
val, err := s.Settings().Get("api_key")
s.Settings().Set("api_key", "new-value")
// Read core config
s.Settings().GetCore("llm.model")
// Read other plugin's config
s.Settings().GetPlugin("other_plugin", "some_key")
Input Delivery
// Normal delivery (processed in order)
s.InjectText(source, channel, text string)
// Interrupt delivery (can interrupt current LLM processing)
s.InjectInterruptText(source, channel, text string)
// No memory recording
s.InjectTextNoMemory(source, channel, text string)
Input Channel Registration — Declare External Message Sources
s.RegisterInputChannel("qq", sdk.ChannelDef{
NoMemory: true,
Cleaner: func(text string) string {
return strings.TrimSpace(text)
},
})
ChannelDef controls channel behavior in the memory computation layer:
| Field | Default | Description |
|---|---|---|
NoMemory |
false |
Channel input/output skips vectorization/keyword/distillation; original text preserved in context |
Cleaner |
nil |
func(string) string computation filter (does not modify original text) |
Noisy sources (QQ group messages, RSS feeds, etc.) should set NoMemory: true.
Output Channel Registration — Declare Output Destinations
s.RegisterOutputChannel("email", 1, "Send Email", sdk.ChannelDef{
NoMemory: true,
}, func(args map[string]interface{}) (interface{}, error) {
to, _ := args["to"].(string)
subject, _ := args["subject"].(string)
body, _ := args["body"].(string)
return map[string]interface{}{"status": "sent"}, nil
})
Parameters: name (route key), caps (1=text/2=rich/4=file/8=image), desc, def (ChannelDef), handler (callback).
Event Subscription
import "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
unsub := s.Events().Subscribe(sdk.EventToolCall, func(evt *sdk.Event) {
log.Printf("Tool was called: %v", evt.Payload)
})
defer unsub()
Capability Access
// Graph Memory (entity-relation store)
entities, relations, err := s.Memory().Recall([]string{"keyword"}, 2)
// Document Memory (vector store)
docs := s.DocMemory().Query("query text", 3)
// Knowledge
results, err := s.Knowledge().Search("query", 5)
// LLM source management
s.LLM().ListSources() // returns []string
s.LLM().SetSource("deepseek")
Event Subscription (built-in plugins)
// Subscribe to system events, returns unsubscribe function
unsub := s.Subscribe("tool_call", func(evt *events.Event) {
log.Printf("Tool was called: %v", evt.Payload)
})
defer unsub()
// Publish event
s.Publish(&events.Event{
Type: "custom_event",
Payload: map[string]interface{}{"key": "value"},
})
IO Channel Management (built-in plugins)
// Register a channel (bind device driver), dev must implement the agentIO.Device interface:
// Name() string
// Type() DeviceType
// Description() string
// Tools() []ToolDef
// Execute(tool string, args map[string]interface{}) (interface{}, error)
// Start() error
// Stop() error
// OutputCapabilities() OutputCapability
s.RegisterChannel("mydevice", deviceImpl)
// Unregister a channel
s.UnregisterChannel("mydevice")
// List all channels
channels := s.ListChannels()
Input Delivery (built-in plugins)
// Queued delivery (processed in order)
s.InjectInput(source, channel, eventType string, payload map[string]interface{})
// Synchronous delivery (waits for response)
resp := s.InjectInputSync(source, channel, eventType string, payload map[string]interface{})
// Interrupt delivery (can preempt current LLM processing)
s.InjectInterrupt(source, channel, eventType string, payload map[string]interface{})
// Synchronous text shortcuts
resp := s.InjectTextSync(source, channel, text string)
resp := s.InjectTextSyncNoMemory(source, channel, text string)
// Get output channel
outputCh := s.OutputChan()
Note
:
Subscribe,Publish,RegisterChannel,UnregisterChannel,ListChannels,InjectInput,InjectInputSync,InjectInterrupt,InjectTextSync,InjectTextSyncNoMemory,OutputChanare only available in built-in plugins (internal/sdkpackage). External dynamic plugins should use the public APIs:InjectText,InjectInterruptText,InjectTextNoMemory.
3. Lua Plugin Development in Detail
Lua plugins are suitable for lightweight rapid prototyping, requiring no Go compilation environment. Changes take effect after kernel restart.
Plugin Structure
-- main.lua
local plugin = {
name = "myluaplugin"
}
function plugin.start(sdk)
sdk.log("info", "myluaplugin starting...")
sdk.register_tool("myluaplugin_hello", {
description = "A hello world tool",
parameters = {
type = "object",
properties = {}
}
}, function(args)
return { content = "Hello from myluaplugin plugin!" }
end)
sdk.log("info", "myluaplugin started")
end
function plugin.stop()
sdk.log("info", "myluaplugin stopped")
end
return plugin
SDK Mock Layer
sdk.lua provides a pure Lua SDK mock implementation, supporting lua main.lua standalone testing:
lua main.lua
# Output:
# [lua-plugin] info: myluaplugin starting...
# [lua-plugin] register_tool: myluaplugin_hello
# [lua-plugin] info: myluaplugin started
When running inside the kernel, sdk.* global variables are injected by the Go layer, and all functions marked with -- !impl are replaced with real implementations.
Lua SDK API
| Function | Description |
|---|---|
sdk.log(level, msg) |
Log output |
sdk.register_tool(name, def, handler) |
Register tool |
sdk.register_stage(stage, handler) |
Register stage hook |
sdk.register_api(name) |
Register API |
sdk.get_setting(key) |
Read config |
sdk.set_setting(key, value) |
Write config |
sdk.inject_text(source, channel, text) |
Deliver text message |
sdk.inject_interrupt(source, channel, text) |
Interrupt delivery |
sdk.json.encode(val) |
JSON encode |
sdk.json.decode(str) |
JSON decode |
sdk.http.get(url) |
HTTP GET request (-- !impl) |
sdk.http.post(url, body, content_type) |
HTTP POST request (-- !impl) |
Note
: Lua plugin's
sdk.register_stagecallback currently only receivesraw_message,user_id,phasefields. The functionality is limited. For complex stage handling logic, use Go plugins.
4. Built-in Plugins
Built-in plugins use init() self-registration, compiled into the kernel, no separate deployment needed.
Directory Structure
internal/plugins/yourplugin/
plugin.go — Plugin main file
Minimal Plugin Example
package yourplugin
import (
"gitcode.com/JianFeeeee/HomeAgent/internal/plugin"
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
)
func init() {
plugin.RegisterFactory("yourplugin", func(name string, config map[string]interface{}) (sdk.Plugin, error) {
return New(name), nil
})
}
type Plugin struct {
name string
}
func New(name string) *Plugin {
return &Plugin{name: name}
}
func (p *Plugin) Name() string { return p.name }
func (p *Plugin) Start(s *sdk.PluginSDK) error {
// Initialize plugin here: start goroutines, register tools, subscribe events, etc.
return nil
}
func (p *Plugin) Stop() error {
// Clean up resources
return nil
}
Register with Kernel
Add blank import in internal/plugins/all.go:
package plugins
import (
_ "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/yourplugin"
// ... other plugins
)
5. Best Practices
Start()is non-blocking — start long tasks in goroutines, don't block StartStop()cleans up resources — close connections, stop goroutines, cancel subscriptions- Unique tool names — use plugin name prefix to avoid conflicts
- When handler returns
error, LLM will receive it and may retry - Use
InjectInterruptTextfor interrupts,InjectTextfor normal delivery - Use
Settings().Get/Setfor config, don't hardcode - External Go plugins compile independently, not tied to kernel version; only built-in plugins need recompilation with kernel
6. Plugin Management
CLI Commands
/plugin list # List all plugins with status (loaded/disabled)
/plugin disable <name> # Disable plugin (immediate, no longer receives input)
/plugin enable <name> # Enable plugin (restored after restart)
/plugin reload # Reload all plugins
WebUI
Dashboard plugin list provides "Disable/Enable" buttons in the actions column. Disabling WebUI itself shows a confirmation dialog to prevent misoperation.
Built-in Plugin API
pmgr := s.PluginMgr()
pmgr.DisablePlugin("qq", "admin") // Disable
pmgr.EnablePlugin("qq") // Enable
list := pmgr.ListDisabledPlugins() // List disabled plugins
loaded := pmgr.ListLoadedPlugins() // List loaded plugins
pmgr.IsPluginDisabled("qq") // Check if disabled
pmgr.ReloadPlugins() // Reload all plugins
Internal: records are stored in SQLite disabled_plugins table (name, disabled_at, disabled_by). Disabling takes effect immediately (plugin stops receiving input); full removal requires a restart.
Note
:
PluginMgr()is only available to built-in plugins; external dynamic plugins cannot call it directly.
7. Example Plugin Reference
SDK Repository Examples (homeagent-sdk/example/)
| Example | Type | Features |
|---|---|---|
| memo | Go | Memo management, PreAction injection + timed interrupt dual reminder |
| files | Go | File system operations, 4 write modes, sandbox isolation |
| browser | Go | Web search + HTTP fetch (SSRF) + Chromium render (merged from web/webfetch) |
| bili | Go | Bilibili video download (yt-dlp) |
| Go | NapCat OneBot integration, 17 tools | |
| editdoc | Go | Office document editing and format conversion |
| a2a | Go | Agent-to-Agent protocol |
| ocr | Go | Offline text recognition (Tesseract) |
| sanitizer | Go | Output sanitizer filter |
Built-in Plugins
| Plugin | Location | Features |
|---|---|---|
| Timer | internal/plugins/timer/ |
Simplest complete example, registers one tool + interrupt feedback |
| CLI | internal/plugins/cli/ |
Unix socket listener + synchronous request-response |
| WebUI | internal/plugins/webui/ |
HTTP service + dependency injection |
Want to understand the project goals? See OVERVIEW.md. Want to understand the architecture? See ARCHITECTURE.md. SDK repository and development tools? See homeagent-sdk.