feat: sdk NoMemory/Cleaner + example build fixes

- sdk/plugin.go: ToolDef adds NoMemory/Cleaner fields
- sdk/plugin_test.go: unit tests for NoMemory/Cleaner
- all example plugins: NoMemory/Cleaner annotated for each tool
- plugindev/templates.go: template shows NoMemory/Cleaner pattern
- plugindev/cmd_build.go: fix ensureGoMod, buildBundle/buildTarget sdkPath param, NewPluginFactory
- example/go.mod: add external dependency declarations (chromedp, gofeed)
- add main.go stubs for all example plugins
This commit is contained in:
JianFeeeee
2026-07-25 11:17:21 +08:00
parent 34f91ebd1a
commit cb7999ca71
54 changed files with 682 additions and 475 deletions

View File

@ -95,6 +95,20 @@ Triple 数据结构新增字段:
- `SubjectType` — 主体类型 - `SubjectType` — 主体类型
- `ObjectType` — 客体类型 - `ObjectType` — 客体类型
### ToolDef 字段说明
`RegisterTool` 的 `def` 参数类型为 `sdk.ToolDef`,包含以下字段:
| 字段 | 类型 | 说明 |
|------|------|------|
| `Name` | `string` | 工具名,建议插件名前缀避免冲突 |
| `Description` | `string` | 工具描述,LLM 据此选择调用 |
| `Parameters` | `map[string]interface{}` | JSON Schema 格式参数定义 |
| `NoMemory` | `bool` | 默认为 `false`;设为 `true` 时输出不参与向量/jieba/蒸馏计算(原文保留) |
| `Cleaner` | `func(string) string` | 可选,输出进入计算层前的清洗函数(如 JSON 提取 `.content`) |
`NoMemory` 和 `Cleaner` 的详细设计意图参见核心仓 `docs/zh/PLUGIN_DEV.md`。
### New 构造函数 ### New 构造函数
`New()` 由内核在加载插件时调用,插件开发者无需手动构造 PluginSDK: `New()` 由内核在加载插件时调用,插件开发者无需手动构造 PluginSDK:

View File

@ -95,6 +95,20 @@ The Triple data structure includes additional fields:
- `SubjectType` — subject type - `SubjectType` — subject type
- `ObjectType` — object type - `ObjectType` — object type
### ToolDef Field Reference
The `def` parameter of `RegisterTool` is of type `sdk.ToolDef`, with the following fields:
| Field | Type | Description |
|-------|------|-------------|
| `Name` | `string` | Tool name, use plugin name prefix to avoid conflicts |
| `Description` | `string` | Tool description, LLM uses this for tool selection |
| `Parameters` | `map[string]interface{}` | JSON Schema parameter definition |
| `NoMemory` | `bool` | Default `false`; when `true`, output skips vector/jieba/distill computation (original text preserved) |
| `Cleaner` | `func(string) string` | Optional, filters output before computation layer (e.g., extract `.content` from JSON) |
For detailed design rationale of `NoMemory` and `Cleaner`, see `docs/en/PLUGIN_DEV.md` in the core repository.
### New Constructor ### New Constructor
`New()` is called by the kernel when loading a plugin. Plugin developers do not need to construct PluginSDK manually: `New()` is called by the kernel when loading a plugin. Plugin developers do not need to construct PluginSDK manually:

View File

@ -2,4 +2,6 @@ module a2a
go 1.25.0 go 1.25.0
require gitcode.com/JianFeeeee/homeagent-sdk v0.7.1 require gitcode.com/JianFeeeee/homeagent-sdk v0.0.0
replace gitcode.com/JianFeeeee/homeagent-sdk => E:/program/homeagent/homeagentsdk

View File

@ -3,7 +3,7 @@
package main package main
import ( import (
"gitcode.com/JianFeeeee/homeagent-sdk/sdk" sdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
) )
func NewPlugin(name string, config map[string]interface{}) (sdk.Plugin, error) { func NewPlugin(name string, config map[string]interface{}) (sdk.Plugin, error) {

View File

@ -47,6 +47,13 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
}, },
"required": []string{"agent_url", "query"}, "required": []string{"agent_url", "query"},
}, },
Cleaner: func(output string) string {
var r struct{ Content string }
if json.Unmarshal([]byte(output), &r) == nil && r.Content != "" {
return r.Content
}
return output
},
}, p.handleA2AQuery) }, p.handleA2AQuery)
s.RegisterTool(tp+"a2a_discover", sdk.ToolDef{ s.RegisterTool(tp+"a2a_discover", sdk.ToolDef{
@ -454,6 +461,6 @@ func (p *Plugin) handleStatus(args map[string]interface{}) (interface{}, error)
addrStr, listening, map[bool]string{true: "运行中", false: "已停止"}[serverRunning]), nil addrStr, listening, map[bool]string{true: "运行中", false: "已停止"}[serverRunning]), nil
} }
func NewPlugin(name string, config map[string]interface{}) (sdk.Plugin, error) { func NewPluginFactory(name string, config map[string]interface{}) (sdk.Plugin, error) {
return &Plugin{name: name}, nil return &Plugin{name: name}, nil
} }

View File

@ -4,4 +4,4 @@ go 1.25.0
require gitcode.com/JianFeeeee/homeagent-sdk v0.0.0 require gitcode.com/JianFeeeee/homeagent-sdk v0.0.0
replace gitcode.com/JianFeeeee/homeagent-sdk => /tmp/opencode/sdk-clone replace gitcode.com/JianFeeeee/homeagent-sdk => E:/program/homeagent/homeagentsdk

11
example/ai_image/main.go Normal file
View File

@ -0,0 +1,11 @@
//go:build !windows || !cgo
package main
import (
sdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
)
func NewPlugin(name string, config map[string]interface{}) (sdk.Plugin, error) {
return NewPluginFactory(name, config)
}

View File

@ -23,7 +23,7 @@ type Plugin struct {
size string size string
} }
func NewPlugin(name string, config map[string]interface{}) (sdk.Plugin, error) { func NewPluginFactory(name string, config map[string]interface{}) (sdk.Plugin, error) {
return &Plugin{name: name}, nil return &Plugin{name: name}, nil
} }

View File

@ -4,4 +4,4 @@ go 1.25.0
require gitcode.com/JianFeeeee/homeagent-sdk v0.0.0 require gitcode.com/JianFeeeee/homeagent-sdk v0.0.0
replace gitcode.com/JianFeeeee/homeagent-sdk => ../.. replace gitcode.com/JianFeeeee/homeagent-sdk => E:/program/homeagent/homeagentsdk

11
example/bili/main.go Normal file
View File

@ -0,0 +1,11 @@
//go:build !windows || !cgo
package main
import (
sdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
)
func NewPlugin(name string, config map[string]interface{}) (sdk.Plugin, error) {
return NewPluginFactory(name, config)
}

View File

@ -43,6 +43,13 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
}, },
"required": []string{"url"}, "required": []string{"url"},
}, },
Cleaner: func(output string) string {
var r struct{ Content string }
if json.Unmarshal([]byte(output), &r) == nil && r.Content != "" {
return r.Content
}
return output
},
}, p.handleBiliVideo) }, p.handleBiliVideo)
return nil return nil
} }
@ -229,6 +236,6 @@ func contains(slice []string, s string) bool {
return false return false
} }
func NewPlugin(name string, config map[string]interface{}) (sdk.Plugin, error) { func NewPluginFactory(name string, config map[string]interface{}) (sdk.Plugin, error) {
return &Plugin{name: name}, nil return &Plugin{name: name}, nil
} }

View File

@ -1,101 +0,0 @@
/* Code generated by cmd/cgo; DO NOT EDIT. */
/* package bili */
#line 1 "cgo-builtin-export-prolog"
#include <stddef.h>
#ifndef GO_CGO_EXPORT_PROLOGUE_H
#define GO_CGO_EXPORT_PROLOGUE_H
#ifndef GO_CGO_GOSTRING_TYPEDEF
typedef struct { const char *p; ptrdiff_t n; } _GoString_;
extern size_t _GoStringLen(_GoString_ s);
extern const char *_GoStringPtr(_GoString_ s);
#endif
#endif
/* Start of preamble from import "C" comments. */
#line 3 "z_bridge_gen.go"
#include <stdlib.h>
int ha_dispatch(int method_id, void* core_api, char* s1, char* s2, char* s3, int i1, int i2, char** result, char** error);
#line 1 "cgo-generated-wrapper"
/* End of preamble from import "C" comments. */
/* Start of boilerplate cgo prologue. */
#line 1 "cgo-gcc-export-header-prolog"
#ifndef GO_CGO_PROLOGUE_H
#define GO_CGO_PROLOGUE_H
typedef signed char GoInt8;
typedef unsigned char GoUint8;
typedef short GoInt16;
typedef unsigned short GoUint16;
typedef int GoInt32;
typedef unsigned int GoUint32;
typedef long long GoInt64;
typedef unsigned long long GoUint64;
typedef GoInt64 GoInt;
typedef GoUint64 GoUint;
typedef size_t GoUintptr;
typedef float GoFloat32;
typedef double GoFloat64;
#ifdef _MSC_VER
#if !defined(__cplusplus) || _MSVC_LANG <= 201402L
#include <complex.h>
typedef _Fcomplex GoComplex64;
typedef _Dcomplex GoComplex128;
#else
#include <complex>
typedef std::complex<float> GoComplex64;
typedef std::complex<double> GoComplex128;
#endif
#else
typedef float _Complex GoComplex64;
typedef double _Complex GoComplex128;
#endif
/*
static assertion to make sure the file is being used on architecture
at least with matching size of GoInt.
*/
typedef char _check_for_64_bit_pointer_matching_GoInt[sizeof(void*)==64/8 ? 1:-1];
#ifndef GO_CGO_GOSTRING_TYPEDEF
typedef _GoString_ GoString;
#endif
typedef void *GoMap;
typedef void *GoChan;
typedef struct { void *t; void *v; } GoInterface;
typedef struct { void *data; GoInt len; GoInt cap; } GoSlice;
#endif
/* End of boilerplate cgo prologue. */
#ifdef __cplusplus
extern "C" {
#endif
extern int go_init_plugin(char* name, char* configJSON, char** errorOut);
extern int go_start_plugin(void* coreAPIptr, int coreVersion, char** errorOut);
extern int go_stop_plugin(char** errorOut);
extern int go_invoke_tool(char* name, char* argsJSON, char** resultOut, char** errorOut);
extern int go_invoke_stage(char* stage, char* ctxJSON, char** errorOut);
extern int go_invoke_output(char* channel, char* msgType, char* payloadJSON, char** errorOut);
extern void go_free_string(char* ptr);
#ifdef __cplusplus
}
#endif

View File

@ -1,21 +1,26 @@
module browser-plugin module browser
go 1.25.0 go 1.25.0
require ( require (
gitcode.com/JianFeeeee/homeagent-sdk v0.0.0 gitcode.com/JianFeeeee/homeagent-sdk v0.0.0
github.com/chromedp/chromedp v0.9.5 github.com/chromedp/chromedp v0.9.5
github.com/chromedp/cdproto v0.0.0-20240202021202-6d0b6a386732
github.com/chromedp/sysutil v1.0.0
github.com/gobwas/httphead v0.1.0
github.com/gobwas/pool v0.2.1
github.com/gobwas/ws v1.3.2
github.com/josharian/intern v1.0.0
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80
github.com/mailru/easyjson v0.7.7
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde
golang.org/x/sys v0.16.0
) )
require ( replace gitcode.com/JianFeeeee/homeagent-sdk => E:/program/homeagent/homeagentsdk
github.com/chromedp/cdproto v0.0.0-20240202021202-6d0b6a386732 // indirect
github.com/chromedp/sysutil v1.0.0 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.3.2 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
golang.org/x/sys v0.16.0 // indirect
)
replace gitcode.com/JianFeeeee/homeagent-sdk => /tmp/opencode/sdk-clone replace gitcode.com/JianFeeeee/homeagent-sdk => E:/program/homeagent/homeagentsdk
replace gitcode.com/JianFeeeee/homeagent-sdk => E:/program/homeagent/homeagentsdk
replace gitcode.com/JianFeeeee/homeagent-sdk => E:/program/homeagent/homeagentsdk

11
example/browser/main.go Normal file
View File

@ -0,0 +1,11 @@
//go:build !windows || !cgo
package main
import (
sdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
)
func NewPlugin(name string, config map[string]interface{}) (sdk.Plugin, error) {
return NewPluginFactory(name, config)
}

View File

@ -51,7 +51,7 @@ type BrowserSession struct {
currentURL string currentURL string
} }
func NewPlugin(name string, config map[string]interface{}) (sdk.Plugin, error) { func NewPluginFactory(name string, config map[string]interface{}) (sdk.Plugin, error) {
return &Plugin{name: name, stopCh: make(chan struct{}), sessions: make(map[string]*BrowserSession)}, nil return &Plugin{name: name, stopCh: make(chan struct{}), sessions: make(map[string]*BrowserSession)}, nil
} }
@ -188,6 +188,14 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
tp := p.name + "_" tp := p.name + "_"
cleaner := func(output string) string {
var r struct{ Content string }
if json.Unmarshal([]byte(output), &r) == nil && r.Content != "" {
return r.Content
}
return output
}
s.RegisterTool(tp+"search", sdk.ToolDef{ s.RegisterTool(tp+"search", sdk.ToolDef{
Name: tp + "search", Name: tp + "search",
Description: "使用 Bing 搜索网页。返回标题、URL 和摘要。", Description: "使用 Bing 搜索网页。返回标题、URL 和摘要。",
@ -199,6 +207,7 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
}, },
"required": []string{"query"}, "required": []string{"query"},
}, },
Cleaner: cleaner,
}, p.handleSearch) }, p.handleSearch)
s.RegisterTool(tp+"fetch", sdk.ToolDef{ s.RegisterTool(tp+"fetch", sdk.ToolDef{
@ -213,6 +222,7 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
}, },
"required": []string{"url"}, "required": []string{"url"},
}, },
Cleaner: cleaner,
}, p.handleFetch) }, p.handleFetch)
s.RegisterTool(tp+"render", sdk.ToolDef{ s.RegisterTool(tp+"render", sdk.ToolDef{
@ -226,6 +236,7 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
}, },
"required": []string{"url"}, "required": []string{"url"},
}, },
Cleaner: cleaner,
}, p.handleRender) }, p.handleRender)
s.RegisterTool(tp+"start", sdk.ToolDef{ s.RegisterTool(tp+"start", sdk.ToolDef{

View File

@ -4,4 +4,4 @@ go 1.25.0
require gitcode.com/JianFeeeee/homeagent-sdk v0.0.0 require gitcode.com/JianFeeeee/homeagent-sdk v0.0.0
replace gitcode.com/JianFeeeee/homeagent-sdk => /tmp/opencode/sdk-clone replace gitcode.com/JianFeeeee/homeagent-sdk => E:/program/homeagent/homeagentsdk

11
example/calendar/main.go Normal file
View File

@ -0,0 +1,11 @@
//go:build !windows || !cgo
package main
import (
sdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
)
func NewPlugin(name string, config map[string]interface{}) (sdk.Plugin, error) {
return NewPluginFactory(name, config)
}

View File

@ -54,7 +54,7 @@ type Plugin struct {
remindTicker *time.Ticker remindTicker *time.Ticker
} }
func NewPlugin(name string, config map[string]interface{}) (sdk.Plugin, error) { func NewPluginFactory(name string, config map[string]interface{}) (sdk.Plugin, error) {
return &Plugin{name: name, stopCh: make(chan struct{})}, nil return &Plugin{name: name, stopCh: make(chan struct{})}, nil
} }

View File

@ -4,4 +4,4 @@ go 1.25.0
require gitcode.com/JianFeeeee/homeagent-sdk v0.0.0 require gitcode.com/JianFeeeee/homeagent-sdk v0.0.0
replace gitcode.com/JianFeeeee/homeagent-sdk => ../.. replace gitcode.com/JianFeeeee/homeagent-sdk => E:/program/homeagent/homeagentsdk

11
example/editdoc/main.go Normal file
View File

@ -0,0 +1,11 @@
//go:build !windows || !cgo
package main
import (
sdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
)
func NewPlugin(name string, config map[string]interface{}) (sdk.Plugin, error) {
return NewPluginFactory(name, config)
}

View File

@ -23,6 +23,7 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
s.RegisterTool("edit_document", sdk.ToolDef{ s.RegisterTool("edit_document", sdk.ToolDef{
Name: "edit_document", Name: "edit_document",
Description: "编辑 Office 文档内容。支持替换文本、修改单元格等操作。编辑后原文件被覆盖。操作前建议先用 read_document 查看内容。支持 .docx / .xlsx / .pptx。", Description: "编辑 Office 文档内容。支持替换文本、修改单元格等操作。编辑后原文件被覆盖。操作前建议先用 read_document 查看内容。支持 .docx / .xlsx / .pptx。",
NoMemory: true,
Parameters: map[string]interface{}{ Parameters: map[string]interface{}{
"type": "object", "type": "object",
"properties": map[string]interface{}{ "properties": map[string]interface{}{
@ -124,6 +125,6 @@ func (p *Plugin) handleEditDocument(args map[string]interface{}) (interface{}, e
} }
func NewPlugin(name string, config map[string]interface{}) (sdk.Plugin, error) { func NewPluginFactory(name string, config map[string]interface{}) (sdk.Plugin, error) {
return &Plugin{name: name}, nil return &Plugin{name: name}, nil
} }

View File

@ -1,6 +1,6 @@
module testplugin module files
go 1.21 go 1.25.0
require gitcode.com/JianFeeeee/homeagent-sdk v0.0.0 require gitcode.com/JianFeeeee/homeagent-sdk v0.0.0

11
example/files/main.go Normal file
View File

@ -0,0 +1,11 @@
//go:build !windows || !cgo
package main
import (
sdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
)
func NewPlugin(name string, config map[string]interface{}) (sdk.Plugin, error) {
return NewPluginFactory(name, config)
}

View File

@ -1,6 +1,7 @@
package main package main
import ( import (
"encoding/json"
"fmt" "fmt"
"log" "log"
"os" "os"
@ -59,6 +60,14 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
}, },
"required": []string{"path"}, "required": []string{"path"},
}, },
NoMemory: false,
Cleaner: func(output string) string {
var r struct{ Content string }
if json.Unmarshal([]byte(output), &r) == nil && r.Content != "" {
return r.Content
}
return output
},
}, p.handleRead) }, p.handleRead)
s.RegisterTool(tp+"write", sdk.ToolDef{ s.RegisterTool(tp+"write", sdk.ToolDef{
@ -478,6 +487,6 @@ func getSetting[T any](s sdk.SettingsAPI, key string, def T) T {
return val return val
} }
func NewPlugin(name string, config map[string]interface{}) (sdk.Plugin, error) { func NewPluginFactory(name string, config map[string]interface{}) (sdk.Plugin, error) {
return &Plugin{name: name}, nil return &Plugin{name: name}, nil
} }

View File

@ -4,4 +4,4 @@ go 1.25.0
require gitcode.com/JianFeeeee/homeagent-sdk v0.0.0 require gitcode.com/JianFeeeee/homeagent-sdk v0.0.0
replace gitcode.com/JianFeeeee/homeagent-sdk => ../.. replace gitcode.com/JianFeeeee/homeagent-sdk => E:/program/homeagent/homeagentsdk

11
example/memo/main.go Normal file
View File

@ -0,0 +1,11 @@
//go:build !windows || !cgo
package main
import (
sdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
)
func NewPlugin(name string, config map[string]interface{}) (sdk.Plugin, error) {
return NewPluginFactory(name, config)
}

View File

@ -269,6 +269,6 @@ func errorResult(msg string) map[string]interface{} {
} }
} }
func NewPlugin(name string, config map[string]interface{}) (sdk.Plugin, error) { func NewPluginFactory(name string, config map[string]interface{}) (sdk.Plugin, error) {
return &Plugin{name: name}, nil return &Plugin{name: name}, nil
} }

View File

@ -2,6 +2,6 @@ module music
go 1.25.0 go 1.25.0
require gitcode.com/JianFeeeee/homeagent-sdk v0.7.1 require gitcode.com/JianFeeeee/homeagent-sdk v0.0.0
replace gitcode.com/JianFeeeee/homeagent-sdk => /tmp/opencode/sdk-clone replace gitcode.com/JianFeeeee/homeagent-sdk => E:/program/homeagent/homeagentsdk

11
example/music/main.go Normal file
View File

@ -0,0 +1,11 @@
//go:build !windows || !cgo
package main
import (
sdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
)
func NewPlugin(name string, config map[string]interface{}) (sdk.Plugin, error) {
return NewPluginFactory(name, config)
}

View File

@ -56,7 +56,7 @@ type lyricData struct {
Lyric string `json:"lyric"` Lyric string `json:"lyric"`
} }
func NewPlugin(name string, config map[string]interface{}) (sdk.Plugin, error) { func NewPluginFactory(name string, config map[string]interface{}) (sdk.Plugin, error) {
return &Plugin{name: name}, nil return &Plugin{name: name}, nil
} }
@ -84,6 +84,13 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
}, },
"required": []string{"keyword"}, "required": []string{"keyword"},
}, },
Cleaner: func(output string) string {
var r struct{ Content string }
if json.Unmarshal([]byte(output), &r) == nil && r.Content != "" {
return r.Content
}
return output
},
}, p.handleSearch) }, p.handleSearch)
s.RegisterTool(p.name+"_lyrics", sdk.ToolDef{ s.RegisterTool(p.name+"_lyrics", sdk.ToolDef{

View File

@ -2,6 +2,6 @@ module ocr
go 1.25.0 go 1.25.0
require gitcode.com/JianFeeeee/homeagent-sdk v0.0.0-20260708004841-e9bdcf9304b0 require gitcode.com/JianFeeeee/homeagent-sdk v0.0.0
replace gitcode.com/JianFeeeee/homeagent-sdk => ../.. replace gitcode.com/JianFeeeee/homeagent-sdk => E:/program/homeagent/homeagentsdk

11
example/ocr/main.go Normal file
View File

@ -0,0 +1,11 @@
//go:build !windows || !cgo
package main
import (
sdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
)
func NewPlugin(name string, config map[string]interface{}) (sdk.Plugin, error) {
return NewPluginFactory(name, config)
}

View File

@ -2,6 +2,7 @@ package main
import ( import (
"encoding/base64" "encoding/base64"
"encoding/json"
"fmt" "fmt"
"io" "io"
"log" "log"
@ -38,6 +39,17 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
"language": map[string]interface{}{"type": "string", "description": "识别语言,默认 chi_sim+eng(中文简体+英文),可选 chi_sim / eng / chi_sim+eng"}, "language": map[string]interface{}{"type": "string", "description": "识别语言,默认 chi_sim+eng(中文简体+英文),可选 chi_sim / eng / chi_sim+eng"},
}, },
}, },
Cleaner: func(output string) string {
var r struct{ Text string }
if json.Unmarshal([]byte(output), &r) == nil && r.Text != "" {
return r.Text
}
var r2 struct{ Content string }
if json.Unmarshal([]byte(output), &r2) == nil && r2.Content != "" {
return r2.Content
}
return output
},
}, p.handleOcrImage) }, p.handleOcrImage)
log.Printf("[%s] plugin started", p.name) log.Printf("[%s] plugin started", p.name)
@ -128,7 +140,6 @@ func (p *Plugin) handleOcrImage(args map[string]interface{}) (interface{}, error
}, nil }, nil
} }
func NewPluginFactory(name string, config map[string]interface{}) (sdk.Plugin, error) {
func NewPlugin(name string, config map[string]interface{}) (sdk.Plugin, error) {
return &Plugin{name: name}, nil return &Plugin{name: name}, nil
} }

View File

@ -2,6 +2,6 @@ module qq
go 1.25.0 go 1.25.0
require gitcode.com/JianFeeeee/homeagent-sdk v0.0.0-20260708004841-e9bdcf9304b0 require gitcode.com/JianFeeeee/homeagent-sdk v0.0.0
replace gitcode.com/JianFeeeee/homeagent-sdk => ../.. replace gitcode.com/JianFeeeee/homeagent-sdk => E:/program/homeagent/homeagentsdk

11
example/qq/main.go Normal file
View File

@ -0,0 +1,11 @@
//go:build !windows || !cgo
package main
import (
sdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
)
func NewPlugin(name string, config map[string]interface{}) (sdk.Plugin, error) {
return NewPluginFactory(name, config)
}

View File

@ -167,155 +167,242 @@ meta JSON 格式:
type 枚举: text(文字)/ voice(语音转文字后发送)/ image(图片URL)/ file(文件URL)`, type 枚举: text(文字)/ voice(语音转文字后发送)/ image(图片URL)/ file(文件URL)`,
p.handleChannelOutput) p.handleChannelOutput)
// 查询类工具输出清洗器:提取 JSON 中的 content/文本字段参与向量化
cleaner := func(output string) string {
var r struct{ Content string }
if json.Unmarshal([]byte(output), &r) == nil && r.Content != "" {
return r.Content
}
return output
}
// ---- 消息 ---- // ---- 消息 ----
p.regTool(s, tp+"get_message", botInfo+"通过 message_id 从 NapCat 实时获取消息正文、发送者、附件等信息。message_id 从中断消息的 message_id=N 获取,或从 reply_to 的 message_id 获取。", map[string]interface{}{ p.regTool(s, sdk.ToolDef{
"type": "object", "properties": map[string]interface{}{ Name: tp + "get_message", Description: botInfo + "通过 message_id 从 NapCat 实时获取消息正文、发送者、附件等信息。message_id 从中断消息的 message_id=N 获取,或从 reply_to 的 message_id 获取。",
"message_id": map[string]interface{}{"type": "integer", "description": "NapCat消息ID(从中断消息的 message_id=N 或 reply_to.message_id 获取)"}, NoMemory: false,
}, "required": []string{"message_id"}, Cleaner: cleaner,
Parameters: map[string]interface{}{
"type": "object", "properties": map[string]interface{}{
"message_id": map[string]interface{}{"type": "integer", "description": "NapCat消息ID(从中断消息的 message_id=N 或 reply_to.message_id 获取)"},
}, "required": []string{"message_id"},
},
}, p.handleGetMessage) }, p.handleGetMessage)
p.regTool(s, tp+"send_file", "发送文件/图片到QQ(私聊或群聊)。文件先复制到remote目录供NapCat容器访问。", map[string]interface{}{ p.regTool(s, sdk.ToolDef{
"type": "object", "properties": map[string]interface{}{ Name: tp + "send_file", Description: "发送文件/图片到QQ(私聊或群聊)。文件先复制到remote目录供NapCat容器访问。",
"group_id": map[string]interface{}{"type": "integer", "description": "目标群号(与user_id二选一)"}, Parameters: map[string]interface{}{
"user_id": map[string]interface{}{"type": "integer", "description": "目标QQ号(与group_id二选一)"}, "type": "object", "properties": map[string]interface{}{
"file": map[string]interface{}{"type": "string", "description": "本地文件路径"}, "group_id": map[string]interface{}{"type": "integer", "description": "目标群号(与user_id二选一)"},
"name": map[string]interface{}{"type": "string", "description": "文件名(可选,默认取原文件名)"}, "user_id": map[string]interface{}{"type": "integer", "description": "目标QQ号(与group_id二选一)"},
"as_image": map[string]interface{}{"type": "boolean", "description": "作为图片发送(true)还是作为文件(false,默认)"}, "file": map[string]interface{}{"type": "string", "description": "本地文件路径"},
"name": map[string]interface{}{"type": "string", "description": "文件名(可选,默认取原文件名)"},
"as_image": map[string]interface{}{"type": "boolean", "description": "作为图片发送(true)还是作为文件(false,默认)"},
},
}, },
NoMemory: true,
}, p.handleSendFile) }, p.handleSendFile)
p.regTool(s, tp+"get_history", "获取QQ群聊/私聊最近历史消息。当收到引用回复消息或需要了解对话上下文时应优先调用此工具查看前后文。返回值每条格式为 [时间] 发送者: 消息内容。如果消息包含文件,会额外返回 files 字段(含 file_id 和 name),可用 qq_download_file 工具下载。", map[string]interface{}{ p.regTool(s, sdk.ToolDef{
"type": "object", "properties": map[string]interface{}{ Name: tp + "get_history", Description: "获取QQ群聊/私聊最近历史消息。当收到引用回复消息或需要了解对话上下文时应优先调用此工具查看前后文。返回值每条格式为 [时间] 发送者: 消息内容。如果消息包含文件,会额外返回 files 字段(含 file_id 和 name),可用 qq_download_file 工具下载。",
"group_id": map[string]interface{}{"type": "integer", "description": "群号(与user_id二选一)"}, NoMemory: false,
"user_id": map[string]interface{}{"type": "integer", "description": "QQ号私聊历史(与group_id二选一)"}, Cleaner: cleaner,
"count": map[string]interface{}{"type": "integer", "description": "拉取条数,默认10"}, Parameters: map[string]interface{}{
}, "required": []string{}, "type": "object", "properties": map[string]interface{}{
"group_id": map[string]interface{}{"type": "integer", "description": "群号(与user_id二选一)"},
"user_id": map[string]interface{}{"type": "integer", "description": "QQ号私聊历史(与group_id二选一)"},
"count": map[string]interface{}{"type": "integer", "description": "拉取条数,默认10"},
}, "required": []string{},
},
}, p.handleGetHistory) }, p.handleGetHistory)
// ---- 查询 ---- // ---- 查询 ----
p.regTool(s, tp+"get_groups", "获取QQ群列表,可按关键词搜索群名", map[string]interface{}{ p.regTool(s, sdk.ToolDef{
"type": "object", "properties": map[string]interface{}{ Name: tp + "get_groups", Description: "获取QQ群列表,可按关键词搜索群名",
"keyword": map[string]interface{}{"type": "string", "description": "搜索关键词(可选)"}, NoMemory: false,
Parameters: map[string]interface{}{
"type": "object", "properties": map[string]interface{}{
"keyword": map[string]interface{}{"type": "string", "description": "搜索关键词(可选)"},
},
}, },
}, p.handleGetGroups) }, p.handleGetGroups)
p.regTool(s, tp+"get_friends", "获取QQ好友列表,可按昵称/备注关键词搜索", map[string]interface{}{ p.regTool(s, sdk.ToolDef{
"type": "object", "properties": map[string]interface{}{ Name: tp + "get_friends", Description: "获取QQ好友列表,可按昵称/备注关键词搜索",
"keyword": map[string]interface{}{"type": "string", "description": "搜索关键词(可选)"}, NoMemory: false,
Parameters: map[string]interface{}{
"type": "object", "properties": map[string]interface{}{
"keyword": map[string]interface{}{"type": "string", "description": "搜索关键词(可选)"},
},
}, },
}, p.handleGetFriends) }, p.handleGetFriends)
p.regTool(s, tp+"get_recent_contacts", "查看最近有消息的联系人和群聊,返回最近消息概览(含消息数、最后一条消息内容)。可用于发现有谁发过消息但未处理。", map[string]interface{}{ p.regTool(s, sdk.ToolDef{
"type": "object", "properties": map[string]interface{}{ Name: tp + "get_recent_contacts", Description: "查看最近有消息的联系人和群聊,返回最近消息概览(含消息数、最后一条消息内容)。可用于发现有谁发过消息但未处理。",
"count": map[string]interface{}{"type": "integer", "description": "获取数量,默认10"}, NoMemory: false,
Parameters: map[string]interface{}{
"type": "object", "properties": map[string]interface{}{
"count": map[string]interface{}{"type": "integer", "description": "获取数量,默认10"},
},
}, },
}, p.handleGetRecentContacts) }, p.handleGetRecentContacts)
p.regTool(s, tp+"resolve_name", "将QQ号或群号解析为可读的用户昵称或群名称", map[string]interface{}{ p.regTool(s, sdk.ToolDef{
"type": "object", "properties": map[string]interface{}{ Name: tp + "resolve_name", Description: "将QQ号或群号解析为可读的用户昵称或群名称",
"user_id": map[string]interface{}{"type": "integer", "description": "QQ号(与group_id二选一)"}, NoMemory: false,
"group_id": map[string]interface{}{"type": "integer", "description": "群号(与user_id二选一)"}, Parameters: map[string]interface{}{
"type": "object", "properties": map[string]interface{}{
"user_id": map[string]interface{}{"type": "integer", "description": "QQ号(与group_id二选一)"},
"group_id": map[string]interface{}{"type": "integer", "description": "群号(与user_id二选一)"},
},
}, },
}, p.handleResolveName) }, p.handleResolveName)
p.regTool(s, tp+"resolve_nickname", "按昵称/备注/群名片搜索QQ用户,返回匹配的QQ号和详细信息。支持搜索好友列表或指定群成员。", map[string]interface{}{ p.regTool(s, sdk.ToolDef{
"type": "object", "properties": map[string]interface{}{ Name: tp + "resolve_nickname", Description: "按昵称/备注/群名片搜索QQ用户,返回匹配的QQ号和详细信息。支持搜索好友列表或指定群成员。",
"keyword": map[string]interface{}{"type": "string", "description": "搜索关键词(昵称/备注/群名片)"}, NoMemory: false,
"group_id": map[string]interface{}{"type": "integer", "description": "所在群号(可选),不传则搜索好友列表"}, Parameters: map[string]interface{}{
}, "required": []string{"keyword"}, "type": "object", "properties": map[string]interface{}{
"keyword": map[string]interface{}{"type": "string", "description": "搜索关键词(昵称/备注/群名片)"},
"group_id": map[string]interface{}{"type": "integer", "description": "所在群号(可选),不传则搜索好友列表"},
}, "required": []string{"keyword"},
},
}, p.handleResolveNickname) }, p.handleResolveNickname)
p.regTool(s, tp+"get_group_member_info", "获取QQ群成员详细信息", map[string]interface{}{ p.regTool(s, sdk.ToolDef{
"type": "object", "properties": map[string]interface{}{ Name: tp + "get_group_member_info", Description: "获取QQ群成员详细信息",
"group_id": map[string]interface{}{"type": "integer", "description": "群号"}, NoMemory: false,
"user_id": map[string]interface{}{"type": "integer", "description": "QQ号"}, Parameters: map[string]interface{}{
}, "required": []string{"group_id", "user_id"}, "type": "object", "properties": map[string]interface{}{
"group_id": map[string]interface{}{"type": "integer", "description": "群号"},
"user_id": map[string]interface{}{"type": "integer", "description": "QQ号"},
}, "required": []string{"group_id", "user_id"},
},
}, p.handleGetGroupMemberInfo) }, p.handleGetGroupMemberInfo)
// ---- 群管理 ---- // ---- 群管理 ----
p.regTool(s, tp+"group_manage", "QQ群综合管理。通过command参数执行各种操作:leave退群, kick踢人, ban禁言, unban解禁, rename改名, mute-all全员禁言, set-card设名片, set-admin设管理, set-title设头衔, member-list成员列表, group-info群详情, member-info成员详情, at-all-remain@全体剩余, msg-history消息历史, recall撤回, pin-msg精华, list-files文件列表, pending-requests待处理请求, folder-create创建文件夹。注意:leave/kick/ban/unban/mute-all/set-admin等破坏性操作必须先请示管理员确认后再执行。", map[string]interface{}{ p.regTool(s, sdk.ToolDef{
"type": "object", "properties": map[string]interface{}{ Name: tp + "group_manage", Description: "QQ群综合管理。通过command参数执行各种操作:leave退群, kick踢人, ban禁言, unban解禁, rename改名, mute-all全员禁言, set-card设名片, set-admin设管理, set-title设头衔, member-list成员列表, group-info群详情, member-info成员详情, at-all-remain@全体剩余, msg-history消息历史, recall撤回, pin-msg精华, list-files文件列表, pending-requests待处理请求, folder-create创建文件夹。注意:leave/kick/ban/unban/mute-all/set-admin等破坏性操作必须先请示管理员确认后再执行。",
"command": map[string]interface{}{"type": "string", "description": "操作命令"}, Parameters: map[string]interface{}{
"group_id": map[string]interface{}{"type": "integer", "description": "群号"}, "type": "object", "properties": map[string]interface{}{
"user_id": map[string]interface{}{"type": "integer", "description": "QQ号(踢人/禁言/设名片等需要)"}, "command": map[string]interface{}{"type": "string", "description": "操作命令"},
"message_id": map[string]interface{}{"type": "integer", "description": "消息ID(撤回/精华)"}, "group_id": map[string]interface{}{"type": "integer", "description": "群号"},
"name": map[string]interface{}{"type": "string", "description": "群名称(rename)或文件夹名(folder-create)"}, "user_id": map[string]interface{}{"type": "integer", "description": "QQ号(踢人/禁言/设名片等需要)"},
"card": map[string]interface{}{"type": "string", "description": "群名片(set-card)"}, "message_id": map[string]interface{}{"type": "integer", "description": "消息ID(撤回/精华)"},
"title": map[string]interface{}{"type": "string", "description": "群头衔(set-title)"}, "name": map[string]interface{}{"type": "string", "description": "群名称(rename)或文件夹名(folder-create)"},
"enable": map[string]interface{}{"type": "boolean", "description": "启用/禁用(set-admin/mute-all)"}, "card": map[string]interface{}{"type": "string", "description": "群名片(set-card)"},
"minutes": map[string]interface{}{"type": "integer", "description": "禁言分钟数(ban),0=解禁"}, "title": map[string]interface{}{"type": "string", "description": "群头衔(set-title)"},
"count": map[string]interface{}{"type": "integer", "description": "消息条数(msg-history),默认10"}, "enable": map[string]interface{}{"type": "boolean", "description": "启用/禁用(set-admin/mute-all)"},
"folder_id": map[string]interface{}{"type": "string", "description": "文件夹ID(list-files)"}, "minutes": map[string]interface{}{"type": "integer", "description": "禁言分钟数(ban),0=解禁"},
"reject_add": map[string]interface{}{"type": "boolean", "description": "踢出时拒绝加群(kick)"}, "count": map[string]interface{}{"type": "integer", "description": "消息条数(msg-history),默认10"},
"confirm": map[string]interface{}{"type": "boolean", "description": "高风险操作确认标记。执行 leave/kick/ban/unban/rename/mute-all/set-card/set-admin/set-title/recall/pin-msg/folder-create 时必须传 true"}, "folder_id": map[string]interface{}{"type": "string", "description": "文件夹ID(list-files)"},
"reject_add": map[string]interface{}{"type": "boolean", "description": "踢出时拒绝加群(kick)"},
"confirm": map[string]interface{}{"type": "boolean", "description": "高风险操作确认标记。执行 leave/kick/ban/unban/rename/mute-all/set-card/set-admin/set-title/recall/pin-msg/folder-create 时必须传 true"},
},
}, },
NoMemory: true,
}, p.handleGroupManage) }, p.handleGroupManage)
p.regTool(s, tp+"friend_action", "QQ好友管理:delete删除好友, block拉黑(删好友+从所有群踢出+拒绝加群), approve-friend同意好友请求, reject-friend拒绝好友请求, list-friends列出好友。注意:涉及删除/拉黑的操作必须请示管理员确认后再执行,未经授权不可操作。", map[string]interface{}{ p.regTool(s, sdk.ToolDef{
"type": "object", "properties": map[string]interface{}{ Name: tp + "friend_action", Description: "QQ好友管理:delete删除好友, block拉黑(删好友+从所有群踢出+拒绝加群), approve-friend同意好友请求, reject-friend拒绝好友请求, list-friends列出好友。注意:涉及删除/拉黑的操作必须请示管理员确认后再执行,未经授权不可操作。",
"command": map[string]interface{}{"type": "string", "description": "操作: delete|block|approve-friend|reject-friend|list-friends"}, Parameters: map[string]interface{}{
"user_id": map[string]interface{}{"type": "integer", "description": "目标QQ号"}, "type": "object", "properties": map[string]interface{}{
"flag": map[string]interface{}{"type": "string", "description": "好友请求flag(approve-friend/reject-friend需要)"}, "command": map[string]interface{}{"type": "string", "description": "操作: delete|block|approve-friend|reject-friend|list-friends"},
"remark": map[string]interface{}{"type": "string", "description": "好友备注(approve-friend可选)"}, "user_id": map[string]interface{}{"type": "integer", "description": "目标QQ号"},
"group_id": map[string]interface{}{"type": "integer", "description": "仅从指定群踢出(block配合)"}, "flag": map[string]interface{}{"type": "string", "description": "好友请求flag(approve-friend/reject-friend需要)"},
"confirm": map[string]interface{}{"type": "boolean", "description": "高风险操作确认标记。执行 delete/block/approve-friend/reject-friend 时必须传 true"}, "remark": map[string]interface{}{"type": "string", "description": "好友备注(approve-friend可选)"},
"group_id": map[string]interface{}{"type": "integer", "description": "仅从指定群踢出(block配合)"},
"confirm": map[string]interface{}{"type": "boolean", "description": "高风险操作确认标记。执行 delete/block/approve-friend/reject-friend 时必须传 true"},
},
}, },
NoMemory: true,
}, p.handleFriendAction) }, p.handleFriendAction)
// ---- 文件 ---- // ---- 文件 ----
p.regTool(s, tp+"get_group_files", "查询群文件列表、搜索文件、下载文件到本地。操作: list列出, search搜索, download下载", map[string]interface{}{ p.regTool(s, sdk.ToolDef{
"type": "object", "properties": map[string]interface{}{ Name: tp + "get_group_files", Description: "查询群文件列表、搜索文件、下载文件到本地。操作: list列出, search搜索, download下载",
"group_id": map[string]interface{}{"type": "integer", "description": "群号"}, NoMemory: false,
"command": map[string]interface{}{"type": "string", "description": "操作: list|search|download"}, Cleaner: cleaner,
"folder_id": map[string]interface{}{"type": "string", "description": "文件夹ID(list指定文件夹)"}, Parameters: map[string]interface{}{
"keyword": map[string]interface{}{"type": "string", "description": "搜索关键词(search)"}, "type": "object", "properties": map[string]interface{}{
"file_id": map[string]interface{}{"type": "string", "description": "文件ID(download)"}, "group_id": map[string]interface{}{"type": "integer", "description": "群号"},
"filename": map[string]interface{}{"type": "string", "description": "保存文件名(download可选)"}, "command": map[string]interface{}{"type": "string", "description": "操作: list|search|download"},
"folder_id": map[string]interface{}{"type": "string", "description": "文件夹ID(list指定文件夹)"},
"keyword": map[string]interface{}{"type": "string", "description": "搜索关键词(search)"},
"file_id": map[string]interface{}{"type": "string", "description": "文件ID(download)"},
"filename": map[string]interface{}{"type": "string", "description": "保存文件名(download可选)"},
},
}, },
}, p.handleGetGroupFiles) }, p.handleGetGroupFiles)
p.regTool(s, tp+"download_file", "从聊天记录下载文件到本地。file_id 从 qq_get_message/qq_get_history 的 files 字段获取。群文件建议提供 group_id,私聊文件建议提供 user_id 以提高成功率。异步下载,完成后推送通知。", map[string]interface{}{ p.regTool(s, sdk.ToolDef{
"type": "object", "properties": map[string]interface{}{ Name: tp + "download_file", Description: "从聊天记录下载文件到本地。file_id 从 qq_get_message/qq_get_history 的 files 字段获取。群文件建议提供 group_id,私聊文件建议提供 user_id 以提高成功率。异步下载,完成后推送通知。",
"file_id": map[string]interface{}{"type": "string", "description": "文件 ID(从 qq_get_message 或 qq_get_history 的 files 字段获取)"}, Parameters: map[string]interface{}{
"url": map[string]interface{}{"type": "string", "description": "文件下载 URL(可选,qq_get_message 返回的 url 字段)"}, "type": "object", "properties": map[string]interface{}{
"group_id": map[string]interface{}{"type": "integer", "description": "群号(可选,群文件下载)"}, "file_id": map[string]interface{}{"type": "string", "description": "文件 ID(从 qq_get_message 或 qq_get_history 的 files 字段获取)"},
"user_id": map[string]interface{}{"type": "integer", "description": "私聊对象QQ号(可选,私聊文件下载)"}, "url": map[string]interface{}{"type": "string", "description": "文件下载 URL(可选,qq_get_message 返回的 url 字段)"},
"filename": map[string]interface{}{"type": "string", "description": "保存文件名(可选,默认用原文件名)"}, "group_id": map[string]interface{}{"type": "integer", "description": "群号(可选,群文件下载)"},
}, "required": []string{"file_id"}, "user_id": map[string]interface{}{"type": "integer", "description": "私聊对象QQ号(可选,私聊文件下载)"},
"filename": map[string]interface{}{"type": "string", "description": "保存文件名(可选,默认用原文件名)"},
}, "required": []string{"file_id"},
},
NoMemory: true,
}, p.handleDownloadFile) }, p.handleDownloadFile)
p.regTool(s, tp+"get_download_tasks", "查看所有下载任务及状态(running/done/failed),包含文件名、保存路径、错误信息等", map[string]interface{}{ p.regTool(s, sdk.ToolDef{
"type": "object", "properties": map[string]interface{}{}, Name: tp + "get_download_tasks", Description: "查看所有下载任务及状态(running/done/failed),包含文件名、保存路径、错误信息等",
NoMemory: false,
Cleaner: cleaner,
Parameters: map[string]interface{}{
"type": "object", "properties": map[string]interface{}{},
},
}, p.handleGetDownloadTasks) }, p.handleGetDownloadTasks)
p.regTool(s, tp+"upload_group_file", "上传文件到QQ群(通过base64编码发送,同时出现在群消息和群文件柜)", map[string]interface{}{ p.regTool(s, sdk.ToolDef{
"type": "object", "properties": map[string]interface{}{ Name: tp + "upload_group_file", Description: "上传文件到QQ群(通过base64编码发送,同时出现在群消息和群文件柜)",
"group_id": map[string]interface{}{"type": "integer", "description": "目标群号"}, Parameters: map[string]interface{}{
"file": map[string]interface{}{"type": "string", "description": "本地文件路径"}, "type": "object", "properties": map[string]interface{}{
"name": map[string]interface{}{"type": "string", "description": "文件名(可选,默认取原文件名)"}, "group_id": map[string]interface{}{"type": "integer", "description": "目标群号"},
}, "required": []string{"group_id", "file"}, "file": map[string]interface{}{"type": "string", "description": "本地文件路径"},
"name": map[string]interface{}{"type": "string", "description": "文件名(可选,默认取原文件名)"},
}, "required": []string{"group_id", "file"},
},
NoMemory: true,
}, p.handleUploadGroupFile) }, p.handleUploadGroupFile)
// ---- 文档/视频/网页工具 ---- // ---- 文档/视频/网页工具 ----
p.regTool(s, tp+"read_document", "读取文档内容文本。支持 PDF、DOCX、DOC、XLSX、XLS、PPTX、PPT、TXT、CSV、MD 格式。使用 libreoffice + pandoc 转换提取文本,返回前 20000 字符。适合处理用户发来的文档文件。", map[string]interface{}{ p.regTool(s, sdk.ToolDef{
"type": "object", "properties": map[string]interface{}{ Name: tp + "read_document", Description: "读取文档内容文本。支持 PDF、DOCX、DOC、XLSX、XLS、PPTX、PPT、TXT、CSV、MD 格式。使用 libreoffice + pandoc 转换提取文本,返回前 20000 字符。适合处理用户发来的文档文件。",
"path": map[string]interface{}{"type": "string", "description": "文档文件路径(已保存到本地的文件路径)"}, NoMemory: false,
}, "required": []string{"path"}, Cleaner: cleaner,
Parameters: map[string]interface{}{
"type": "object", "properties": map[string]interface{}{
"path": map[string]interface{}{"type": "string", "description": "文档文件路径(已保存到本地的文件路径)"},
}, "required": []string{"path"},
},
}, p.handleReadDocument) }, p.handleReadDocument)
p.regTool(s, tp+"video_download", "下载视频到本地。支持 B站、YouTube 等主流视频网站(通过 yt-dlp)。先调用 info_only 查看视频信息,再下载。下载后文件保存在 agentfs 目录。", map[string]interface{}{ p.regTool(s, sdk.ToolDef{
"type": "object", "properties": map[string]interface{}{ Name: tp + "video_download", Description: "下载视频到本地。支持 B站、YouTube 等主流视频网站(通过 yt-dlp)。先调用 info_only 查看视频信息,再下载。下载后文件保存在 agentfs 目录。",
"url": map[string]interface{}{"type": "string", "description": "视频分享链接"}, NoMemory: false,
"info_only": map[string]interface{}{"type": "boolean", "description": "仅获取视频信息(标题、时长、清晰度列表),不下"}, Cleaner: cleaner,
}, "required": []string{"url"}, Parameters: map[string]interface{}{
"type": "object", "properties": map[string]interface{}{
"url": map[string]interface{}{"type": "string", "description": "视频分享链接"},
"info_only": map[string]interface{}{"type": "boolean", "description": "仅获取视频信息(标题、时长、清晰度列表),不下"},
}, "required": []string{"url"},
},
}, p.handleVideoDownload) }, p.handleVideoDownload)
// ---- 附加 ---- // ---- 附加 ----
p.regTool(s, tp+"send_like", "给QQ好友点赞/戳一戳", map[string]interface{}{ p.regTool(s, sdk.ToolDef{
"type": "object", "properties": map[string]interface{}{ Name: tp + "send_like", Description: "给QQ好友点赞/戳一戳",
"user_id": map[string]interface{}{"type": "integer", "description": "目标QQ号"}, Parameters: map[string]interface{}{
"times": map[string]interface{}{"type": "integer", "description": "点赞次数1-20,默认1"}, "type": "object", "properties": map[string]interface{}{
}, "required": []string{"user_id"}, "user_id": map[string]interface{}{"type": "integer", "description": "目标QQ号"},
"times": map[string]interface{}{"type": "integer", "description": "点赞次数1-20,默认1"},
}, "required": []string{"user_id"},
},
NoMemory: true,
}, p.handleSendLike) }, p.handleSendLike)
s.RegisterStage(sdk.StageBeforeToolcall, p.beforeOwnToolcall, sdk.StageScopeOwnTools) s.RegisterStage(sdk.StageBeforeToolcall, p.beforeOwnToolcall, sdk.StageScopeOwnTools)
@ -347,8 +434,8 @@ func (p *Plugin) Stop() error {
return nil return nil
} }
func (p *Plugin) regTool(s *sdk.PluginSDK, name, desc string, params map[string]interface{}, handler sdk.ToolHandler) { func (p *Plugin) regTool(s *sdk.PluginSDK, def sdk.ToolDef, handler sdk.ToolHandler) {
s.RegisterTool(name, sdk.ToolDef{Name: name, Description: desc, Parameters: params}, handler) s.RegisterTool(def.Name, def, handler)
} }
// ======== Bot Identity ======== // ======== Bot Identity ========
@ -2078,7 +2165,7 @@ func convInt64(v interface{}) (int64, error) {
return 0, fmt.Errorf("cannot convert %T to int64", v) return 0, fmt.Errorf("cannot convert %T to int64", v)
} }
func NewPlugin(name string, config map[string]interface{}) (sdk.Plugin, error) { func NewPluginFactory(name string, config map[string]interface{}) (sdk.Plugin, error) {
return &Plugin{ return &Plugin{
name: name, name: name,
allowFrom: make(map[int64]struct{}), allowFrom: make(map[int64]struct{}),

View File

@ -1,101 +0,0 @@
/* Code generated by cmd/cgo; DO NOT EDIT. */
/* package qq */
#line 1 "cgo-builtin-export-prolog"
#include <stddef.h>
#ifndef GO_CGO_EXPORT_PROLOGUE_H
#define GO_CGO_EXPORT_PROLOGUE_H
#ifndef GO_CGO_GOSTRING_TYPEDEF
typedef struct { const char *p; ptrdiff_t n; } _GoString_;
extern size_t _GoStringLen(_GoString_ s);
extern const char *_GoStringPtr(_GoString_ s);
#endif
#endif
/* Start of preamble from import "C" comments. */
#line 3 "z_bridge_gen.go"
#include <stdlib.h>
int ha_dispatch(int method_id, void* core_api, char* s1, char* s2, char* s3, int i1, int i2, char** result, char** error);
#line 1 "cgo-generated-wrapper"
/* End of preamble from import "C" comments. */
/* Start of boilerplate cgo prologue. */
#line 1 "cgo-gcc-export-header-prolog"
#ifndef GO_CGO_PROLOGUE_H
#define GO_CGO_PROLOGUE_H
typedef signed char GoInt8;
typedef unsigned char GoUint8;
typedef short GoInt16;
typedef unsigned short GoUint16;
typedef int GoInt32;
typedef unsigned int GoUint32;
typedef long long GoInt64;
typedef unsigned long long GoUint64;
typedef GoInt64 GoInt;
typedef GoUint64 GoUint;
typedef size_t GoUintptr;
typedef float GoFloat32;
typedef double GoFloat64;
#ifdef _MSC_VER
#if !defined(__cplusplus) || _MSVC_LANG <= 201402L
#include <complex.h>
typedef _Fcomplex GoComplex64;
typedef _Dcomplex GoComplex128;
#else
#include <complex>
typedef std::complex<float> GoComplex64;
typedef std::complex<double> GoComplex128;
#endif
#else
typedef float _Complex GoComplex64;
typedef double _Complex GoComplex128;
#endif
/*
static assertion to make sure the file is being used on architecture
at least with matching size of GoInt.
*/
typedef char _check_for_64_bit_pointer_matching_GoInt[sizeof(void*)==64/8 ? 1:-1];
#ifndef GO_CGO_GOSTRING_TYPEDEF
typedef _GoString_ GoString;
#endif
typedef void *GoMap;
typedef void *GoChan;
typedef struct { void *t; void *v; } GoInterface;
typedef struct { void *data; GoInt len; GoInt cap; } GoSlice;
#endif
/* End of boilerplate cgo prologue. */
#ifdef __cplusplus
extern "C" {
#endif
extern int go_init_plugin(char* name, char* configJSON, char** errorOut);
extern int go_start_plugin(void* coreAPIptr, int coreVersion, char** errorOut);
extern int go_stop_plugin(char** errorOut);
extern int go_invoke_tool(char* name, char* argsJSON, char** resultOut, char** errorOut);
extern int go_invoke_stage(char* stage, char* ctxJSON, char** errorOut);
extern int go_invoke_output(char* channel, char* msgType, char* payloadJSON, char** errorOut);
extern void go_free_string(char* ptr);
#ifdef __cplusplus
}
#endif

View File

@ -2,13 +2,16 @@ module rss
go 1.25.0 go 1.25.0
require gitcode.com/JianFeeeee/homeagent-sdk v0.0.0
require ( require (
github.com/mmcdole/gofeed v1.4.0 // indirect gitcode.com/JianFeeeee/homeagent-sdk v0.0.0
github.com/mmcdole/goxpp/v2 v2.0.0 // indirect github.com/mmcdole/gofeed v1.4.0
golang.org/x/net v0.56.0 // indirect github.com/mmcdole/goxpp/v2 v2.0.0
golang.org/x/text v0.38.0 // indirect golang.org/x/net v0.56.0
golang.org/x/text v0.38.0
) )
replace gitcode.com/JianFeeeee/homeagent-sdk => /tmp/opencode/sdk-clone replace gitcode.com/JianFeeeee/homeagent-sdk => E:/program/homeagent/homeagentsdk
replace gitcode.com/JianFeeeee/homeagent-sdk => E:/program/homeagent/homeagentsdk
replace gitcode.com/JianFeeeee/homeagent-sdk => E:/program/homeagent/homeagentsdk

11
example/rss/main.go Normal file
View File

@ -0,0 +1,11 @@
//go:build !windows || !cgo
package main
import (
sdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
)
func NewPlugin(name string, config map[string]interface{}) (sdk.Plugin, error) {
return NewPluginFactory(name, config)
}

View File

@ -37,7 +37,7 @@ type Plugin struct {
pollTicker *time.Ticker pollTicker *time.Ticker
} }
func NewPlugin(name string, config map[string]interface{}) (sdk.Plugin, error) { func NewPluginFactory(name string, config map[string]interface{}) (sdk.Plugin, error) {
return &Plugin{name: name}, nil return &Plugin{name: name}, nil
} }
@ -122,6 +122,7 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
tp := p.name + "_" tp := p.name + "_"
s.RegisterTool(tp+"subscribe", sdk.ToolDef{ s.RegisterTool(tp+"subscribe", sdk.ToolDef{
Name: tp + "subscribe", Description: "Subscribe to an RSS/Atom feed URL", Name: tp + "subscribe", Description: "Subscribe to an RSS/Atom feed URL",
NoMemory: true,
Parameters: map[string]interface{}{ Parameters: map[string]interface{}{
"type": "object", "type": "object",
"properties": map[string]interface{}{ "properties": map[string]interface{}{
@ -134,6 +135,7 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
s.RegisterTool(tp+"unsubscribe", sdk.ToolDef{ s.RegisterTool(tp+"unsubscribe", sdk.ToolDef{
Name: tp + "unsubscribe", Description: "Unsubscribe from a feed", Name: tp + "unsubscribe", Description: "Unsubscribe from a feed",
NoMemory: true,
Parameters: map[string]interface{}{ Parameters: map[string]interface{}{
"type": "object", "type": "object",
"properties": map[string]interface{}{ "properties": map[string]interface{}{
@ -153,6 +155,7 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
s.RegisterTool(tp+"check_now", sdk.ToolDef{ s.RegisterTool(tp+"check_now", sdk.ToolDef{
Name: tp + "check_now", Description: "Manually check all feeds for new articles now", Name: tp + "check_now", Description: "Manually check all feeds for new articles now",
NoMemory: true,
Parameters: map[string]interface{}{ Parameters: map[string]interface{}{
"type": "object", "type": "object",
"properties": map[string]interface{}{}, "properties": map[string]interface{}{},

View File

@ -2,6 +2,6 @@ module sanitizer
go 1.25.0 go 1.25.0
require gitcode.com/JianFeeeee/homeagent-sdk v0.0.0-20260708004841-e9bdcf9304b0 require gitcode.com/JianFeeeee/homeagent-sdk v0.0.0
replace gitcode.com/JianFeeeee/homeagent-sdk => ../.. replace gitcode.com/JianFeeeee/homeagent-sdk => E:/program/homeagent/homeagentsdk

11
example/sanitizer/main.go Normal file
View File

@ -0,0 +1,11 @@
//go:build !windows || !cgo
package main
import (
sdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
)
func NewPlugin(name string, config map[string]interface{}) (sdk.Plugin, error) {
return NewPluginFactory(name, config)
}

View File

@ -53,7 +53,7 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
func (p *Plugin) Stop() error { return nil } func (p *Plugin) Stop() error { return nil }
func NewPlugin(name string, config map[string]interface{}) (sdk.Plugin, error) { func NewPluginFactory(name string, config map[string]interface{}) (sdk.Plugin, error) {
return &Plugin{}, nil return &Plugin{}, nil
} }

View File

@ -4,4 +4,4 @@ go 1.25.0
require gitcode.com/JianFeeeee/homeagent-sdk v0.0.0 require gitcode.com/JianFeeeee/homeagent-sdk v0.0.0
replace gitcode.com/JianFeeeee/homeagent-sdk => /tmp/opencode/sdk-clone replace gitcode.com/JianFeeeee/homeagent-sdk => E:/program/homeagent/homeagentsdk

View File

@ -21,7 +21,7 @@ type Plugin struct {
defaultLoc string defaultLoc string
} }
func NewPlugin(name string, config map[string]interface{}) (sdk.Plugin, error) { func NewPluginFactory(name string, config map[string]interface{}) (sdk.Plugin, error) {
return &Plugin{name: name}, nil return &Plugin{name: name}, nil
} }

View File

@ -5,7 +5,7 @@ package meta
var ( var (
// Version 是 HomeAgent SDK 版本号。 // Version 是 HomeAgent SDK 版本号。
// 通过 `-ldflags="-X gitcode.com/JianFeeeee/homeagent-sdk/meta.Version=vX.Y.Z"` 注入。 // 通过 `-ldflags="-X gitcode.com/JianFeeeee/homeagent-sdk/meta.Version=vX.Y.Z"` 注入。
Version = "0.7.1" Version = "0.8.0"
// Commit 是构建时的 Git commit hash。 // Commit 是构建时的 Git commit hash。
Commit = "unknown" Commit = "unknown"

View File

@ -92,6 +92,8 @@ type ToolDef struct {
Plugin string `json:"plugin,omitempty"` Plugin string `json:"plugin,omitempty"`
Description string `json:"description"` Description string `json:"description"`
Parameters map[string]interface{} `json:"parameters"` Parameters map[string]interface{} `json:"parameters"`
NoMemory bool `json:"no_memory,omitempty"` // 此工具输出不参与记忆计算,但原文保留
Cleaner func(string) string `json:"-"` // 计算层过滤函数,不改原文;仅在向量化/jieba/蒸馏时调用
} }
// IOInjector provides methods for injecting input and interrupts into the agent pipeline. // IOInjector provides methods for injecting input and interrupts into the agent pipeline.

View File

@ -177,3 +177,77 @@ func TestRegisterStageOwnToolsNilRegStage(t *testing.T) {
s := &PluginSDK{name: "test"} s := &PluginSDK{name: "test"}
s.RegisterStage(StageBeforeToolcall, func(ctx *StageContext) error { return nil }, StageScopeOwnTools) s.RegisterStage(StageBeforeToolcall, func(ctx *StageContext) error { return nil }, StageScopeOwnTools)
} }
func TestToolDefCleaner(t *testing.T) {
called := false
def := ToolDef{
Name: "test_clean",
Description: "A test tool with cleaner",
Parameters: map[string]interface{}{"type": "object", "properties": map[string]interface{}{}},
Cleaner: func(output string) string {
called = true
return "cleaned:" + output
},
}
if def.Cleaner == nil {
t.Fatal("Cleaner should not be nil")
}
result := def.Cleaner("raw output")
if !called {
t.Error("Cleaner was not called")
}
if result != "cleaned:raw output" {
t.Errorf("expected 'cleaned:raw output', got '%s'", result)
}
}
func TestToolDefNoMemory(t *testing.T) {
def := ToolDef{
Name: "test_nomem",
Description: "A test tool with NoMemory",
Parameters: map[string]interface{}{"type": "object", "properties": map[string]interface{}{}},
NoMemory: true,
}
if !def.NoMemory {
t.Error("NoMemory should be true")
}
if def.Cleaner != nil {
t.Error("Cleaner should be nil when not set")
}
}
func TestToolDefNoMemoryDefaultFalse(t *testing.T) {
def := ToolDef{
Name: "test_default",
Description: "A test tool with defaults",
Parameters: map[string]interface{}{"type": "object", "properties": map[string]interface{}{}},
}
if def.NoMemory {
t.Error("NoMemory should default to false")
}
}
func TestToolDefRegisterPreservesNoMemory(t *testing.T) {
var capturedDef ToolDef
regTool := func(name string, def ToolDef, handler ToolHandler) error {
capturedDef = def
return nil
}
s := &PluginSDK{regTool: regTool, name: "test"}
def := ToolDef{
Name: "test_tool",
Description: "test desc",
Parameters: map[string]interface{}{"type": "object", "properties": map[string]interface{}{}},
NoMemory: true,
Cleaner: func(s string) string { return s },
}
s.RegisterTool("test_tool", def, func(args map[string]interface{}) (interface{}, error) {
return nil, nil
})
if !capturedDef.NoMemory {
t.Error("NoMemory should be preserved through RegisterTool")
}
if capturedDef.Cleaner == nil {
t.Error("Cleaner should be preserved through RegisterTool")
}
}

View File

@ -14,8 +14,9 @@ import (
type BuildConfig struct { type BuildConfig struct {
OutDir string OutDir string
Targets []string // "linux/amd64", "windows/amd64", "lua" Targets []string
Bundle bool Bundle bool
SDKPath string
} }
func cmdBuild(args []string) { func cmdBuild(args []string) {
@ -24,48 +25,43 @@ func cmdBuild(args []string) {
switch args[i] { switch args[i] {
case "--outdir": case "--outdir":
if i+1 < len(args) { if i+1 < len(args) {
cfg.OutDir = args[i+1] cfg.OutDir = args[i+1]; i++
i++
} }
case "--target": case "--target":
if i+1 < len(args) { if i+1 < len(args) {
cfg.Targets = append(cfg.Targets, args[i+1]) cfg.Targets = append(cfg.Targets, args[i+1]); i++
i++
} }
case "--bundle": case "--bundle":
cfg.Bundle = true cfg.Bundle = true
case "--sdk-path":
if i+1 < len(args) {
cfg.SDKPath = args[i+1]; i++
}
} }
} }
// read plg.json
plg, err := readPlgJSON("plg.json") plg, err := readPlgJSON("plg.json")
if err != nil { if err != nil {
fmt.Printf("error: read plg.json: %v\n", err) fmt.Printf("error: read plg.json: %v\n", err); os.Exit(1)
os.Exit(1)
} }
if plg.IsLua() { if plg.IsLua() {
buildTarget(plg, "lua", cfg.OutDir) buildTarget(plg, "lua", cfg.OutDir, "")
return return
} }
if cfg.Bundle { // Ensure go.mod exists with correct SDK path
buildBundle(plg, cfg.OutDir) ensureGoMod(plg, cfg.SDKPath)
// Default: bundle mode (all 3 platforms in one .hmap)
if cfg.Bundle || len(cfg.Targets) == 0 {
buildBundle(plg, cfg.OutDir, cfg.SDKPath)
return return
} }
// determine targets // Explicit --target: build each separately
targets := cfg.Targets for _, t := range cfg.Targets {
if len(targets) == 0 { buildTarget(plg, t, cfg.OutDir, cfg.SDKPath)
targets = parseTargets(plg.Targets)
}
if len(targets) == 0 {
targets = []string{"native"}
}
// build for each target
for _, t := range targets {
buildTarget(plg, t, cfg.OutDir)
} }
} }
@ -80,7 +76,7 @@ var allBundleTargets = []struct {
{"windows/amd64", "plugin.dll"}, {"windows/amd64", "plugin.dll"},
} }
func buildBundle(plg *PlgConfig, outDir string) { func buildBundle(plg *PlgConfig, outDir string, sdkPath string) {
os.MkdirAll(outDir, 0755) os.MkdirAll(outDir, 0755)
buildDir := "build" buildDir := "build"
os.MkdirAll(buildDir, 0755) os.MkdirAll(buildDir, 0755)
@ -141,9 +137,7 @@ func buildBundle(plg *PlgConfig, outDir string) {
fmt.Printf(" packaged %s\n", filepath.Base(hmapPath)) fmt.Printf(" packaged %s\n", filepath.Base(hmapPath))
} }
func (p *PlgConfig) IsLua() bool { func (p *PlgConfig) IsLua() bool { return p.Entry == "main.lua" }
return p.Entry == "main.lua"
}
func readPlgJSON(path string) (*PlgConfig, error) { func readPlgJSON(path string) (*PlgConfig, error) {
data, err := os.ReadFile(path) data, err := os.ReadFile(path)
@ -225,7 +219,108 @@ func resolveBuild(target string) (*buildConfig, string) {
} }
} }
func buildTarget(plg *PlgConfig, target, outDir string) { // ensureGoMod 确保插件项目的 go.mod 包含 SDK 的 replace 指令。
// 如果 go.mod 不存在或已有正确 replace,则跳过。
func ensureGoMod(plg *PlgConfig, sdkPath string) {
if sdkPath == "" {
// 从 plugindev 自身推断 SDK 路径
self, err := os.Executable()
if err != nil {
return
}
cand := filepath.Dir(filepath.Dir(filepath.Dir(self)))
if _, err := os.Stat(filepath.Join(cand, "sdk", "plugin.go")); err != nil {
return
}
sdkPath = cand
}
gomodPath := "go.mod"
data, err := os.ReadFile(gomodPath)
if err != nil {
return // no go.mod, skip
}
lines := strings.Split(string(data), "\n")
var sdkModule string
for _, line := range lines {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "require ") || strings.HasPrefix(line, "require (") {
continue
}
if strings.Contains(line, "homeagent-sdk/sdk") || strings.Contains(line, "homeagent-sdk") {
parts := strings.Fields(line)
if len(parts) >= 1 && !strings.HasPrefix(parts[0], "//") && !strings.HasPrefix(parts[0], "replace") {
sdkModule = parts[0]
}
}
}
if sdkModule == "" {
return
}
// 检查是否已有 replace 指令
absSDK, _ := filepath.Abs(sdkPath)
absSDK = strings.ReplaceAll(absSDK, "\\", "/")
for _, line := range lines {
if strings.Contains(line, "replace") && strings.Contains(line, sdkModule) {
parts := strings.Fields(line)
if len(parts) >= 3 && strings.ReplaceAll(parts[2], "\\", "/") == absSDK {
return // 已存在且路径正确
}
}
}
// 追加 replace 指令
replaceLine := fmt.Sprintf("replace %s => %s", sdkModule, absSDK)
newData := string(data) + "\n" + replaceLine + "\n"
if err := os.WriteFile(gomodPath, []byte(newData), 0644); err != nil {
fmt.Printf(" warn: update go.mod replace: %v\n", err)
}
}
func resolveSDKPath(sdkPath string) string {
if sdkPath != "" {
abs, _ := filepath.Abs(sdkPath)
if _, err := os.Stat(filepath.Join(abs, "sdk", "plugin.go")); err == nil {
return abs
}
fmt.Printf("error: --sdk-path %q not a valid SDK\n", sdkPath)
os.Exit(1)
}
// Detect from plugindev's own location (internal dev)
self, err := os.Executable()
if err == nil {
cand := filepath.Dir(filepath.Dir(filepath.Dir(self)))
if _, err := os.Stat(filepath.Join(cand, "sdk", "plugin.go")); err == nil {
return cand
}
}
// Active SDK via plugindev sdk use
store := os.Getenv("HOMEAGENT_SDK_DIR")
if store == "" {
home, _ := os.UserHomeDir()
if home != "" {
store = filepath.Join(home, ".homeagent", "plugindev", "sdk")
}
}
if store != "" {
if d, err := os.ReadFile(filepath.Join(store, "current")); err == nil {
ver := strings.TrimSpace(string(d))
if ver != "" {
root := filepath.Join(store, ver)
if _, err := os.Stat(filepath.Join(root, "sdk", "plugin.go")); err == nil {
return root
}
}
}
}
fmt.Printf("error: cannot locate SDK. Use --sdk-path or 'plugindev sdk use'\n")
os.Exit(1)
return ""
}
func buildTarget(plg *PlgConfig, target, outDir, sdkPath string) {
os.MkdirAll(outDir, 0755) os.MkdirAll(outDir, 0755)
// Lua: no compilation, package source directly // Lua: no compilation, package source directly

View File

@ -46,8 +46,14 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
}) })
tp := p.name + "_" tp := p.name + "_"
s.RegisterTool(tp+"hello", sdk.ToolDef{ s.RegisterTool(tp+"hello", sdk.ToolDef{
Name: tp + "hello", Description: "A hello world tool", Name: tp + "hello",
Parameters: map[string]interface{}{"type": "object", "properties": map[string]interface{}{}}, Description: "A hello world tool",
Parameters: map[string]interface{}{"type": "object", "properties": map[string]interface{}{}},
NoMemory: false, // 工具输出对 LLM 注意力有信号价值时为 false,纯操作工具为 true
// Cleaner: func(output string) string {
// // 工具输出参与向量化/jieba/蒸馏前,在此过滤噪音
// return output
// },
}, p.handleHello) }, p.handleHello)
fmt.Printf("[%s] started\n", p.name) fmt.Printf("[%s] started\n", p.name)
return nil return nil
@ -59,7 +65,7 @@ func (p *Plugin) handleHello(args map[string]interface{}) (interface{}, error) {
return map[string]interface{}{"content": "Hello from {{.Plg.Name}} plugin!"}, nil return map[string]interface{}{"content": "Hello from {{.Plg.Name}} plugin!"}, nil
} }
func NewPlugin(name string, config map[string]interface{}) (sdk.Plugin, error) { func NewPluginFactory(name string, config map[string]interface{}) (sdk.Plugin, error) {
return &Plugin{name: name}, nil return &Plugin{name: name}, nil
} }
` `
@ -157,7 +163,7 @@ func NewPlugin(name *C.char, configJSON *C.char) unsafe.Pointer {
if c, ok := wrapper["config"].(map[string]interface{}); ok { config = c } if c, ok := wrapper["config"].(map[string]interface{}); ok { config = c }
} }
} }
plg, err := NewPlugin(goName, config) plg, err := NewPluginFactory(goName, config)
if err != nil { return nil } if err != nil { return nil }
return newHandle(plg) return newHandle(plg)
} }
@ -173,6 +179,7 @@ func StartPlugin(handle unsafe.Pointer) C.int {
}, },
func(stage sdk.Stage, handler sdk.StageHandler) { bs.stages[string(stage)] = handler }, func(stage sdk.Stage, handler sdk.StageHandler) { bs.stages[string(stage)] = handler },
func(name string) error { return nil }, func(name string) error { return nil },
func(name string, caps int, desc string, handler sdk.ToolHandler) error { return nil },
) )
if err := bs.plugin.Start(mockSDK); err != nil { return 1 } if err := bs.plugin.Start(mockSDK); err != nil { return 1 }
return 0 return 0
@ -207,11 +214,11 @@ func InvokeToolJSON(handle unsafe.Pointer, toolName *C.char, argsJSON *C.char) *
if bs == nil || toolName == nil { return nil } if bs == nil || toolName == nil { return nil }
goName := C.GoString(toolName) goName := C.GoString(toolName)
handler, ok := bs.handlers[goName] handler, ok := bs.handlers[goName]
if !ok { r, _ := json.Marshal(map[string]interface{}{"error": "tool not found: " + goName}); return C.CString(string(r)) } if !ok { errMsg, _ := json.Marshal(map[string]interface{}{"error": "tool not found: " + goName}); return C.CString(string(errMsg)) }
var args map[string]interface{} var args map[string]interface{}
if argsJSON != nil { json.Unmarshal([]byte(C.GoString(argsJSON)), &args) } if argsJSON != nil { json.Unmarshal([]byte(C.GoString(argsJSON)), &args) }
r, err := handler(args) r, err := handler(args)
if err != nil { r, _ = json.Marshal(map[string]interface{}{"error": err.Error()}); return C.CString(string(r)) } if err != nil { errMsg, _ := json.Marshal(map[string]interface{}{"error": err.Error()}); return C.CString(string(errMsg)) }
b, _ := json.Marshal(r) b, _ := json.Marshal(r)
return C.CString(string(b)) return C.CString(string(b))
} }

View File

@ -1 +0,0 @@
I need to rewrite the preamble section. Let me use a python script to make this change.

View File

@ -1,13 +0,0 @@
# testplugin
testplugin plugin
## Build
```bash
plugindev build
```
## Install
Upload the .hmap file through the Plugin Manager API.

View File

@ -1,11 +0,0 @@
{
"name": "testplugin",
"name_zh": "中文名",
"name_en": "Testplugin",
"version": "0.1.0",
"description": "testplugin plugin",
"author": "HomeAgent",
"entry": "plugin.so",
"tags": ["testplugin"],
"targets": "linux/amd64,windows/amd64"
}

View File

@ -1,57 +0,0 @@
package main
import (
"fmt"
"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
s.Settings().RegisterDef(sdk.ConfigDef{
Key: "plugin.testplugin.example",
Default: "hello",
Type: "string",
DisplayName: "示例配置",
Description: "An example configuration key",
Category: "testplugin",
})
tp := p.name + "_"
s.RegisterTool(tp+"hello", sdk.ToolDef{
Name: tp + "hello",
Description: "A hello world tool",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{},
},
}, p.handleHello)
fmt.Printf("[%s] started\n", p.name)
return nil
}
func (p *Plugin) Stop() error {
fmt.Printf("[%s] stopped\n", p.name)
return nil
}
func (p *Plugin) handleHello(args map[string]interface{}) (interface{}, error) {
return map[string]interface{}{
"content": "Hello from testplugin plugin!",
}, nil
}
// NewPluginFactory creates a Plugin instance. Called by both Linux entry (main.go)
// and Windows bridge (z_bridge_gen.go) to avoid naming conflict with C export.
func NewPluginFactory(name string, config map[string]interface{}) (sdk.Plugin, error) {
return &Plugin{name: name}, nil
}