## plugin-migration-plan.md Part 6 标记完成,并**记录实际执行与计划的偏离**而非假装一致: 原计划「逐插件迁移,随时回退」。用户决策改为彻底舍弃 .so、无回退通道, 本轮直接删 internal/plugin/cabi/。因此【V】的「.so ↔ .bin 混跑集群冒烟」 不再适用——新内核根本不认 .so。改为验证「新内核面对旧 .so 给可操作错误 且不崩溃」,已在真实二进制上确认。 最终验收清单加「结果」列,13 项逐项对账。**两项未完全达标,如实标注**: - #9 SetToolBlocks:method 已定义并划入 CapCore,但内核侧仍返回未实现。 C ABI 时代它也是空实现(§1.4),故不是回归,但也没兑现 §3.8 的承诺。 - #13 内存:15 进程 RSS=88.0MB,远超「基线 +29MB」。根因是每插件静态链接 整个 Go runtime,15 个不同二进制无共同物理页(PSS/RSS 99.9% vs 基线 44%)。 实验 5 基线用的是 2.68MB 最小插件,绝对数字不可比;结构性指标 (均摊线程 5.5 vs 4.9)同量级。 新增两节实录:Part 6.5 生产切换(执行顺序为何不能反过来、hmap 正规通道 vs 手工拷贝的对照表、真实 QQ 消息的端到端证据链)与 Part 6.6 压测数据。 ## plugin-interface-matrix.md 状态从「基线 v1」升为「完成 v2」。三个合同面逐一标注达成情况: - 合同面 B:51 个整数 method id 已平移为 Method* 字符串常量。保留原表作 历史对照,但注明 case 25(CoreFreeString)无对应 method(内存管理是 C 层 特有问题),以及 io.setToolBlocks 已定义但内核侧未实现。 - 合同面 C:C1 标题从「今天」改为「迁移前」(迁移已完成,「今天」会误导); C2 补上「全部插件共享同一块 memfd」这个关键决定及其理由——第一版设计 是每插件一段,那会退化成副本模型复现 lost update。 - 第六节「新获得的能力」加「实际结果」列。事件订阅标注机制已完成但 零用户使用,故未经真实负载检验——这比只写 ✅ 诚实。 「刻意不给」清单同步为带 API 后缀的新命名(SelftestAPI 等),与 capability.go 的 withheldCapabilities 对齐,并说明为何加后缀: 不加时子串匹配会把 tool.register / io.setToolBlocks 误判为泄漏 ToolAPI。 ## PLUGIN_DEV.md(中英双份) C ABI 时代的描述全部改掉: - 「动态 .so/.dll 插件」→「子进程插件」 - 「生成 C ABI bridge(z_bridge_gen.go + z_entry.c)」→ 子进程运行时三文件 - 「go build -buildmode=c-shared」→「go build(CGO_ENABLED=0)」 - 平台二进制表:三平台统一 plugin.bin(bundle 包内按 goos.goarch 区分) - 「不能跨 C ABI 边界序列化」→「不能跨进程序列化」 - 「ABI v2 写回」→「Stage 写回」 新增 v1.0.0 破坏性变更提示框,五条要点:.so 不再加载、业务代码不需改、 entry 字段对 Go 插件已无意义、不再需要 cgo、Windows 从 3 字段升到全字段。 保留 .so 字样的只有变更说明本身(3 处),其余全部清理。
30 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 |
|---|---|---|
| Subprocess 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.
Source Debugging
plugindev debug interprets plugin source and prints a call trace, no compilation environment needed:
plugindev debug [dir] # dir defaults to the current directory
Creating a Go Plugin
plugindev init myplugin
cd myplugin
# Edit plugin code
vim plugin.go
# Build and package (default is a multi-platform bundle, see below)
plugindev build
# Output: dist/myplugin_bundle.hmap
# Single-platform build:
plugindev build --no-bundle
# 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
Subprocess runtime files (z_proc_gen.go and friends) 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 # default bundle mode (multi-platform)
plugindev build --no-bundle # single-target build (per plg.json targets)
plugindev build --target linux/amd64 # append a target on top of plg.json targets
plugindev build --outdir dist # output directory (default: dist)
plugindev build --sdk-path <path> # SDK path override (go.mod replace)
plugindev build --replace <mod@path> # append a go.mod replace directive (repeatable)
Execution process:
- Reads
plg.jsontargets/bundlefields to determine build targets (bundle takes priority, see below) - Auto-generates subprocess runtime code (
z_proc_gen.go+z_proc_shm_unix.go+z_proc_shm_windows.go) - Go plugin: Runs
go build(a plain executable,CGO_ENABLED=0) - Lua plugin: Packages source code directly, no compilation needed (contents:
plugin.json+main.lua, plus optionalREADME.md,LICENSE,thirdpart/*.lua) - 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 — single-target build list (e.g. "linux/amd64,windows/amd64"); bundle — multi-platform bundle switch (default true) |
plugin.json |
Build artifact manifest, auto-generated | entry — entry filename; platforms — declared platforms |
Each target produces a separate .hmap. Subprocess plugins are plain executables with
no platform-specific extension:
| Platform | Binary |
|---|---|
| Linux / macOS / Windows | plugin.bin |
Inside a bundle package the per-platform entries are named plugin.bin.<goos>.<goarch>;
the kernel picks the one matching the current platform and renames it to plugin.bin.
⚠️ v1.0.0 breaking change: external plugins moved from C ABI shared libraries to subprocess + shared memory.
plugin.so/plugin.dylib/plugin.dllare no longer loaded. The new kernel skips legacy artifacts with an actionable error instead of crashing.- Business code needs no changes — the public SDK interface is unchanged; just rebuild with the new
plugindev.- The
entryfield inplg.jsonis meaningless for Go plugins now (leavingplugin.sothere is harmless); it only distinguishes Lua plugins.- Artifacts no longer need cgo, so cross-compiling requires no target C toolchain.
- Windows went from "only 3 stage fields delivered, no writeback" to all 16 fields visible plus writeback, sharing the same RPC implementation as Unix.
Build Targets & Multi-platform Bundle
plugindev build defaults to bundle mode (unless plg.json explicitly sets "bundle": false): it 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.
plugindev build # default bundle, outputs dist/myplugin_bundle.hmap
plugindev build --bundle # explicitly enable bundle (same as above)
plugindev build --no-bundle # disable bundle, build per plg.json targets
Notes:
- In bundle mode the
plg.jsontargetsfield is ignored; the three platforms above are always built - Cross-compilation needs the corresponding toolchains (e.g. building darwin on Linux requires clang/macOS SDK); if a toolchain is missing the build fails — use
--no-bundleto build only the current platform - Single-target output naming:
{name}_{os}_{arch}.hmap, e.g.myplugin_linux_amd64.hmap
Output in dist/ directory:
dist/
├── myplugin_bundle.hmap # default bundle: multi-platform
├── myplugin_linux_amd64.hmap # after --no-bundle: Linux
├── myplugin_windows_amd64.hmap # after --no-bundle: Windows
├── myplugin_darwin_amd64.hmap # after --no-bundle: macOS
└── myplugin_lua.hmap # Lua plugin
Deployment
Install via PluginMgr HTTP API (three methods):
# 1. Install from URL (http/https only, streamed, no local temp file)
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 (reads the given file, source file untouched)
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
9876 is the pluginmgr local port (defaults to listening on 127.0.0.1 only, no auth).
Reload plugins via /api/v1/plugins/reload or restart the kernel to activate.
Or upload via the WebUI plugin management page, or through the WebUI HTTP API (default port 8080, requires the api_key bearer token; it proxies to pluginmgr):
curl -X POST http://127.0.0.1:8080/api/v1/plugins \
-H "Authorization: Bearer <api_key>" \
-H "Content-Type: application/json" \
-d '{"path": "/path/to/myplugin.hmap"}'
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 subprocess runtime code
(z_proc_gen.go for the platform-independent part, plus z_proc_shm_unix.go /
z_proc_shm_windows.go). All three platforms share the same entry point and the same
RPC logic; only the cross-process resource-passing mechanism differs (inherited fds on
Unix, named kernel objects on Windows). 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 be serialized across process boundaries, so it is unavailable for C/C++/Rust remote plugins. Lua plugins are not affected: pass a Lua function in the def table (cleaner = function(text) return text end) — the Go bridge calls it back per invocation during memory computation.
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.
Execution Model
Lua plugins run inside the kernel process on a gopher-lua interpreter (single Lua state guarded by a mutex). This differs fundamentally from Go plugins:
- Passive callback model:
main.luaexecutes only once at load time. Afterward, tools, stage hooks, output/input channels, and registered APIs are all invoked by the kernel via callbacks into Lua functions. Plugins cannot start background tasks on their own. - No concurrency / no long-running services: Lua has no goroutines, coroutine scheduling,
os/iolibraries, or socket listening. The only outbound capability issdk.http.get/post(synchronous). Any blocking loop will stall every call of that plugin while holding the lock. - For long-running services (listening on a port, background polling, timers) use a Go plugin (
plugin.binbuilt with the toolchain, which may spawn goroutines — see the webui/cli plugins). The Lua equivalent is event-driven: register tools/stage hooks/channels to be called back by the kernel, or interact with external processes viasdk.http.
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
The sdk.* API of Lua plugins is fully aligned with external plugins (toolchain-built plugin.bin subprocesses): registration functions raise a Lua error on failure; data functions uniformly return (result, err) with err == nil on success. Subsystems not wired by the core (e.g. SocialAPI) return empty values instead of errors.
Registration
| Function | Description |
|---|---|
sdk.log(level, msg) |
Log output |
sdk.register_tool(name, def, handler) |
Register tool; def supports description, parameters, no_memory, cleaner |
sdk.register_stage(stage, handler, scope) |
Register stage hook; scope is nil/"global" (default) or "own_tools" (fires only for before_toolcall/after_toolcall when the tool belongs to this plugin) |
sdk.register_api(name) |
Register API |
sdk.register_output_channel(name, caps, desc, def, handler) |
Register output channel; def supports no_memory, cleaner |
sdk.register_input_channel(name, def) |
Register input channel; def as above |
sdk.set_auto_restart(enabled) |
Auto-restart the plugin after a crash |
Stage hook context
Stage handlers receive the full context (same as external plugins): raw_message, user_id, group_id, phase, llm_text, final_text, no_memory, response (when responded), tool_calls, tool_results.
Stage writeback: the ctx table passed to the handler is a reference — mutating writable fields inside the handler syncs back to the core StageContext (aligned with subprocess external-plugin capability):
sdk.register_stage("on_input", function(ctx)
ctx.raw_message = "[clean]" .. ctx.raw_message -- modify input, adopted by core
end)
sdk.register_stage("post_action", function(ctx)
ctx.llm_text = ctx.llm_text .. "[tail]" -- modify LLM output
ctx.tool_results = { { call_id = "x", result = "rewritten" } }
end)
Writable fields: raw_message, llm_text, final_text, user_id, group_id, no_memory, response, tool_calls, tool_results. Other fields are read-only.
IO and config
| Function | Description |
|---|---|
sdk.get_setting(key) / sdk.set_setting(key, value) |
Own plugin config read/write |
sdk.settings.get_core/set_core/list_core(key) |
Core config read/write |
sdk.settings.get_plugin/set_plugin/list_plugin(plugin, key) |
Other plugin config read/write |
sdk.settings.list/defs/dump/plugins(prefix) |
Config queries |
sdk.settings.register_def(def) |
Register config definition (WebUI display) |
sdk.inject_text(source, channel, text) |
Deliver text message |
sdk.inject_interrupt(source, channel, text) |
Interrupt delivery |
sdk.inject_text_no_memory(source, channel, text) |
Deliver without memory computation |
Data APIs (aligned with subprocess external plugins, all return (result, err))
| Sub-table | Functions |
|---|---|
sdk.memory.* |
recall(query, depth), commit({triples}), introspect(), merge(source, target), purge(criteria, hard) |
sdk.doc.* |
query(text, top_k), insert({id,title,content}), remove(id), stats() |
sdk.knowledge.* |
search(query, limit), add(tag, content), list() |
sdk.text_memory.* |
append({role,content,timestamp,channel}) |
sdk.llm.* |
list_sources(), set_source(name), current_source() |
sdk.social.* (read-only) |
get_person(name), get_network(name, depth), get_trait(name, trait), get_relations(name), list_persons() |
sdk.json.* |
encode(val), decode(str) |
sdk.http.* |
get(url), post(url, body, content_type) |
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 |
|---|---|---|
| weather | Go | Weather queries (wttr.in); demonstrates NoMemory/Cleaner/stage hooks/channels/text memory |
| luademo | Lua | Full-featured Lua example covering the whole v0.8.0 Lua SDK surface |
| Go | NapCat OneBot integration, 17 tools, full input/output channel wiring | |
| 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) |
| 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 |
| calendar | Go | Calendar management |
| rss | Go | RSS subscriptions |
| ai_image | Go | AI image generation |
| music | Go | Music playback |
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.