docs: 修正全部文档使其与源码实现一致

- Plugin.Start(sdk *PluginSDK) 接口签名改为指针
- 方法表重写: 移除 CallLLM/QueryKnowledge/SetMemory 等不存在方法
- IOInjector 参数顺序修正为 (source, channel, text)
- 删除虚构 SDKConfig, 替换为实际 New() 构造函数签名
- .hmap 内容描述一致化 (plugin.so + plugin.dll + main.lua)
- 添加 meta/ 包元数据文件
This commit is contained in:
root
2026-07-18 20:47:17 +08:00
commit 1762f0c34b
62 changed files with 9454 additions and 0 deletions

7
example/sanitizer/go.mod Normal file
View File

@ -0,0 +1,7 @@
module sanitizer
go 1.25.0
require gitcode.com/JianFeeeee/homeagent-sdk v0.0.0-20260708004841-e9bdcf9304b0
replace gitcode.com/JianFeeeee/homeagent-sdk => ../..

View File

@ -0,0 +1,11 @@
{
"name": "sanitizer",
"name_zh": "输出清洗",
"name_en": "sanitizer",
"version": "0.1.0",
"description": "清洗 LLM 输出中的工具调用残留(思维泄漏)",
"author": "HomeAgent SDK",
"entry": "plugin.so",
"tags": ["sanitizer"],
"targets": "linux/amd64"
}

102
example/sanitizer/plugin.go Normal file
View File

@ -0,0 +1,102 @@
// Package main 是一个外部插件示例(编译为 .so 通过 -buildmode=plugin)。
// 在 StagePostAction 阶段清洗 LLM 输出中的工具调用残留(思维泄漏)。
//
// 编译:
//
// go build -buildmode=plugin -o sanitizer.so .
//
// 安装到 HomeAgent 插件目录(如 plugins/sanitizer/plugin.so),
// HomeAgent 自动通过 tryLoadSO 加载。
package main
import (
"log"
"regexp"
"strings"
"gitcode.com/JianFeeeee/homeagent-sdk/sdk"
)
var (
toolCallTagRE = regexp.MustCompile(`(?s)<tool_call[^>]*>.*?</tool_call>`)
invokeTagRE = regexp.MustCompile(`(?s)<invoke[^>]*>.*?</invoke>`)
toolTagRE = regexp.MustCompile(`(?s)<tool[^>]*>.*?</tool>`)
functionTagRE = regexp.MustCompile(`(?s)<function[^>]*>.*?</function>`)
toolCodeBlockRE = regexp.MustCompile("(?s)```(?:xml|json)?\\s*<tool_call[^>]*>.*?</tool_call>\\s*```")
invokeCodeBlockRE = regexp.MustCompile("(?s)```(?:xml|json)?\\s*<invoke[^>]*>.*?</invoke>\\s*```")
toolCodeBlockRE2 = regexp.MustCompile("(?s)```(?:xml|json)?\\s*<tool[^>]*>.*?</tool>\\s*```")
chineseMarkerRE = regexp.MustCompile(`(?s)【tool_call】.*?【/tool_call】`)
multiNewlineRE = regexp.MustCompile(`\n{3,}`)
toolNameRE = regexp.MustCompile(`^(cmd_run|terminal_create|terminal_write|memory_|knowledge_|doc_|social_|output_send|output_set_channel|llm_|plgreload|spawn_child|child_result|describe_image|transcribe_audio|ocr_image|timer_set|plugin_install|plugin_remove|qq_|a2a_|mcp_|healthcheck|files_|web_)`)
)
type Plugin struct{}
func (p *Plugin) Name() string { return "sanitizer" }
func (p *Plugin) Start(s *sdk.PluginSDK) error {
s.SetAutoRestart(true)
s.RegisterStage(sdk.StagePostAction, func(ctx *sdk.StageContext) error {
ctx.Lock()
before := len(ctx.LLMText)
ctx.LLMText = cleanToolCallLeakage(ctx.LLMText)
after := len(ctx.LLMText)
ctx.Unlock()
if before != after {
log.Printf("[sanitizer] cleaned %d bytes (before=%d after=%d)", before-after, before, after)
}
return nil
})
log.Printf("[sanitizer] stage PostAction registered")
return nil
}
func (p *Plugin) Stop() error { return nil }
func NewPlugin(name string, config map[string]interface{}) (sdk.Plugin, error) {
return &Plugin{}, nil
}
func cleanToolCallLeakage(content string) string {
if content == "" {
return content
}
before := len(content)
content = toolCodeBlockRE.ReplaceAllString(content, "")
content = invokeCodeBlockRE.ReplaceAllString(content, "")
content = toolCodeBlockRE2.ReplaceAllString(content, "")
content = toolCallTagRE.ReplaceAllString(content, "")
content = invokeTagRE.ReplaceAllString(content, "")
content = toolTagRE.ReplaceAllString(content, "")
content = functionTagRE.ReplaceAllString(content, "")
content = chineseMarkerRE.ReplaceAllString(content, "")
lines := strings.Split(content, "\n")
var cleaned []string
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if trimmed == "" {
cleaned = append(cleaned, line)
continue
}
if toolNameRE.MatchString(trimmed) {
if strings.Contains(trimmed, "(") || strings.Contains(trimmed, "\"") || strings.Contains(trimmed, ":") {
continue
}
}
cleaned = append(cleaned, line)
}
content = strings.Join(cleaned, "\n")
content = multiNewlineRE.ReplaceAllString(content, "\n\n")
content = strings.TrimSpace(content)
if len(content) != before {
log.Printf("[sanitizer] cleanToolCallLeakage: %d bytes removed", before-len(content))
}
return content
}

View File

@ -0,0 +1,31 @@
package main
import "testing"
func TestCleanToolCallLeakage(t *testing.T) {
tests := []struct {
name, input, want string
}{
{"empty", "", ""},
{"clean", "你好", "你好"},
{"tool_call", "a<tool_call>x</tool_call>b", "ab"},
{"invoke", "a<invoke>x</invoke>b", "ab"},
{"function", "a<function>x</function>b", "ab"},
{"xml_block", "a\n```xml\n<tool_call>x</tool_call>\n```\nb", "a\n\nb"},
{"json_block", "a\n```json\n<invoke>x</invoke>\n```\nb", "a\n\nb"},
{"bare_code", "```python\nprint(1)\n```", "```python\nprint(1)\n```"},
{"chinese_marker", "a【tool_call】x【/tool_call】b", "ab"},
{"tool_line", "cmd_run(\"ls\")\nok", "ok"},
{"prose_kept", "cmd_run 是一个工具", "cmd_run 是一个工具"},
{"multiline", "a\n<tool_call>\nx\n</tool_call>\nb", "a\n\nb"},
{"whitespace", "a\n\n\n\nb", "a\n\nb"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := cleanToolCallLeakage(tt.input)
if got != tt.want {
t.Errorf("got %q, want %q", got, tt.want)
}
})
}
}