Files
HomeAgent/docs/en/PLUGIN_DEV.md
root c9e67d3d55 docs: 修正全部文档使其与源码实现一致
主仓库:
- 修复 4 份英文文档语言切换链接指向错误 (../zh/ → ../en/)
- ARCHITECTURE.md 标题 "三种加载方式" → "四种加载方式" (实际表格4行)
- PLUGIN_DEV.md 示例表: 添加 webfetch, 移除不存在的 luaplugintest/testlua
- PLUGIN_DEV.md 代码示例: InjectInput/InjectInterrupt → InjectText/InjectInterruptText
- PLUGIN_DEV.md 代码示例: Memory/Knowledge/LLM/Events 接口签名修正
- PLUGIN_DEV.md .hmap 内容统一, plugindev 编译去除 .exe 后缀

SDK 仓库:
- Plugin.Start(sdk *PluginSDK) 接口签名改为指针
- 方法表重写: 移除 CallLLM/QueryKnowledge/SetMemory 等不存在方法
- IOInjector 参数顺序修正为 (source, channel, text)
- 删除虚构 SDKConfig, 替换为实际 New() 构造函数签名
- .hmap 内容描述一致化

修正前一次会话中的 QQ/Bili 插件问题:
- qq napcat() 超时, fetchBotInfo 竞态, handleWebhook 同步阻塞
- bili CDN 直连失败, 添加 HTTP_PROXY 代理
2026-07-18 20:46:58 +08:00

16 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

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 platform)
├── main.go        — Entry point (compiled for non-Windows or non-cgo)
├── plugin.go      — Plugin implementation (Plugin interface)
├── go.mod         — Go module definition
└── README.md      — Documentation

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:

  1. Reads plg.json to determine target platform
  2. Go plugin: Runs go build -buildmode=c-shared (produces .so + C ABI header)
  3. Lua plugin: Packages source code directly, no compilation needed
  4. Generates plugin.json manifest file
  5. Packages as .hmap distribution (zip format, containing plugin.json + plugin.so + plugin.dll + main.lua)

Output in dist/ directory:

dist/
├── myplugin_linux_amd64.hmap      # Go plugin Linux version
├── myplugin_windows_amd64.hmap    # Go plugin Windows version
└── myplugin_lua.hmap              # Lua plugin

Deployment

Install via PluginMgr HTTP API:

# Kernel PluginMgr listens on :9876
curl -X POST http://127.0.0.1:9876/plugins \
  -F "file=@dist/myplugin_linux_amd64.hmap"

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

main.go provides the NewPlugin export function, which is the entry point when the kernel loads the plugin:

//go:build !windows || !cgo

package main

import "gitcode.com/JianFeeeee/homeagent-sdk/sdk"

func NewPlugin(name string, config map[string]interface{}) (sdk.Plugin, error) {
    return NewPluginFactory(name, config)
}

For -buildmode=c-shared, plugindev build auto-generates C ABI bridge code (z_bridge_gen.go + z_entry.c), no manual handling 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",
    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
})

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)

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()

Note

: Subscribe, Publish, RegisterChannel, UnregisterChannel, ListChannels, InjectInput, InjectInputSync, InjectInterrupt, InjectTextSync, InjectTextSyncNoMemory, OutputChan are only available in built-in plugins (internal/sdk package). 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_stage callback currently only receives raw_message, user_id, phase fields. 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

  1. Start() is non-blocking — start long tasks in goroutines, don't block Start
  2. Stop() cleans up resources — close connections, stop goroutines, cancel subscriptions
  3. Unique tool names — use plugin name prefix to avoid conflicts
  4. When handler returns error, LLM will receive it and may retry
  5. Use InjectInterruptText for interrupts, InjectText for normal delivery
  6. Use Settings().Get/Set for config, don't hardcode
  7. External Go plugins compile independently, not tied to kernel version; only built-in plugins need recompilation with kernel

:

6. 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
web Go DuckDuckGo search + web scraping, SSRF protection
webfetch Go Web content fetching
qq Go NapCat OneBot integration, 17 tools
bili Go Bilibili video download (you-get)
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.