mirror of
https://gitcode.com/JianFeeeee/homeagent-sdk.git
synced 2026-09-21 01:18:02 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 87136057b1 | |||
| 84bf100a12 | |||
| 78ef7998c2 | |||
| b166697dd7 | |||
| 4d01e75282 | |||
| c5bcae9404 | |||
| c7c66b8d39 |
92
README.md
92
README.md
@ -23,7 +23,8 @@ type Plugin interface {
|
|||||||
| 分类 | 方法 | 说明 |
|
| 分类 | 方法 | 说明 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| 阶段钩子 | `RegisterStage(stage, handler, scope...)` | 注册阶段回调,scope 可选:`StageScopeGlobal`(全局,默认)或 `StageScopeOwnTools`(仅自己工具) |
|
| 阶段钩子 | `RegisterStage(stage, handler, scope...)` | 注册阶段回调,scope 可选:`StageScopeGlobal`(全局,默认)或 `StageScopeOwnTools`(仅自己工具) |
|
||||||
| 输出通道 | `RegisterOutputChannel(name, caps, desc, handler)` | 注册输出通道,caps 为能力位掩码 |
|
| 输入通道 | `RegisterInputChannel(name, def)` | 注册输入通道,def 为 `ChannelDef`(NoMemory/Cleaner) |
|
||||||
|
| 输出通道 | `RegisterOutputChannel(name, caps, desc, def, handler)` | 注册输出通道,def 为 `ChannelDef`,caps 为能力位掩码 |
|
||||||
| 工具注册 | `RegisterTool(name, def, handler)` | 注册工具供 LLM 调用 |
|
| 工具注册 | `RegisterTool(name, def, handler)` | 注册工具供 LLM 调用 |
|
||||||
| 插件 API | `RegisterPluginAPI(name)` | 注册插件 API 供其他插件访问 |
|
| 插件 API | `RegisterPluginAPI(name)` | 注册插件 API 供其他插件访问 |
|
||||||
| 图记忆 | `Memory()` | 访问图记忆 API(实体-关系存储) |
|
| 图记忆 | `Memory()` | 访问图记忆 API(实体-关系存储) |
|
||||||
@ -47,10 +48,30 @@ sdk.RegisterStage(StagePreAction, func(ctx *StageContext) error { return nil })
|
|||||||
sdk.RegisterStage(StageBeforeToolcall, myHandler, StageScopeOwnTools)
|
sdk.RegisterStage(StageBeforeToolcall, myHandler, StageScopeOwnTools)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### ChannelDef
|
||||||
|
|
||||||
|
```go
|
||||||
|
type ChannelDef struct {
|
||||||
|
NoMemory bool // 通道输入/输出不参与记忆计算(向量/关键词/蒸馏),原文保留
|
||||||
|
Cleaner func(string) string // 可选:计算层过滤函数(不改原文)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`ChannelDef` 控制通道在记忆计算层的行为,与 `ToolDef` 的 `NoMemory`/`Cleaner` 语义一致。
|
||||||
|
|
||||||
|
### 输入通道
|
||||||
|
|
||||||
|
```go
|
||||||
|
sdk.RegisterInputChannel("qq", ChannelDef{
|
||||||
|
NoMemory: true,
|
||||||
|
Cleaner: func(text string) string { return strings.TrimSpace(text) },
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
### 输出通道
|
### 输出通道
|
||||||
|
|
||||||
```go
|
```go
|
||||||
sdk.RegisterOutputChannel("my-channel", CapText|CapFile, "通道描述", handler)
|
sdk.RegisterOutputChannel("my-channel", CapText|CapFile, "通道描述", ChannelDef{}, handler)
|
||||||
```
|
```
|
||||||
|
|
||||||
handler 接收三个参数:
|
handler 接收三个参数:
|
||||||
@ -117,7 +138,7 @@ Triple 数据结构新增字段:
|
|||||||
func New(name string, sett SettingsAPI, regTool ToolRegistrar, regStage StageRegistrar, regAPI APIRegistrar, regOutput OutputChannelRegistrar) *PluginSDK
|
func New(name string, sett SettingsAPI, regTool ToolRegistrar, regStage StageRegistrar, regAPI APIRegistrar, regOutput OutputChannelRegistrar) *PluginSDK
|
||||||
```
|
```
|
||||||
|
|
||||||
插件开发者只需实现 `Plugin` 接口并导出 `NewPlugin()` 入口函数。
|
插件开发者只需实现 `Plugin` 接口并导出 `NewPluginFactory()` 入口函数。
|
||||||
|
|
||||||
## plugindev 工具链
|
## plugindev 工具链
|
||||||
|
|
||||||
@ -125,13 +146,25 @@ func New(name string, sett SettingsAPI, regTool ToolRegistrar, regStage StageReg
|
|||||||
|
|
||||||
| 命令 | 说明 |
|
| 命令 | 说明 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| `plugindev init` | 初始化插件项目(生成 plg.json、入口模板) |
|
| `plugindev init <name> [--lua]` | 初始化插件项目(生成 plg.json、plugin.go 或 main.lua、go.mod、README.md) |
|
||||||
| `plugindev build` | 构建插件,输出 .hmap 包 |
|
| `plugindev build [flags]` | 编译并打包为 `.hmap` 包(支持跨平台编译和 bundle 模式) |
|
||||||
| `plugindev clean` | 清理构建产物 |
|
| `plugindev clean` | 清理 `build/`、`dist/` 目录及生成文件(plugin.json、z_bridge_gen.go) |
|
||||||
| `plugindev debug` | 本地调试模式运行插件 |
|
| `plugindev debug [dir]` | 通过 Yaegi Go 解释器加载插件源码,启动交互式 REPL 调试 |
|
||||||
|
| `plugindev sdk <command>` | SDK 版本管理(子命令:list/install/use/path/current/latest) |
|
||||||
|
|
||||||
支持 **Go** 和 **Lua** 两种插件语言。
|
支持 **Go** 和 **Lua** 两种插件语言。
|
||||||
|
|
||||||
|
### build 命令 flags
|
||||||
|
|
||||||
|
| Flag | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `--outdir <dir>` | 输出目录(默认 `dist`,可覆盖 plg.json 中的 `outdir`) |
|
||||||
|
| `--target <os/arch>` | 构建目标(如 `linux/amd64`),可重复指定(追加到 plg.json 中的 targets) |
|
||||||
|
| `--bundle` | 强制 bundle 模式(同时编译 linux/amd64, darwin/amd64, windows/amd64) |
|
||||||
|
| `--no-bundle` | 关闭 bundle 模式,仅按 targets 逐个编译 |
|
||||||
|
| `--sdk-path <path>` | 指定 SDK 源码路径(覆盖 plg.json 中的 `sdk_path`) |
|
||||||
|
| `--replace <from=to>` / `-R` | Go 模块替换(追加到 plg.json 中的 replaces),`from` 为模块路径,`to` 为本地路径 |
|
||||||
|
|
||||||
### plg.json 清单格式
|
### plg.json 清单格式
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@ -164,13 +197,15 @@ func New(name string, sett SettingsAPI, regTool ToolRegistrar, regStage StageReg
|
|||||||
| `version` | string | 版本号 |
|
| `version` | string | 版本号 |
|
||||||
| `description` | string | 插件描述 |
|
| `description` | string | 插件描述 |
|
||||||
| `author` | string | 作者 |
|
| `author` | string | 作者 |
|
||||||
| `entry` | string | 入口文件(`plugin.so` / `main.lua`) |
|
| `entry` | string | 入口文件(`plugin.so` / `plugin.dll` / `main.lua`) |
|
||||||
| `tags` | string[] | 标签 |
|
| `tags` | string[] | 标签 |
|
||||||
| `targets` | string | 构建目标,逗号分隔(如 `linux/amd64,windows/amd64`) |
|
| `targets` | string | 构建目标,逗号分隔(如 `linux/amd64,windows/amd64`,Lua 插件为 `lua`) |
|
||||||
| `outdir` | string | 输出目录(默认 `dist`) |
|
| `outdir` | string | 输出目录(默认 `dist`) |
|
||||||
| `bundle` | bool | 是否 bundle 模式(同时编译多平台) |
|
| `bundle` | bool | 是否 bundle 模式(同时编译多平台,默认 `true`) |
|
||||||
|
| `sdk_path` | string | SDK 源码路径(覆盖自动检测的 SDK 路径) |
|
||||||
|
| `go_version` | string | Go 版本(如 `1.21`,默认从 SDK 的 go.mod 读取) |
|
||||||
| `replaces` | object | Go 模块替换,key=模块路径,value=本地路径 |
|
| `replaces` | object | Go 模块替换,key=模块路径,value=本地路径 |
|
||||||
| `source_dirs` | string[] | 额外源码搜索路径(编译时自动导入) |
|
| `source_dirs` | string[] | 额外源码搜索路径(编译时自动导入,用于引入 `thirdpart/` 外部的共享代码) |
|
||||||
|
|
||||||
### .hmap 包格式
|
### .hmap 包格式
|
||||||
|
|
||||||
@ -179,10 +214,34 @@ func New(name string, sett SettingsAPI, regTool ToolRegistrar, regStage StageReg
|
|||||||
- `plugin.json` — 插件元数据
|
- `plugin.json` — 插件元数据
|
||||||
- `plugin.so` — Go 编译产物(Linux)
|
- `plugin.so` — Go 编译产物(Linux)
|
||||||
- `plugin.dll` — Go 编译产物(Windows)
|
- `plugin.dll` — Go 编译产物(Windows)
|
||||||
|
- `plugin.dylib` — Go 编译产物(macOS,bundle 模式)
|
||||||
- `main.lua` — Lua 插件入口(Lua 插件时)
|
- `main.lua` — Lua 插件入口(Lua 插件时)
|
||||||
|
|
||||||
## 插件生命周期
|
## 插件生命周期
|
||||||
|
|
||||||
|
### 入口函数
|
||||||
|
|
||||||
|
插件必须导出 `NewPluginFactory` 入口函数(Go)或 `start()` 函数(Lua):
|
||||||
|
|
||||||
|
**Go 插件** — 实现 `Plugin` 接口并导出工厂函数:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func NewPluginFactory(name string, config map[string]interface{}) (sdk.Plugin, error) {
|
||||||
|
return &Plugin{name: name}, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
该函数由内核在加载插件时调用,`name` 为插件名,`config` 为 `skill.json` 中的配置(如有)。
|
||||||
|
|
||||||
|
**Lua 插件** — 返回包含 `start(sdk)` 和 `stop()` 方法的 table:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local plugin = { name = "my-plugin" }
|
||||||
|
function plugin.start(sdk) -- 注册工具等 end
|
||||||
|
function plugin.stop() end
|
||||||
|
return plugin
|
||||||
|
```
|
||||||
|
|
||||||
### 启动与停止
|
### 启动与停止
|
||||||
|
|
||||||
- `Start(sdk *PluginSDK) error` — 插件启动,接收 SDK 实例
|
- `Start(sdk *PluginSDK) error` — 插件启动,接收 SDK 实例
|
||||||
@ -214,14 +273,19 @@ enabled := sdk.AutoRestart()
|
|||||||
| 插件 | 说明 |
|
| 插件 | 说明 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| a2a | Agent-to-Agent 协议通信 |
|
| a2a | Agent-to-Agent 协议通信 |
|
||||||
|
| ai_image | AI 图片生成 |
|
||||||
| bili | Bilibili 视频下载 |
|
| bili | Bilibili 视频下载 |
|
||||||
| browser | 网络搜索、网页抓取、浏览器渲染(合并自 web/webfetch) |
|
| browser | 网络搜索、网页抓取、浏览器渲染 |
|
||||||
|
| calendar | 日历管理 |
|
||||||
| editdoc | 文档编辑 |
|
| editdoc | 文档编辑 |
|
||||||
| files | 文件管理 |
|
| files | 文件管理 |
|
||||||
| memo | 备忘录/记忆 |
|
| memo | 备忘录 |
|
||||||
|
| music | 音乐播放 |
|
||||||
| ocr | 光学字符识别 |
|
| ocr | 光学字符识别 |
|
||||||
| qq | QQ 消息集成 |
|
| qq | QQ 消息集成(NapCat webhook,15 个工具) |
|
||||||
|
| rss | RSS 订阅 |
|
||||||
| sanitizer | 内容清洗/安全过滤 |
|
| sanitizer | 内容清洗/安全过滤 |
|
||||||
|
| weather | 天气查询(wttr.in) |
|
||||||
|
|
||||||
## 构建与安装
|
## 构建与安装
|
||||||
|
|
||||||
|
|||||||
25
README_EN.md
25
README_EN.md
@ -23,7 +23,8 @@ The SDK instance injected via `Start(sdk *PluginSDK)` provides:
|
|||||||
| Category | Method | Description |
|
| Category | Method | Description |
|
||||||
|----------|--------|-------------|
|
|----------|--------|-------------|
|
||||||
| Stage Hooks | `RegisterStage(stage, handler, scope...)` | Register stage callback; scope: `StageScopeGlobal` (all, default) or `StageScopeOwnTools` (own tools only) |
|
| Stage Hooks | `RegisterStage(stage, handler, scope...)` | Register stage callback; scope: `StageScopeGlobal` (all, default) or `StageScopeOwnTools` (own tools only) |
|
||||||
| Output Channel | `RegisterOutputChannel(name, caps, desc, handler)` | Register output channel with capability bitmask |
|
| Input Channel | `RegisterInputChannel(name, def)` | Register input channel with `ChannelDef` (NoMemory/Cleaner) |
|
||||||
|
| Output Channel | `RegisterOutputChannel(name, caps, desc, def, handler)` | Register output channel with `ChannelDef` and capability bitmask |
|
||||||
| Tool Registration | `RegisterTool(name, def, handler)` | Register a tool for LLM invocation |
|
| Tool Registration | `RegisterTool(name, def, handler)` | Register a tool for LLM invocation |
|
||||||
| Plugin API | `RegisterPluginAPI(name)` | Register plugin API for inter-plugin access |
|
| Plugin API | `RegisterPluginAPI(name)` | Register plugin API for inter-plugin access |
|
||||||
| Graph Memory | `Memory()` | Access graph memory API (entity-relation store) |
|
| Graph Memory | `Memory()` | Access graph memory API (entity-relation store) |
|
||||||
@ -47,10 +48,30 @@ sdk.RegisterStage(StagePreAction, func(ctx *StageContext) error { return nil })
|
|||||||
sdk.RegisterStage(StageBeforeToolcall, myHandler, StageScopeOwnTools)
|
sdk.RegisterStage(StageBeforeToolcall, myHandler, StageScopeOwnTools)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### ChannelDef
|
||||||
|
|
||||||
|
```go
|
||||||
|
type ChannelDef struct {
|
||||||
|
NoMemory bool // Channel input/output skips memory computation (vector/keyword/distill), original text preserved
|
||||||
|
Cleaner func(string) string // Optional: computation layer filter (does not modify original text)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`ChannelDef` controls channel behavior in the memory computation layer, with the same semantics as `ToolDef.NoMemory`/`Cleaner`.
|
||||||
|
|
||||||
|
### Input Channels
|
||||||
|
|
||||||
|
```go
|
||||||
|
sdk.RegisterInputChannel("qq", ChannelDef{
|
||||||
|
NoMemory: true,
|
||||||
|
Cleaner: func(text string) string { return strings.TrimSpace(text) },
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
### Output Channels
|
### Output Channels
|
||||||
|
|
||||||
```go
|
```go
|
||||||
sdk.RegisterOutputChannel("my-channel", CapText|CapFile, "channel description", handler)
|
sdk.RegisterOutputChannel("my-channel", CapText|CapFile, "channel description", ChannelDef{}, handler)
|
||||||
```
|
```
|
||||||
|
|
||||||
The handler receives three arguments:
|
The handler receives three arguments:
|
||||||
|
|||||||
@ -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 => E:/program/homeagent/homeagentsdk
|
replace gitcode.com/JianFeeeee/homeagent-sdk => ../../
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"name": "qq",
|
"name": "qq",
|
||||||
"name_zh": "QQ消息",
|
"name_zh": "QQ消息",
|
||||||
"name_en": "qq",
|
"name_en": "qq",
|
||||||
@ -9,7 +9,7 @@
|
|||||||
"tags": ["qq", "messaging"],
|
"tags": ["qq", "messaging"],
|
||||||
"targets": "linux/amd64",
|
"targets": "linux/amd64",
|
||||||
"outdir": "dist",
|
"outdir": "dist",
|
||||||
"bundle": true,
|
"bundle": false,
|
||||||
"replaces": {},
|
"replaces": {},
|
||||||
"source_dirs": []
|
"source_dirs": []
|
||||||
}
|
}
|
||||||
|
|||||||
@ -102,6 +102,13 @@ type Plugin struct {
|
|||||||
agentfsDir string
|
agentfsDir string
|
||||||
downloadMu sync.Mutex
|
downloadMu sync.Mutex
|
||||||
downloadTasks []*DownloadTask
|
downloadTasks []*DownloadTask
|
||||||
|
typingMu sync.Mutex
|
||||||
|
typingMap map[int64]*typingState
|
||||||
|
}
|
||||||
|
|
||||||
|
type typingState struct {
|
||||||
|
userID int64
|
||||||
|
stopCh chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Plugin) Name() string { return p.name }
|
func (p *Plugin) Name() string { return p.name }
|
||||||
@ -165,7 +172,28 @@ meta JSON 格式:
|
|||||||
"reply_to": 12345 // 可选,回复指定消息 ID
|
"reply_to": 12345 // 可选,回复指定消息 ID
|
||||||
}
|
}
|
||||||
type 枚举: text(文字)/ voice(语音转文字后发送)/ image(图片URL)/ file(文件URL)`,
|
type 枚举: text(文字)/ voice(语音转文字后发送)/ image(图片URL)/ file(文件URL)`,
|
||||||
p.handleChannelOutput)
|
sdk.ChannelDef{}, p.handleChannelOutput)
|
||||||
|
|
||||||
|
// ---- 注册输入通道(记忆计算层行为) ----
|
||||||
|
inputCleaner := func(text string) string {
|
||||||
|
// 从中断模板中提取语义内容:消息来源和发送者昵称
|
||||||
|
// 模板: 来自「昵称」的私聊/群聊消息(message_id=N)...
|
||||||
|
// 模板: 【重要!老大消息】来自「昵称」...
|
||||||
|
cleaned := text
|
||||||
|
// 去掉模板前缀
|
||||||
|
if strings.HasPrefix(cleaned, "【重要!老大消息】") {
|
||||||
|
cleaned = strings.TrimPrefix(cleaned, "【重要!老大消息】")
|
||||||
|
}
|
||||||
|
// 提取 "来自「XXX」" 中的昵称作为关键词
|
||||||
|
if start := strings.Index(cleaned, "来自「"); start >= 0 {
|
||||||
|
if end := strings.Index(cleaned[start:], "」"); end >= 0 {
|
||||||
|
nick := cleaned[start+len("来自「") : start+end]
|
||||||
|
cleaned = nick
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cleaned
|
||||||
|
}
|
||||||
|
s.RegisterInputChannel("qq", sdk.ChannelDef{NoMemory: true, Cleaner: inputCleaner})
|
||||||
|
|
||||||
// 查询类工具输出清洗器:提取 JSON 中的 content/文本字段参与向量化
|
// 查询类工具输出清洗器:提取 JSON 中的 content/文本字段参与向量化
|
||||||
cleaner := func(output string) string {
|
cleaner := func(output string) string {
|
||||||
@ -738,6 +766,10 @@ func (p *Plugin) handleWebhook(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if evt.MessageType == "private" {
|
||||||
|
p.startTyping(evt.UserID)
|
||||||
|
}
|
||||||
|
|
||||||
if p.sdk != nil {
|
if p.sdk != nil {
|
||||||
p.sdk.InjectInterruptText(p.name, p.name, interrupt)
|
p.sdk.InjectInterruptText(p.name, p.name, interrupt)
|
||||||
}
|
}
|
||||||
@ -935,6 +967,10 @@ func (p *Plugin) handleChannelOutput(args map[string]interface{}) (interface{},
|
|||||||
return nil, fmt.Errorf("meta 中需要 group_id 或 user_id 字段。用 output_send__qq_help 查看格式说明")
|
return nil, fmt.Errorf("meta 中需要 group_id 或 user_id 字段。用 output_send__qq_help 查看格式说明")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if userID > 0 {
|
||||||
|
p.stopTyping(userID)
|
||||||
|
}
|
||||||
|
|
||||||
switch rawType {
|
switch rawType {
|
||||||
case "text":
|
case "text":
|
||||||
text := p.sensitiveFilter(payload)
|
text := p.sensitiveFilter(payload)
|
||||||
@ -2116,6 +2152,54 @@ func (p *Plugin) napcat(action string, params map[string]interface{}) (interface
|
|||||||
return raw, nil
|
return raw, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Plugin) setInputStatus(userID int64, eventType int) (interface{}, error) {
|
||||||
|
return p.napcat("set_input_status", map[string]interface{}{
|
||||||
|
"user_id": userID,
|
||||||
|
"event_type": eventType,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Plugin) startTyping(userID int64) {
|
||||||
|
p.typingMu.Lock()
|
||||||
|
if _, ok := p.typingMap[userID]; ok {
|
||||||
|
p.typingMu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ts := &typingState{userID: userID, stopCh: make(chan struct{})}
|
||||||
|
p.typingMap[userID] = ts
|
||||||
|
p.typingMu.Unlock()
|
||||||
|
go p.typingLoop(ts)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Plugin) stopTyping(userID int64) {
|
||||||
|
p.typingMu.Lock()
|
||||||
|
ts, ok := p.typingMap[userID]
|
||||||
|
if ok {
|
||||||
|
delete(p.typingMap, userID)
|
||||||
|
}
|
||||||
|
p.typingMu.Unlock()
|
||||||
|
if ok {
|
||||||
|
close(ts.stopCh)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Plugin) typingLoop(ts *typingState) {
|
||||||
|
ticker := time.NewTicker(5 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
timeout := time.After(30 * time.Second)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
p.setInputStatus(ts.userID, 1)
|
||||||
|
case <-ts.stopCh:
|
||||||
|
return
|
||||||
|
case <-timeout:
|
||||||
|
p.stopTyping(ts.userID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ======== Helpers ========
|
// ======== Helpers ========
|
||||||
|
|
||||||
// rawString extracts a string from napcat's return type (json.RawMessage or string).
|
// rawString extracts a string from napcat's return type (json.RawMessage or string).
|
||||||
@ -2171,6 +2255,7 @@ func NewPluginFactory(name string, config map[string]interface{}) (sdk.Plugin, e
|
|||||||
allowFrom: make(map[int64]struct{}),
|
allowFrom: make(map[int64]struct{}),
|
||||||
groupAllowFrom: make(map[int64]struct{}),
|
groupAllowFrom: make(map[int64]struct{}),
|
||||||
downloadTasks: make([]*DownloadTask, 0),
|
downloadTasks: make([]*DownloadTask, 0),
|
||||||
|
typingMap: make(map[int64]*typingState),
|
||||||
dmPolicy: "open",
|
dmPolicy: "open",
|
||||||
groupPolicy: "open",
|
groupPolicy: "open",
|
||||||
}, nil
|
}, nil
|
||||||
|
|||||||
2
go.mod
2
go.mod
@ -1,3 +1,3 @@
|
|||||||
module gitcode.com/JianFeeeee/homeagent-sdk
|
module gitcode.com/JianFeeeee/homeagent-sdk
|
||||||
|
|
||||||
go 1.25.0
|
go 1.21.0
|
||||||
|
|||||||
@ -6,7 +6,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.2"
|
Version = "0.8.0"
|
||||||
|
|
||||||
// Commit 是构建时的 Git commit hash。
|
// Commit 是构建时的 Git commit hash。
|
||||||
Commit = "unknown"
|
Commit = "unknown"
|
||||||
@ -21,7 +21,7 @@ var (
|
|||||||
CoreModule = "gitcode.com/JianFeeeee/HomeAgent"
|
CoreModule = "gitcode.com/JianFeeeee/HomeAgent"
|
||||||
|
|
||||||
// CoreVersion 是此 SDK 所兼容的最低核心版本。
|
// CoreVersion 是此 SDK 所兼容的最低核心版本。
|
||||||
CoreVersion = "0.7.2"
|
CoreVersion = "0.8.0"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FullVersion 返回完整的版本字符串。
|
// FullVersion 返回完整的版本字符串。
|
||||||
|
|||||||
@ -35,6 +35,14 @@ const (
|
|||||||
StageAfterOutput Stage = "after_output"
|
StageAfterOutput Stage = "after_output"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ChannelDef 描述通道在记忆计算层的行为,与 ToolDef.NoMemory/Cleaner 语义一致。
|
||||||
|
// NoMemory: 此通道输入/输出不参与记忆计算(向量化/关键词提取/蒸馏),但原文保留在上下文中
|
||||||
|
// Cleaner: 计算层过滤函数,不改原文;仅在向量化/jieba/蒸馏/存档提取关键词时调用
|
||||||
|
type ChannelDef struct {
|
||||||
|
NoMemory bool
|
||||||
|
Cleaner func(string) string
|
||||||
|
}
|
||||||
|
|
||||||
// StageContext provides context for stage handlers.
|
// StageContext provides context for stage handlers.
|
||||||
type StageContext struct {
|
type StageContext struct {
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
@ -156,8 +164,11 @@ type StageRegistrar func(stage Stage, handler StageHandler)
|
|||||||
// APIRegistrar registers a plugin API for external access.
|
// APIRegistrar registers a plugin API for external access.
|
||||||
type APIRegistrar func(name string) error
|
type APIRegistrar func(name string) error
|
||||||
|
|
||||||
|
// InputChannelRegistrar registers an input channel with its memory behavior.
|
||||||
|
type InputChannelRegistrar func(name string, def ChannelDef) error
|
||||||
|
|
||||||
// OutputChannelRegistrar registers an output channel that the output_send tool can use.
|
// OutputChannelRegistrar registers an output channel that the output_send tool can use.
|
||||||
type OutputChannelRegistrar func(name string, caps int, desc string, handler ToolHandler) error
|
type OutputChannelRegistrar func(name string, caps int, desc string, def ChannelDef, handler ToolHandler) error
|
||||||
|
|
||||||
// Output capability flags
|
// Output capability flags
|
||||||
const (
|
const (
|
||||||
@ -176,6 +187,7 @@ type PluginSDK struct {
|
|||||||
regStage StageRegistrar
|
regStage StageRegistrar
|
||||||
regAPI APIRegistrar
|
regAPI APIRegistrar
|
||||||
regOutput OutputChannelRegistrar
|
regOutput OutputChannelRegistrar
|
||||||
|
regInput InputChannelRegistrar
|
||||||
io IOInjector
|
io IOInjector
|
||||||
mem MemoryAPI
|
mem MemoryAPI
|
||||||
textMem TextMemoryAPI
|
textMem TextMemoryAPI
|
||||||
@ -289,10 +301,21 @@ func (s *PluginSDK) RegisterPluginAPI(name string) error {
|
|||||||
// name: channel name (e.g. "qq", "webui")
|
// name: channel name (e.g. "qq", "webui")
|
||||||
// caps: bitmask of supported output capabilities (CapText, CapFile, etc.)
|
// caps: bitmask of supported output capabilities (CapText, CapFile, etc.)
|
||||||
// desc: description of the channel, expected meta format, and type enum
|
// desc: description of the channel, expected meta format, and type enum
|
||||||
|
// def: 通道在记忆计算层的行为(NoMemory/Cleaner)
|
||||||
// handler: receives args map with keys: payload (string), type (string), meta (string|optional)
|
// handler: receives args map with keys: payload (string), type (string), meta (string|optional)
|
||||||
func (s *PluginSDK) RegisterOutputChannel(name string, caps int, desc string, handler ToolHandler) error {
|
func (s *PluginSDK) RegisterOutputChannel(name string, caps int, desc string, def ChannelDef, handler ToolHandler) error {
|
||||||
if s.regOutput != nil {
|
if s.regOutput != nil {
|
||||||
return s.regOutput(name, caps, desc, handler)
|
return s.regOutput(name, caps, desc, def, handler)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterInputChannel registers an input channel with its memory behavior.
|
||||||
|
// def.NoMemory: 此通道输入不参与记忆计算
|
||||||
|
// def.Cleaner: 计算层对输入文本清洗后(不改原文)再向量化/提关键词
|
||||||
|
func (s *PluginSDK) RegisterInputChannel(name string, def ChannelDef) error {
|
||||||
|
if s.regInput != nil {
|
||||||
|
return s.regInput(name, def)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -300,6 +323,9 @@ func (s *PluginSDK) RegisterOutputChannel(name string, caps int, desc string, ha
|
|||||||
// SetOutputChannelRegistrar sets the output channel registrar (called by the core at startup).
|
// SetOutputChannelRegistrar sets the output channel registrar (called by the core at startup).
|
||||||
func (s *PluginSDK) SetOutputChannelRegistrar(r OutputChannelRegistrar) { s.regOutput = r }
|
func (s *PluginSDK) SetOutputChannelRegistrar(r OutputChannelRegistrar) { s.regOutput = r }
|
||||||
|
|
||||||
|
// SetInputChannelRegistrar sets the input channel registrar (called by the core at startup).
|
||||||
|
func (s *PluginSDK) SetInputChannelRegistrar(r InputChannelRegistrar) { s.regInput = r }
|
||||||
|
|
||||||
// SetIOInjector sets the IO injector (called by the core at startup).
|
// SetIOInjector sets the IO injector (called by the core at startup).
|
||||||
func (s *PluginSDK) SetIOInjector(io IOInjector) { s.io = io }
|
func (s *PluginSDK) SetIOInjector(io IOInjector) { s.io = io }
|
||||||
|
|
||||||
|
|||||||
@ -284,22 +284,29 @@ func ensureGoMod(plg *PlgConfig, sdkPath string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否已有 replace 指令
|
|
||||||
absSDK, _ := filepath.Abs(sdkPath)
|
absSDK, _ := filepath.Abs(sdkPath)
|
||||||
absSDK = strings.ReplaceAll(absSDK, "\\", "/")
|
absSDK = strings.ReplaceAll(absSDK, "\\", "/")
|
||||||
|
|
||||||
|
// Remove any existing replace line for this module (even if path differs)
|
||||||
|
var keep []string
|
||||||
|
replaceLine := fmt.Sprintf("replace %s => %s", sdkModule, absSDK)
|
||||||
|
alreadyExists := false
|
||||||
for _, line := range lines {
|
for _, line := range lines {
|
||||||
if strings.Contains(line, "replace") && strings.Contains(line, sdkModule) {
|
if strings.HasPrefix(strings.TrimSpace(line), "replace ") &&
|
||||||
|
strings.Contains(line, sdkModule) {
|
||||||
parts := strings.Fields(line)
|
parts := strings.Fields(line)
|
||||||
if len(parts) >= 3 && strings.ReplaceAll(parts[2], "\\", "/") == absSDK {
|
if len(parts) >= 3 && strings.ReplaceAll(parts[2], "\\", "/") == absSDK {
|
||||||
return // 已存在且路径正确
|
alreadyExists = true
|
||||||
}
|
}
|
||||||
|
continue // strip any existing replace for this module
|
||||||
}
|
}
|
||||||
|
keep = append(keep, line)
|
||||||
}
|
}
|
||||||
|
if alreadyExists {
|
||||||
// 追加 replace 指令
|
return
|
||||||
replaceLine := fmt.Sprintf("replace %s => %s", sdkModule, absSDK)
|
}
|
||||||
newData := string(data) + "\n" + replaceLine + "\n"
|
keep = append(keep, replaceLine, "")
|
||||||
if err := os.WriteFile(gomodPath, []byte(newData), 0644); err != nil {
|
if err := os.WriteFile(gomodPath, []byte(strings.Join(keep, "\n")), 0644); err != nil {
|
||||||
fmt.Printf(" warn: update go.mod replace: %v\n", err)
|
fmt.Printf(" warn: update go.mod replace: %v\n", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -387,7 +394,7 @@ func buildTarget(plg *PlgConfig, target, outDir, sdkPath string) {
|
|||||||
|
|
||||||
// Auto-generate C ABI bridge (all platforms use c-shared)
|
// Auto-generate C ABI bridge (all platforms use c-shared)
|
||||||
bridgeCleanup := generateBridge(cfg.goos)
|
bridgeCleanup := generateBridge(cfg.goos)
|
||||||
_ = bridgeCleanup // DISABLED cleanup for debug
|
defer bridgeCleanup()
|
||||||
|
|
||||||
// Auto-link thirdpart/ contents + source_dirs + replace targets
|
// Auto-link thirdpart/ contents + source_dirs + replace targets
|
||||||
thirdpartCleanup := linkThirdpart(plg, target)
|
thirdpartCleanup := linkThirdpart(plg, target)
|
||||||
@ -566,33 +573,6 @@ func detectWindowsCC() string {
|
|||||||
|
|
||||||
// stripIncludeGuard strips preprocessor guards and C++ comments from a C header,
|
// stripIncludeGuard strips preprocessor guards and C++ comments from a C header,
|
||||||
// since these can confuse cgo's type resolution.
|
// since these can confuse cgo's type resolution.
|
||||||
func stripIncludeGuard(header string) string {
|
|
||||||
lines := strings.Split(header, "\n")
|
|
||||||
var out []string
|
|
||||||
for _, line := range lines {
|
|
||||||
trimmed := strings.TrimSpace(line)
|
|
||||||
if trimmed == "#ifndef HOMEAGENT_CABI_H" || trimmed == "#define HOMEAGENT_CABI_H" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if trimmed == "#endif" || strings.HasPrefix(trimmed, "#endif") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if trimmed == "#ifdef __cplusplus" || trimmed == "extern \"C\" {" || trimmed == "}" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// Strip C++-style comments (cgo parser may not handle them in /* */ blocks)
|
|
||||||
if idx := strings.Index(line, "//"); idx >= 0 {
|
|
||||||
line = line[:idx]
|
|
||||||
}
|
|
||||||
cleaned := strings.TrimSpace(line)
|
|
||||||
if cleaned == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
out = append(out, line)
|
|
||||||
}
|
|
||||||
return strings.Join(out, "\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
// generateBridge generates the C ABI bridge files for non-Lua builds.
|
// generateBridge generates the C ABI bridge files for non-Lua builds.
|
||||||
// Returns a cleanup function to remove generated files.
|
// Returns a cleanup function to remove generated files.
|
||||||
func generateBridge(goos string) func() {
|
func generateBridge(goos string) func() {
|
||||||
@ -662,7 +642,11 @@ func linkThirdpart(plg *PlgConfig, target string) func() {
|
|||||||
dirs = append(dirs, "thirdpart")
|
dirs = append(dirs, "thirdpart")
|
||||||
}
|
}
|
||||||
dirs = append(dirs, plg.SourceDirs...)
|
dirs = append(dirs, plg.SourceDirs...)
|
||||||
for _, to := range plg.Replaces {
|
for _, r := range plg.ReplacesToSlice() {
|
||||||
|
_, to, found := strings.Cut(r, "=")
|
||||||
|
if !found {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if abs, err := filepath.Abs(to); err == nil {
|
if abs, err := filepath.Abs(to); err == nil {
|
||||||
if info, err := os.Stat(abs); err == nil && info.IsDir() {
|
if info, err := os.Stat(abs); err == nil && info.IsDir() {
|
||||||
dirs = append(dirs, abs)
|
dirs = append(dirs, abs)
|
||||||
@ -699,25 +683,21 @@ func linkThirdpart(plg *PlgConfig, target string) func() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine import path: for relative dirs under module, use module path prefix;
|
|
||||||
// for absolute paths, derive from replace or use package name
|
|
||||||
dirName := filepath.Base(d)
|
|
||||||
if !filepath.IsAbs(d) {
|
if !filepath.IsAbs(d) {
|
||||||
importPath := modulePath + "/" + d
|
importPath := modulePath + "/" + d
|
||||||
stubs = append(stubs, importPath)
|
stubs = append(stubs, importPath)
|
||||||
} else {
|
} else {
|
||||||
// External directory: use the replace "from" key if found, else use dir name
|
// External directory: must be in replaces to get a valid import path
|
||||||
found := false
|
for _, r := range plg.ReplacesToSlice() {
|
||||||
for from, to := range plg.Replaces {
|
from, to, found := strings.Cut(r, "=")
|
||||||
|
if !found {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if absTo, _ := filepath.Abs(to); absTo == d {
|
if absTo, _ := filepath.Abs(to); absTo == d {
|
||||||
stubs = append(stubs, from)
|
stubs = append(stubs, strings.TrimSpace(from))
|
||||||
found = true
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !found && dirName != "" {
|
|
||||||
stubs = append(stubs, modulePath+"/"+dirName)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -153,7 +153,7 @@ func debugLua(dir, sdkPath, luaPath string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func debugGo(dir string, replaces []string) {
|
func debugGo(dir string, replaces []string) {
|
||||||
debug, err := yaegi.NewGoPluginDebug(dir, replaces)
|
debug, err := yaegi.NewYaegiDebugger(dir, replaces)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("error: %v\n", err)
|
fmt.Printf("error: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@ -161,33 +161,14 @@ func debugGo(dir string, replaces []string) {
|
|||||||
|
|
||||||
fmt.Printf("[debug] Plugin dir: %s\n", dir)
|
fmt.Printf("[debug] Plugin dir: %s\n", dir)
|
||||||
|
|
||||||
// Clean any stale debug harness
|
if err := debug.LoadPlugin(); err != nil {
|
||||||
debug.Cleanup()
|
fmt.Printf("[debug] load plugin: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
// Try Yaegi interpreter first (fast, no compilation)
|
if err := debug.StartREPL(); err != nil {
|
||||||
if err := debug.DebugWithYaegi(); err != nil {
|
fmt.Printf("[debug] repl error: %v\n", err)
|
||||||
// Fall back to go run with generated debug harness
|
os.Exit(1)
|
||||||
// Apply third-party replace directives before go run
|
|
||||||
patcher := NewGoModPatcher(dir, replaces)
|
|
||||||
restore, pErr := patcher.Apply()
|
|
||||||
if pErr != nil {
|
|
||||||
fmt.Printf("[debug] warn: apply replaces: %v\n", pErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, genErr := debug.GenerateDebugMain(); genErr != nil {
|
|
||||||
restore()
|
|
||||||
fmt.Printf("[debug] generate fallback: %v\n", genErr)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
runErr := debug.DebugWithGoRun()
|
|
||||||
debug.Cleanup()
|
|
||||||
restore()
|
|
||||||
|
|
||||||
if runErr != nil {
|
|
||||||
fmt.Printf("[debug] go run failed: %v\n", runErr)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const sdkDirName = "plugindev/sdk"
|
const sdkDirName = "plugindev/sdk"
|
||||||
@ -165,7 +166,8 @@ func cmdSDKInstall(version string) {
|
|||||||
tmpPath := tmpFile.Name()
|
tmpPath := tmpFile.Name()
|
||||||
defer os.Remove(tmpPath)
|
defer os.Remove(tmpPath)
|
||||||
|
|
||||||
resp, err := http.Get(url)
|
client := &http.Client{Timeout: 30 * time.Second}
|
||||||
|
resp, err := client.Get(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("error: download SDK %s: %v\n", version, err)
|
fmt.Printf("error: download SDK %s: %v\n", version, err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@ -191,12 +193,19 @@ func cmdSDKInstall(version string) {
|
|||||||
}
|
}
|
||||||
defer os.RemoveAll(tmpDir)
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
gzr, err := gzip.NewReader(openFile(tmpPath))
|
f, err := openFile(tmpPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
fmt.Printf("error: open archive: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
gzr, err := gzip.NewReader(f)
|
||||||
|
if err != nil {
|
||||||
|
f.Close()
|
||||||
fmt.Printf("error: read archive: %v\n", err)
|
fmt.Printf("error: read archive: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
defer gzr.Close()
|
defer gzr.Close()
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
tr := tar.NewReader(gzr)
|
tr := tar.NewReader(gzr)
|
||||||
for {
|
for {
|
||||||
@ -241,8 +250,12 @@ func cmdSDKInstall(version string) {
|
|||||||
gzr.Close()
|
gzr.Close()
|
||||||
|
|
||||||
if err := os.Rename(tmpDir, dest); err != nil {
|
if err := os.Rename(tmpDir, dest); err != nil {
|
||||||
fmt.Printf("error: move SDK to store: %v\n", err)
|
// Cross-filesystem rename fallback
|
||||||
os.Exit(1)
|
if err := copyDir(tmpDir, dest); err != nil {
|
||||||
|
fmt.Printf("error: move SDK to store: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
os.RemoveAll(tmpDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("SDK version %s installed at %s\n", version, dest)
|
fmt.Printf("SDK version %s installed at %s\n", version, dest)
|
||||||
@ -253,12 +266,12 @@ func cmdSDKInstall(version string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func openFile(path string) *os.File {
|
func openFile(path string) (*os.File, error) {
|
||||||
f, err := os.Open(path)
|
f, err := os.Open(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
return f
|
return f, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// cmdSDKUse switches the active SDK version.
|
// cmdSDKUse switches the active SDK version.
|
||||||
@ -396,17 +409,19 @@ func compareSemver(a, b string) int {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseSemver extracts [major, minor, patch] from a vX.Y.Z string.
|
// parseSemver extracts [major, minor, patch] from a vX.Y.Z[-pre] string.
|
||||||
|
// Prerelease tags parse to the same major.minor.patch as their release (ignoring prerelease).
|
||||||
func parseSemver(tag string) [3]int {
|
func parseSemver(tag string) [3]int {
|
||||||
var v [3]int
|
var v [3]int
|
||||||
s := strings.TrimPrefix(tag, "v")
|
s := strings.TrimPrefix(tag, "v")
|
||||||
|
// Strip prerelease suffix (-...)
|
||||||
|
if idx := strings.IndexByte(s, '-'); idx >= 0 {
|
||||||
|
s = s[:idx]
|
||||||
|
}
|
||||||
parts := strings.SplitN(s, ".", 3)
|
parts := strings.SplitN(s, ".", 3)
|
||||||
for i, p := range parts {
|
for i := 0; i < 3 && i < len(parts); i++ {
|
||||||
if i >= 3 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
n := 0
|
n := 0
|
||||||
fmt.Sscanf(p, "%d", &n)
|
fmt.Sscanf(parts[i], "%d", &n)
|
||||||
v[i] = n
|
v[i] = n
|
||||||
}
|
}
|
||||||
return v
|
return v
|
||||||
@ -433,6 +448,35 @@ func activeSDKRoot() string {
|
|||||||
return root
|
return root
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// copyDir recursively copies src to dst (cross-filesystem rename fallback).
|
||||||
|
func copyDir(src, dst string) error {
|
||||||
|
if err := os.MkdirAll(dst, 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
entries, err := os.ReadDir(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, e := range entries {
|
||||||
|
srcPath := filepath.Join(src, e.Name())
|
||||||
|
dstPath := filepath.Join(dst, e.Name())
|
||||||
|
if e.IsDir() {
|
||||||
|
if err := copyDir(srcPath, dstPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
data, err := os.ReadFile(srcPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(dstPath, data, 0644); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// readMetaVersion reads the Version string from the SDK's meta/meta.go.
|
// readMetaVersion reads the Version string from the SDK's meta/meta.go.
|
||||||
// If the file is missing or unreadable, returns "0.0.0".
|
// If the file is missing or unreadable, returns "0.0.0".
|
||||||
func readMetaVersion(sdkRoot string) string {
|
func readMetaVersion(sdkRoot string) string {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
module github.com/JianFeeeee/homeagent-sdk/tools/plugindev
|
module github.com/JianFeeeee/homeagent-sdk/tools/plugindev
|
||||||
|
|
||||||
go 1.25.0
|
go 1.21.0
|
||||||
|
|
||||||
require gitcode.com/JianFeeeee/homeagent-sdk v0.0.0
|
require gitcode.com/JianFeeeee/homeagent-sdk v0.0.0
|
||||||
|
|
||||||
|
|||||||
@ -30,7 +30,7 @@ func (p *GoModPatcher) Apply() (func(), error) {
|
|||||||
p.backup = string(data)
|
p.backup = string(data)
|
||||||
|
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
sb.WriteString(strings.TrimRight(string(data), "\n"))
|
sb.WriteString(strings.TrimRight(string(data), "\r\n"))
|
||||||
sb.WriteString("\n")
|
sb.WriteString("\n")
|
||||||
for _, r := range p.replaces {
|
for _, r := range p.replaces {
|
||||||
from, to, found := strings.Cut(r, "=")
|
from, to, found := strings.Cut(r, "=")
|
||||||
|
|||||||
@ -74,13 +74,55 @@ const tmplSDKLua = `-- HomeAgent Lua Plugin SDK (standalone mock)
|
|||||||
sdk = {}
|
sdk = {}
|
||||||
function sdk.log(level, msg) print("[lua-plugin] " .. tostring(level) .. ": " .. tostring(msg)) end
|
function sdk.log(level, msg) print("[lua-plugin] " .. tostring(level) .. ": " .. tostring(msg)) end
|
||||||
function sdk.register_tool(name, def, handler) print("[lua-plugin] register_tool: " .. tostring(name)) end
|
function sdk.register_tool(name, def, handler) print("[lua-plugin] register_tool: " .. tostring(name)) end
|
||||||
function sdk.register_stage(stage, handler) print("[lua-plugin] register_stage: " .. tostring(stage)) end
|
function sdk.register_stage(stage, handler, scope) print("[lua-plugin] register_stage: " .. tostring(stage) .. " scope=" .. tostring(scope)) end
|
||||||
function sdk.register_api(name) print("[lua-plugin] register_api: " .. tostring(name)) end
|
function sdk.register_api(name) print("[lua-plugin] register_api: " .. tostring(name)) end
|
||||||
|
function sdk.register_output_channel(name, caps, desc, def, handler) print("[lua-plugin] register_output_channel: " .. tostring(name)) end
|
||||||
|
function sdk.register_input_channel(name, def) print("[lua-plugin] register_input_channel: " .. tostring(name)) end
|
||||||
function sdk.get_setting(key) return nil end
|
function sdk.get_setting(key) return nil end
|
||||||
function sdk.set_setting(key, value) print("[lua-plugin] set_setting: " .. tostring(key)) end
|
function sdk.set_setting(key, value) print("[lua-plugin] set_setting: " .. tostring(key)) end
|
||||||
function sdk.inject_text(source, channel, text) print("[lua-plugin] inject_text: " .. tostring(source)) end
|
function sdk.inject_text(source, channel, text) print("[lua-plugin] inject_text: " .. tostring(source)) end
|
||||||
function sdk.inject_interrupt(source, channel, text) print("[lua-plugin] inject_interrupt: " .. tostring(source)) end
|
function sdk.inject_interrupt(source, channel, text) print("[lua-plugin] inject_interrupt: " .. tostring(source)) end
|
||||||
function sdk.inject_text_no_memory(source, channel, text) print("[lua-plugin] inject_text_no_memory: " .. tostring(source)) end
|
function sdk.inject_text_no_memory(source, channel, text) print("[lua-plugin] inject_text_no_memory: " .. tostring(source)) end
|
||||||
|
function sdk.set_auto_restart(enabled) print("[lua-plugin] set_auto_restart: " .. tostring(enabled)) end
|
||||||
|
sdk.memory = {}
|
||||||
|
function sdk.memory.recall(query, depth) return {entities={}, relations={}} end
|
||||||
|
function sdk.memory.commit(triples) return nil end
|
||||||
|
function sdk.memory.introspect() return {} end
|
||||||
|
function sdk.memory.merge(source, target) return 0 end
|
||||||
|
function sdk.memory.purge(criteria, hard) return 0 end
|
||||||
|
sdk.doc = {}
|
||||||
|
function sdk.doc.query(text, top_k) return {} end
|
||||||
|
function sdk.doc.insert(doc) return nil end
|
||||||
|
function sdk.doc.remove(id) return nil end
|
||||||
|
function sdk.doc.stats() return {} end
|
||||||
|
sdk.knowledge = {}
|
||||||
|
function sdk.knowledge.search(query, limit) return {} end
|
||||||
|
function sdk.knowledge.add(tag, content) return nil end
|
||||||
|
function sdk.knowledge.list() return {} end
|
||||||
|
sdk.text_memory = {}
|
||||||
|
function sdk.text_memory.append(evt) return nil end
|
||||||
|
sdk.llm = {}
|
||||||
|
function sdk.llm.list_sources() return {} end
|
||||||
|
function sdk.llm.set_source(name) return nil end
|
||||||
|
function sdk.llm.current_source() return nil end
|
||||||
|
sdk.social = {}
|
||||||
|
function sdk.social.get_person(name) return {} end
|
||||||
|
function sdk.social.get_network(name, depth) return {} end
|
||||||
|
function sdk.social.get_trait(name, trait) return {value=nil, found=false} end
|
||||||
|
function sdk.social.get_relations(name) return {} end
|
||||||
|
function sdk.social.list_persons() return {} end
|
||||||
|
sdk.settings = {}
|
||||||
|
function sdk.settings.get_core(key) return nil end
|
||||||
|
function sdk.settings.set_core(key, value) return nil end
|
||||||
|
function sdk.settings.list_core(prefix) return {} end
|
||||||
|
function sdk.settings.get_plugin(plugin, key) return nil end
|
||||||
|
function sdk.settings.set_plugin(plugin, key, value) return nil end
|
||||||
|
function sdk.settings.list_plugin(plugin, prefix) return {} end
|
||||||
|
function sdk.settings.list(prefix) return {} end
|
||||||
|
function sdk.settings.register_def(def) return nil end
|
||||||
|
function sdk.settings.defs(prefix) return {} end
|
||||||
|
function sdk.settings.dump() return {} end
|
||||||
|
function sdk.settings.plugins() return {} end
|
||||||
sdk.json = {}
|
sdk.json = {}
|
||||||
function sdk.json.encode(val)
|
function sdk.json.encode(val)
|
||||||
if type(val) == "string" then return '"' .. val:gsub('"', '\\"'):gsub('\n', '\\n') .. '"'
|
if type(val) == "string" then return '"' .. val:gsub('"', '\\"'):gsub('\n', '\\n') .. '"'
|
||||||
@ -179,8 +221,9 @@ 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 },
|
func(name string, caps int, desc string, def sdk.ChannelDef, handler sdk.ToolHandler) error { return nil },
|
||||||
)
|
)
|
||||||
|
mockSDK.SetInputChannelRegistrar(func(name string, def sdk.ChannelDef) 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
|
||||||
}
|
}
|
||||||
@ -356,6 +399,7 @@ enum {
|
|||||||
CORE_SETTINGS_DEFS = 43,
|
CORE_SETTINGS_DEFS = 43,
|
||||||
CORE_SETTINGS_DUMP = 44,
|
CORE_SETTINGS_DUMP = 44,
|
||||||
CORE_SETTINGS_PLUGINS = 45,
|
CORE_SETTINGS_PLUGINS = 45,
|
||||||
|
CORE_REGISTER_INPUT_CH = 46,
|
||||||
};
|
};
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
@ -390,6 +434,7 @@ var (
|
|||||||
coreAPI unsafe.Pointer
|
coreAPI unsafe.Pointer
|
||||||
|
|
||||||
handlerMu sync.RWMutex
|
handlerMu sync.RWMutex
|
||||||
|
coreAPIMu sync.RWMutex
|
||||||
toolHandlers = map[string]sdk.ToolHandler{}
|
toolHandlers = map[string]sdk.ToolHandler{}
|
||||||
stageHandlers = map[string]sdk.StageHandler{}
|
stageHandlers = map[string]sdk.StageHandler{}
|
||||||
outputHandlers = map[string]sdk.ToolHandler{}
|
outputHandlers = map[string]sdk.ToolHandler{}
|
||||||
@ -398,29 +443,35 @@ var (
|
|||||||
// ---- CoreAPI dispatch helpers ----
|
// ---- CoreAPI dispatch helpers ----
|
||||||
|
|
||||||
func callVoid(methodID int, s1, s2, s3 string, i1, i2 int) error {
|
func callVoid(methodID int, s1, s2, s3 string, i1, i2 int) error {
|
||||||
|
coreAPIMu.RLock()
|
||||||
|
api := coreAPI
|
||||||
|
coreAPIMu.RUnlock()
|
||||||
var c1, c2, c3 *C.char
|
var c1, c2, c3 *C.char
|
||||||
if s1 != "" { c1 = C.CString(s1); defer C.free(unsafe.Pointer(c1)) }
|
if s1 != "" { c1 = C.CString(s1); defer C.free(unsafe.Pointer(c1)) }
|
||||||
if s2 != "" { c2 = C.CString(s2); defer C.free(unsafe.Pointer(c2)) }
|
if s2 != "" { c2 = C.CString(s2); defer C.free(unsafe.Pointer(c2)) }
|
||||||
if s3 != "" { c3 = C.CString(s3); defer C.free(unsafe.Pointer(c3)) }
|
if s3 != "" { c3 = C.CString(s3); defer C.free(unsafe.Pointer(c3)) }
|
||||||
var cErr *C.char
|
var cErr *C.char
|
||||||
if C.ha_dispatch(C.int(methodID), coreAPI, c1, c2, c3, C.int(i1), C.int(i2), nil, &cErr) != 0 && cErr != nil {
|
if C.ha_dispatch(C.int(methodID), api, c1, c2, c3, C.int(i1), C.int(i2), nil, &cErr) != 0 && cErr != nil {
|
||||||
return fmt.Errorf("%s", C.GoString(cErr))
|
return fmt.Errorf("%s", C.GoString(cErr))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func callString(methodID int, s1, s2, s3 string, i1, i2 int) (string, error) {
|
func callString(methodID int, s1, s2, s3 string, i1, i2 int) (string, error) {
|
||||||
|
coreAPIMu.RLock()
|
||||||
|
api := coreAPI
|
||||||
|
coreAPIMu.RUnlock()
|
||||||
var c1, c2, c3 *C.char
|
var c1, c2, c3 *C.char
|
||||||
if s1 != "" { c1 = C.CString(s1); defer C.free(unsafe.Pointer(c1)) }
|
if s1 != "" { c1 = C.CString(s1); defer C.free(unsafe.Pointer(c1)) }
|
||||||
if s2 != "" { c2 = C.CString(s2); defer C.free(unsafe.Pointer(c2)) }
|
if s2 != "" { c2 = C.CString(s2); defer C.free(unsafe.Pointer(c2)) }
|
||||||
if s3 != "" { c3 = C.CString(s3); defer C.free(unsafe.Pointer(c3)) }
|
if s3 != "" { c3 = C.CString(s3); defer C.free(unsafe.Pointer(c3)) }
|
||||||
var strResult, cErr *C.char
|
var strResult, cErr *C.char
|
||||||
if C.ha_dispatch(C.int(methodID), coreAPI, c1, c2, c3, C.int(i1), C.int(i2), &strResult, &cErr) != 0 && cErr != nil {
|
if C.ha_dispatch(C.int(methodID), api, c1, c2, c3, C.int(i1), C.int(i2), &strResult, &cErr) != 0 && cErr != nil {
|
||||||
return "", fmt.Errorf("%s", C.GoString(cErr))
|
return "", fmt.Errorf("%s", C.GoString(cErr))
|
||||||
}
|
}
|
||||||
if strResult != nil {
|
if strResult != nil {
|
||||||
result := C.GoString(strResult)
|
result := C.GoString(strResult)
|
||||||
C.ha_dispatch(C.int(25), coreAPI, strResult, nil, nil, 0, 0, nil, nil)
|
C.ha_dispatch(C.int(25), api, strResult, nil, nil, 0, 0, nil, nil)
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
return "", nil
|
return "", nil
|
||||||
@ -447,11 +498,12 @@ func buildPluginSDK(name string) *sdk.PluginSDK {
|
|||||||
callVoid(2, string(stage), "", "", 0, 0)
|
callVoid(2, string(stage), "", "", 0, 0)
|
||||||
},
|
},
|
||||||
func(name string) error { return callVoid(4, name, "", "", 0, 0) },
|
func(name string) error { return callVoid(4, name, "", "", 0, 0) },
|
||||||
func(name string, caps int, desc string, handler sdk.ToolHandler) error {
|
func(name string, caps int, desc string, def sdk.ChannelDef, handler sdk.ToolHandler) error {
|
||||||
handlerMu.Lock()
|
handlerMu.Lock()
|
||||||
outputHandlers[name] = handler
|
outputHandlers[name] = handler
|
||||||
handlerMu.Unlock()
|
handlerMu.Unlock()
|
||||||
return callVoid(3, name, desc, "", caps, 0)
|
defJSON, _ := json.Marshal(def)
|
||||||
|
return callVoid(3, name, desc, string(defJSON), caps, 0)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
base.SetIOInjector(dispatchIO{})
|
base.SetIOInjector(dispatchIO{})
|
||||||
@ -461,6 +513,12 @@ func buildPluginSDK(name string) *sdk.PluginSDK {
|
|||||||
base.SetLLMAPI(dispatchLLM{})
|
base.SetLLMAPI(dispatchLLM{})
|
||||||
base.SetSocialAPI(dispatchSocial{})
|
base.SetSocialAPI(dispatchSocial{})
|
||||||
base.SetTextMemoryAPI(dispatchTextMemory{})
|
base.SetTextMemoryAPI(dispatchTextMemory{})
|
||||||
|
base.SetInputChannelRegistrar(
|
||||||
|
func(name string, def sdk.ChannelDef) error {
|
||||||
|
defJSON, _ := json.Marshal(def)
|
||||||
|
return callVoid(46, name, string(defJSON), "", 0, 0)
|
||||||
|
},
|
||||||
|
)
|
||||||
return base
|
return base
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -571,7 +629,9 @@ func go_init_plugin(name *C.char, configJSON *C.char, errorOut **C.char) C.int {
|
|||||||
func go_start_plugin(coreAPIptr unsafe.Pointer, coreVersion C.int, errorOut **C.char) C.int {
|
func go_start_plugin(coreAPIptr unsafe.Pointer, coreVersion C.int, errorOut **C.char) C.int {
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
plg := currentPlg
|
plg := currentPlg
|
||||||
|
coreAPIMu.Lock()
|
||||||
coreAPI = coreAPIptr
|
coreAPI = coreAPIptr
|
||||||
|
coreAPIMu.Unlock()
|
||||||
mu.Unlock()
|
mu.Unlock()
|
||||||
_ = coreVersion
|
_ = coreVersion
|
||||||
if plg == nil { *errorOut = C.CString("not initialized"); return 1 }
|
if plg == nil { *errorOut = C.CString("not initialized"); return 1 }
|
||||||
@ -585,7 +645,9 @@ func go_stop_plugin(errorOut **C.char) C.int {
|
|||||||
mu.Lock()
|
mu.Lock()
|
||||||
plg := currentPlg
|
plg := currentPlg
|
||||||
currentPlg = nil
|
currentPlg = nil
|
||||||
|
coreAPIMu.Lock()
|
||||||
coreAPI = nil
|
coreAPI = nil
|
||||||
|
coreAPIMu.Unlock()
|
||||||
mu.Unlock()
|
mu.Unlock()
|
||||||
if plg != nil {
|
if plg != nil {
|
||||||
if err := plg.Stop(); err != nil { *errorOut = C.CString(err.Error()); return 1 }
|
if err := plg.Stop(); err != nil { *errorOut = C.CString(err.Error()); return 1 }
|
||||||
|
|||||||
@ -345,23 +345,23 @@ func New(name string) *PluginSDK {
|
|||||||
|
|
||||||
func (s *PluginSDK) RegisterTool(name string, def ToolDef, handler ToolHandler) {
|
func (s *PluginSDK) RegisterTool(name string, def ToolDef, handler ToolHandler) {
|
||||||
logf("register_tool: %s", name)
|
logf("register_tool: %s", name)
|
||||||
mu.Lock()
|
s.mu.Lock()
|
||||||
defer mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
s.toolDefs[name] = def
|
s.toolDefs[name] = def
|
||||||
s.toolHandlers[name] = handler
|
s.toolHandlers[name] = handler
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PluginSDK) RegisterStage(stage Stage, handler StageHandler) {
|
func (s *PluginSDK) RegisterStage(stage Stage, handler StageHandler) {
|
||||||
logf("register_stage: %s", string(stage))
|
logf("register_stage: %s", string(stage))
|
||||||
mu.Lock()
|
s.mu.Lock()
|
||||||
defer mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
s.stageHandlers[string(stage)] = handler
|
s.stageHandlers[string(stage)] = handler
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PluginSDK) RegisterOutputChannel(name string, caps int, desc string, handler ToolHandler) {
|
func (s *PluginSDK) RegisterOutputChannel(name string, caps int, desc string, handler ToolHandler) {
|
||||||
logf("register_output_channel: %s", name)
|
logf("register_output_channel: %s", name)
|
||||||
mu.Lock()
|
s.mu.Lock()
|
||||||
defer mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
s.outChannels[name] = handler
|
s.outChannels[name] = handler
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -370,9 +370,9 @@ func (s *PluginSDK) RegisterPluginAPI(name string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *PluginSDK) CallTool(name string, args map[string]interface{}) (interface{}, error) {
|
func (s *PluginSDK) CallTool(name string, args map[string]interface{}) (interface{}, error) {
|
||||||
mu.Lock()
|
s.mu.RLock()
|
||||||
handler, ok := s.toolHandlers[name]
|
handler, ok := s.toolHandlers[name]
|
||||||
mu.Unlock()
|
s.mu.RUnlock()
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("tool not found: %s", name)
|
return nil, fmt.Errorf("tool not found: %s", name)
|
||||||
}
|
}
|
||||||
@ -380,9 +380,9 @@ func (s *PluginSDK) CallTool(name string, args map[string]interface{}) (interfac
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *PluginSDK) CallStage(stage string, ctx *StageContext) error {
|
func (s *PluginSDK) CallStage(stage string, ctx *StageContext) error {
|
||||||
mu.Lock()
|
s.mu.RLock()
|
||||||
handler, ok := s.stageHandlers[stage]
|
handler, ok := s.stageHandlers[stage]
|
||||||
mu.Unlock()
|
s.mu.RUnlock()
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -390,8 +390,8 @@ func (s *PluginSDK) CallStage(stage string, ctx *StageContext) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *PluginSDK) ListTools() []ToolDef {
|
func (s *PluginSDK) ListTools() []ToolDef {
|
||||||
mu.Lock()
|
s.mu.RLock()
|
||||||
defer mu.Unlock()
|
defer s.mu.RUnlock()
|
||||||
defs := make([]ToolDef, 0, len(s.toolDefs))
|
defs := make([]ToolDef, 0, len(s.toolDefs))
|
||||||
for _, def := range s.toolDefs {
|
for _, def := range s.toolDefs {
|
||||||
defs = append(defs, def)
|
defs = append(defs, def)
|
||||||
|
|||||||
Reference in New Issue
Block a user