mirror of
https://gitcode.com/JianFeeeee/homeagent-sdk.git
synced 2026-09-21 17:38:03 +00:00
Compare commits
5 Commits
v1.3.0
...
release/v1
| Author | SHA1 | Date | |
|---|---|---|---|
| 5d14afa8d6 | |||
| 87241bc4ff | |||
| 535922c6a0 | |||
| fe66f72b9c | |||
| 0a4eb76542 |
52
README.md
52
README.md
@ -4,7 +4,7 @@ HomeAgent 插件开发 SDK,用于构建与 HomeAgent 平台交互的智能插
|
|||||||
|
|
||||||
## 版本与兼容性
|
## 版本与兼容性
|
||||||
|
|
||||||
当前:**SDK 1.2.0**(需内核 **1.2.0+**)。
|
当前:**SDK 1.3.0**(需内核 **1.3.0+**)。
|
||||||
|
|
||||||
**版本号跟随内核的中版本,patch 位恒为 `.0`**:
|
**版本号跟随内核的中版本,patch 位恒为 `.0`**:
|
||||||
|
|
||||||
@ -12,11 +12,15 @@ HomeAgent 插件开发 SDK,用于构建与 HomeAgent 平台交互的智能插
|
|||||||
|---|---|
|
|---|---|
|
||||||
| 1.0.0 / 1.0.1 / … / 1.0.4 | 1.0.0 |
|
| 1.0.0 / 1.0.1 / … / 1.0.4 | 1.0.0 |
|
||||||
| 1.1.0 / 1.1.1 / … / 1.1.N | **1.1.0** |
|
| 1.1.0 / 1.1.1 / … / 1.1.N | **1.1.0** |
|
||||||
| 1.2.0 起 | 1.2.0 |
|
| 1.2.0 / 1.2.1 / … / 1.2.N | **1.2.0** |
|
||||||
|
| 1.3.0 起 | **1.3.0** |
|
||||||
|
|
||||||
内核的 patch 位专用于 bugfix 与漏洞修复,不碰公开接口,所以 SDK 版本号不跟着动——
|
内核的 patch 位专用于 bugfix 与漏洞修复,不碰公开接口,所以 SDK 版本号不跟着动——
|
||||||
否则你要么被迫跟版、要么怀疑自己版本过时,而接口其实一个字都没变。
|
否则你要么被迫跟版、要么怀疑自己版本过时,而接口其实一个字都没变。
|
||||||
|
|
||||||
|
因此 **SDK 仓在一个中版本里只发一次**(`vX.Y.0`),核心的 `v1.3.1`/`v1.3.2`/… 不伴随 SDK 发版。
|
||||||
|
(2026-09-13 曾误发过 `v1.3.1`,已撤回 —— patch 位带非零数字的 SDK tag 都是错误的。)
|
||||||
|
|
||||||
**1.0.x 插件升到 1.1.x:不需要改代码,也不需要重编。** 1.1.0 的新增全部是
|
**1.0.x 插件升到 1.1.x:不需要改代码,也不需要重编。** 1.1.0 的新增全部是
|
||||||
「插件调用、内核实现」方向,不调就不受影响(已用 SDK 0.9.2 编的旧 `plugin.bin`
|
「插件调用、内核实现」方向,不调就不受影响(已用 SDK 0.9.2 编的旧 `plugin.bin`
|
||||||
实测验证:在新内核上直接建链通过,因为握手校验的是 `ProtocolVersion`、不是 SDK 版本)。
|
实测验证:在新内核上直接建链通过,因为握手校验的是 `ProtocolVersion`、不是 SDK 版本)。
|
||||||
@ -28,6 +32,40 @@ HomeAgent 插件开发 SDK,用于构建与 HomeAgent 平台交互的智能插
|
|||||||
所以 `plugin.bin` 必须用配套的 `hmapdev` 重编后与内核**同批**安装——否则握手时协议版本
|
所以 `plugin.bin` 必须用配套的 `hmapdev` 重编后与内核**同批**安装——否则握手时协议版本
|
||||||
不匹配会被拒绝(错误信息会明确提示用配套 hmapdev 重编,不会静默降级)。
|
不匹配会被拒绝(错误信息会明确提示用配套 hmapdev 重编,不会静默降级)。
|
||||||
|
|
||||||
|
## 1.3.0 新增:注入优先级与动态输出通道
|
||||||
|
|
||||||
|
### 注入优先级(`InjectOptions.Priority`)
|
||||||
|
|
||||||
|
插件可以声明**自己这次注入的中断级别**,内核按四级阶梯调度:
|
||||||
|
|
||||||
|
| 级别 | 常量 | 谁用 |
|
||||||
|
|---|---|---|
|
||||||
|
| L1–L3 | `PriorityL1` / `PriorityL2` / `PriorityL3` | 插件按紧急程度自选(L1 最低) |
|
||||||
|
| L4 | `PriorityL4` | **保留给内核与内核级插件**(内核自身事件、内核级通道) |
|
||||||
|
|
||||||
|
- 零值(不声明)与旧的注入调用**完全等价**:按排队处理,不抢占任何正在执行的回合
|
||||||
|
⇒ 存量插件不需要改一行、也不需要重编。
|
||||||
|
- 高优先级中断可以**抢占**低优先级正在跑的回合;被抢占的回合挂起、之后恢复继续
|
||||||
|
(现场保存/恢复对插件透明)。
|
||||||
|
- 排队输入**没有级别**:排队就是排队,任何中断都能插到它前面。
|
||||||
|
|
||||||
|
### 动态输出通道(`UnregisterOutputChannel`)
|
||||||
|
|
||||||
|
`RegisterOutputChannel` 注册的通道此前只增不减。对**随资源生灭**的通道(典型:远程设备
|
||||||
|
一台设备一个输出通道),设备掉线后通道还在,模型会继续对一个死通道发消息并以为发成功了。
|
||||||
|
|
||||||
|
1.3.0 起成对提供:
|
||||||
|
|
||||||
|
| API | 用途 |
|
||||||
|
|---|---|
|
||||||
|
| `UnregisterOutputChannel(name)` | 注销输出通道(含能力表与工具) |
|
||||||
|
| `OutputChannelUnregistrar` / `SetOutputChannelUnregistrar` | 插件侧拿到注销句柄(内核注入) |
|
||||||
|
|
||||||
|
⚠️ 通道名要**由插件派生得又合法又唯一**(外部 id 不能直接当通道名)——
|
||||||
|
设备 id 这类外部输入可能带 `/` 等字符,而通道名会拼进 LLM 函数名 `output_send__<name>`,
|
||||||
|
违规会让**整条 LLM 请求**被上游拒绝(2026-09-13 生产事故:`device/<id>` 导致全量对话 403)。
|
||||||
|
派生规则与约束见下方「输出通道」一节。
|
||||||
|
|
||||||
## 注入行为与上下文裁剪(1.2.0)
|
## 注入行为与上下文裁剪(1.2.0)
|
||||||
|
|
||||||
「记不记入记忆」与「要不要据此裁剪上下文」这两件事,原先只有 `ToolDef` 能声明;
|
「记不记入记忆」与「要不要据此裁剪上下文」这两件事,原先只有 `ToolDef` 能声明;
|
||||||
@ -92,7 +130,7 @@ type Plugin interface {
|
|||||||
|------|------|------|
|
|------|------|------|
|
||||||
| 阶段钩子 | `RegisterStage(stage, handler, scope...)` | 注册阶段回调,scope 可选:`StageScopeGlobal`(全局,默认)或 `StageScopeOwnTools`(仅自己工具) |
|
| 阶段钩子 | `RegisterStage(stage, handler, scope...)` | 注册阶段回调,scope 可选:`StageScopeGlobal`(全局,默认)或 `StageScopeOwnTools`(仅自己工具) |
|
||||||
| 输入通道 | `RegisterInputChannel(name, def)` | 注册输入通道(**入站**:谁会往这个通道注入输入),def 为 `ChannelDef`(NoMemory/Cleaner) |
|
| 输入通道 | `RegisterInputChannel(name, def)` | 注册输入通道(**入站**:谁会往这个通道注入输入),def 为 `ChannelDef`(NoMemory/Cleaner) |
|
||||||
| 输出通道 | `RegisterOutputChannel(name, caps, desc, def, handler)` | 注册输出通道(**出站**:`output_send__<name>` 的回复发给谁),def 为 `ChannelDef`,caps 为能力位掩码 |
|
| 输出通道 | `RegisterOutputChannel(name, caps, desc, def, handler)` | 注册输出通道(**出站**:`output_send__<name>` 的回复发给谁),def 为 `ChannelDef`,caps 为能力位掩码。⚠️ 通道名只能用 `[A-Za-z0-9_-]`(见下方"输出通道"一节的命名约束) |
|
||||||
| 工具注册 | `RegisterTool(name, def, handler)` | 注册工具供 LLM 调用 |
|
| 工具注册 | `RegisterTool(name, def, handler)` | 注册工具供 LLM 调用 |
|
||||||
| 插件 API | `RegisterPluginAPI(name)` | 注册插件 API 供其他插件访问 |
|
| 插件 API | `RegisterPluginAPI(name)` | 注册插件 API 供其他插件访问 |
|
||||||
| 图记忆 | `Memory()` | 访问图记忆 API(实体-关系存储) |
|
| 图记忆 | `Memory()` | 访问图记忆 API(实体-关系存储) |
|
||||||
@ -139,6 +177,14 @@ sdk.RegisterInputChannel("qq", ChannelDef{
|
|||||||
|
|
||||||
### 输出通道
|
### 输出通道
|
||||||
|
|
||||||
|
> ⚠️ **命名约束(会进 LLM 函数名)**:内核按 `output_send__<name>` 生成工具,
|
||||||
|
> 而上游对函数名的规范是 `^[a-zA-Z0-9_-]{1,64}$`。名字违规的后果不是
|
||||||
|
> "这个工具不可用",而是**整条请求被 400 拒绝**(`Invalid 'tools[N].function.name'`),
|
||||||
|
> 网关 auto tier 全链条失败,表现成**整个 agent 不回应**。
|
||||||
|
> 所以 `name` 只能用 `[A-Za-z0-9_-]`,并留出 `output_send__`(13 字符)的余量。
|
||||||
|
> 名字若来自外部输入(设备自报 id 之类),请在插件侧派生一个合规且唯一的名字 ——
|
||||||
|
> 内核**不会**替你净化。
|
||||||
|
|
||||||
```go
|
```go
|
||||||
sdk.RegisterOutputChannel("my-channel", CapText|CapFile, "通道描述", ChannelDef{}, handler)
|
sdk.RegisterOutputChannel("my-channel", CapText|CapFile, "通道描述", ChannelDef{}, handler)
|
||||||
```
|
```
|
||||||
|
|||||||
24
README_EN.md
24
README_EN.md
@ -4,7 +4,7 @@ Plugin development SDK for building intelligent plugins that interact with the H
|
|||||||
|
|
||||||
## Version and Compatibility
|
## Version and Compatibility
|
||||||
|
|
||||||
Current: **SDK 1.2.0** (requires kernel **1.2.0+**).
|
Current: **SDK 1.3.0** (requires kernel **1.3.0+**).
|
||||||
|
|
||||||
**The version tracks the kernel's minor version, with the patch position pinned at `.0`**:
|
**The version tracks the kernel's minor version, with the patch position pinned at `.0`**:
|
||||||
|
|
||||||
@ -12,13 +12,33 @@ Current: **SDK 1.2.0** (requires kernel **1.2.0+**).
|
|||||||
|---|---|
|
|---|---|
|
||||||
| 1.0.0 / 1.0.1 / … / 1.0.4 | 1.0.0 |
|
| 1.0.0 / 1.0.1 / … / 1.0.4 | 1.0.0 |
|
||||||
| 1.1.0 / 1.1.1 / … / 1.1.N | **1.1.0** |
|
| 1.1.0 / 1.1.1 / … / 1.1.N | **1.1.0** |
|
||||||
| 1.2.0 onward | 1.2.0 |
|
| 1.2.0 / 1.2.1 / … / 1.2.N | **1.2.0** |
|
||||||
|
| 1.3.0 onward | **1.3.0** |
|
||||||
|
|
||||||
The kernel's patch position is reserved for bugfixes and vulnerability fixes, which never touch the
|
The kernel's patch position is reserved for bugfixes and vulnerability fixes, which never touch the
|
||||||
public interface, so the SDK version has no reason to move with it — otherwise you would either be
|
public interface, so the SDK version has no reason to move with it — otherwise you would either be
|
||||||
forced to chase releases or suspect your version is stale, when not one character of the interface
|
forced to chase releases or suspect your version is stale, when not one character of the interface
|
||||||
has changed.
|
has changed.
|
||||||
|
|
||||||
|
The SDK repository therefore publishes **exactly once per minor version** (`vX.Y.0`); kernel patches
|
||||||
|
such as `v1.3.1` do not trigger an SDK release. (A `v1.3.1` tag was mistakenly cut on 2026-09-13 and
|
||||||
|
has been withdrawn — any SDK tag with a non-zero patch position is wrong.)
|
||||||
|
|
||||||
|
## New in 1.3.0: Injection Priority and Dynamic Output Channels
|
||||||
|
|
||||||
|
- **`InjectOptions.Priority` / `PriorityL1`–`PriorityL4`** — a plugin declares the interrupt level of
|
||||||
|
its own injection; the kernel schedules L1–L4, where **L4 is reserved for the kernel and
|
||||||
|
kernel-level plugins**. The zero value is fully equivalent to the old three-argument call
|
||||||
|
(queued, never preempting), so existing plugins need neither a code change nor a rebuild.
|
||||||
|
Queued input has no level: anything can jump ahead of it.
|
||||||
|
- **`UnregisterOutputChannel` / `OutputChannelUnregistrar` / `SetOutputChannelUnregistrar`** —
|
||||||
|
channels that die with their resource (one channel per remote device) can now be unregistered;
|
||||||
|
previously they lingered and the model kept "successfully" sending into a dead channel.
|
||||||
|
- **Channel names must be legal and unique.** The name is spliced into the LLM function name
|
||||||
|
`output_send__<name>`, so it may only contain `[A-Za-z0-9_-]`. A real production incident
|
||||||
|
(2026-09-13): `device/<id>` made every LLM request fail with 403. Derive channel names from
|
||||||
|
external IDs — never use the raw ID.
|
||||||
|
|
||||||
**Upgrading a 1.0.x plugin to 1.1.x: no code changes, no rebuild.** Everything added in 1.1.0 is
|
**Upgrading a 1.0.x plugin to 1.1.x: no code changes, no rebuild.** Everything added in 1.1.0 is
|
||||||
in the "plugin calls, kernel implements" direction, so not calling it means not being affected
|
in the "plugin calls, kernel implements" direction, so not calling it means not being affected
|
||||||
(verified with an old `plugin.bin` built against SDK 0.9.2: it handshakes fine on the new kernel,
|
(verified with an old `plugin.bin` built against SDK 0.9.2: it handshakes fine on the new kernel,
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
"name": "a2a",
|
"name": "a2a",
|
||||||
"name_zh": "A2A 代理通信",
|
"name_zh": "A2A 代理通信",
|
||||||
"name_en": "A2A Agent Communication",
|
"name_en": "A2A Agent Communication",
|
||||||
"version": "1.3.0",
|
"version": "1.3.1",
|
||||||
"description": "Agent-to-Agent 协议通信插件,支持双向 A2A 通信:可查询其他 Agent 并回复其请求。提供 HTTP 服务端暴露本 Agent 能力。",
|
"description": "Agent-to-Agent 协议通信插件,支持双向 A2A 通信:可查询其他 Agent 并回复其请求。提供 HTTP 服务端暴露本 Agent 能力。",
|
||||||
"author": "HomeAgent",
|
"author": "HomeAgent",
|
||||||
"entry": "plugin.so",
|
"entry": "plugin.so",
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
"name": "acp",
|
"name": "acp",
|
||||||
"name_zh": "ACP 代理通信",
|
"name_zh": "ACP 代理通信",
|
||||||
"name_en": "ACP Agent Client Protocol",
|
"name_en": "ACP Agent Client Protocol",
|
||||||
"version": "1.2.0",
|
"version": "1.2.1",
|
||||||
"description": "Agent Client Protocol 通信插件:充当 ACP 服务端接受其他 Agent 的任务请求,同时提供客户端工具向远程 ACP Agent(如 opencode)发起会话并读取回复",
|
"description": "Agent Client Protocol 通信插件:充当 ACP 服务端接受其他 Agent 的任务请求,同时提供客户端工具向远程 ACP Agent(如 opencode)发起会话并读取回复",
|
||||||
"author": "HomeAgent",
|
"author": "HomeAgent",
|
||||||
"entry": "plugin.so",
|
"entry": "plugin.so",
|
||||||
|
|||||||
47
example/plugindev/README.md
Normal file
47
example/plugindev/README.md
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
# plugindev — 插件开发工具链(Agent 可调用)
|
||||||
|
|
||||||
|
把 SDK 的 `hmapdev` 封装成插件,让 **Agent 自己**走完「新建插件 → 构建 → 安装」全流程,
|
||||||
|
不需要人来敲命令行:
|
||||||
|
|
||||||
|
```
|
||||||
|
plugindev_init 生成工程骨架(等价 hmapdev init <name> [--lua])
|
||||||
|
↓ 改 plugin.go
|
||||||
|
plugindev_build 构建打包(等价在该目录 hmapdev build)→ dist/*.hmap
|
||||||
|
↓
|
||||||
|
plugin_install 安装(用 path 指向刚构建出的 .hmap,overwrite=true 表示原地更新)
|
||||||
|
↓
|
||||||
|
plgreload 重载生效
|
||||||
|
```
|
||||||
|
|
||||||
|
## 工具
|
||||||
|
|
||||||
|
| 工具 | 参数 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| `plugindev_status` | — | hmapdev 是否可用/版本/当前 SDK 版本与路径/工作区;**排查"为什么不能构建"先用它** |
|
||||||
|
| `plugindev_init` | `name`、`lang`(go/lua)、`dir` | 生成工程骨架;插件名必须 `[a-zA-Z0-9_-]{1,64}` |
|
||||||
|
| `plugindev_build` | `dir`、`target` | 在工程目录构建打包;产物路径会在返回里给出 |
|
||||||
|
| `plugindev_sdk` | `action`、`version`、`from` | SDK 版本管理(list/current/path/latest/install/use);`from` 可指向本地 SDK 源码 |
|
||||||
|
| `plugindev_projects` | — | 列出工作区里已有工程与产物 |
|
||||||
|
|
||||||
|
## 配置
|
||||||
|
|
||||||
|
| 键 | 默认 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| `hmapdev_path` | 自动查找 | 依次尝试:本配置项 → PATH → `/usr/local/bin/hmapdev` → `/root/go/bin/hmapdev` |
|
||||||
|
| `workspace_dir` | `<data_dir>/plugindev` | `plugindev_init` 生成工程的默认目录 |
|
||||||
|
| `build_timeout_sec` | 600 | 单次 hmapdev 调用超时 |
|
||||||
|
|
||||||
|
## 前置:装 hmapdev
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd <sdk-repo>/tools/hmapdev && go build -buildvcs=false -o /usr/local/bin/hmapdev .
|
||||||
|
hmapdev version
|
||||||
|
```
|
||||||
|
|
||||||
|
## 安全边界(都在实现里,不只写在文档里)
|
||||||
|
|
||||||
|
- 只 exec **hmapdev 一个可执行文件**,不接受任意命令、不做 shell 拼接;
|
||||||
|
- `plugindev_build` 只接受含 `plg.json` 的目录("看起来是插件工程"才构建),
|
||||||
|
避免把这个工具变成对任意目录跑构建;
|
||||||
|
- 子进程全部带超时,输出**截断**后才返回(构建日志动辄几百 KB,直接回灌会撑爆模型上下文);
|
||||||
|
- 工程名约束与内核/上游对"进工具名的标识符"的规则一致(`[a-zA-Z0-9_-]{1,64}`)。
|
||||||
7
example/plugindev/go.mod
Normal file
7
example/plugindev/go.mod
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
module plugindev
|
||||||
|
|
||||||
|
go 1.25.0
|
||||||
|
|
||||||
|
require gitcode.com/JianFeeeee/homeagent-sdk v0.0.0
|
||||||
|
|
||||||
|
replace gitcode.com/JianFeeeee/homeagent-sdk => ../../
|
||||||
11
example/plugindev/main.go
Normal file
11
example/plugindev/main.go
Normal 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)
|
||||||
|
}
|
||||||
19
example/plugindev/plg.json
Normal file
19
example/plugindev/plg.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "plugindev",
|
||||||
|
"name_zh": "插件开发工具链",
|
||||||
|
"name_en": "Plugin Dev Toolchain",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "把 hmapdev 工具链封装成 Agent 可调用的工具:脚手架生成插件工程、构建打包 .hmap、管理 SDK 版本。配合 plugin_install 即可让 Agent 自己做完「新建插件 → 构建 → 安装」全流程。",
|
||||||
|
"author": "HomeAgent",
|
||||||
|
"entry": "plugin.so",
|
||||||
|
"tags": [
|
||||||
|
"plugindev",
|
||||||
|
"toolchain",
|
||||||
|
"developer"
|
||||||
|
],
|
||||||
|
"targets": "linux/amd64",
|
||||||
|
"outdir": "dist",
|
||||||
|
"bundle": true,
|
||||||
|
"replaces": {},
|
||||||
|
"source_dirs": []
|
||||||
|
}
|
||||||
455
example/plugindev/plugin.go
Normal file
455
example/plugindev/plugin.go
Normal file
@ -0,0 +1,455 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
// plugindev:把 SDK 的 hmapdev 工具链封装成 Agent 可调用的插件。
|
||||||
|
//
|
||||||
|
// 为什么需要它:hmapdev 是"给人和 CI 用"的命令行工具。做成插件后,Agent 能自己:
|
||||||
|
// plugindev_init(脚手架)→ plugindev_build(构建出 .hmap)→ plugin_install(安装)→ plgreload
|
||||||
|
// 也就是"让 Agent 自己写/改/装插件"这条链不需要人来敲命令。
|
||||||
|
//
|
||||||
|
// 安全边界(都在实现里落实,不只写在描述里):
|
||||||
|
// - 只有 **hmapdev 一个可执行文件**会被 exec(不接受任意命令/参数拼接);
|
||||||
|
// - `plugindev_build` 只接受"看起来是插件工程"的目录(含 plg.json),
|
||||||
|
// 避免把一个 `hmapdev build` 变成对任意目录的操作;
|
||||||
|
// - `plugindev_init` 生成的工程名必须满足 `[a-zA-Z0-9_-]{1,64}`(与 LLM 函数名
|
||||||
|
// 同一套约束 —— 插件名会进 `output_send__<通道>` 之类的工具名);
|
||||||
|
// - 所有子进程都有超时,输出截断后再返回(防止把几十 MB 构建日志灌进模型上下文)。
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitcode.com/JianFeeeee/homeagent-sdk/sdk"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultBuildTimeout = 10 * time.Minute
|
||||||
|
maxOutputChars = 6000
|
||||||
|
)
|
||||||
|
|
||||||
|
// namePattern 与内核/上游对"会进工具名的标识符"的约束一致。
|
||||||
|
var namePattern = regexp.MustCompile(`^[a-zA-Z0-9_-]{1,64}$`)
|
||||||
|
|
||||||
|
type Plugin struct {
|
||||||
|
name string
|
||||||
|
sdk *sdk.PluginSDK
|
||||||
|
|
||||||
|
hmapdev string // 解析到的 hmapdev 可执行文件路径
|
||||||
|
workspace string // 默认工作区(生成的工程落在这里)
|
||||||
|
timeout time.Duration // 单次 hmapdev 调用的超时
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPluginFactory(name string, config map[string]interface{}) (sdk.Plugin, error) {
|
||||||
|
return &Plugin{name: name}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Plugin) Name() string { return p.name }
|
||||||
|
|
||||||
|
func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
||||||
|
p.sdk = s
|
||||||
|
s.SetAutoRestart(true)
|
||||||
|
|
||||||
|
s.Settings().RegisterDef(sdk.ConfigDef{
|
||||||
|
Key: "hmapdev_path", Type: "string", DisplayName: "hmapdev 路径",
|
||||||
|
Description: "插件开发工具链可执行文件路径。留空则按 PATH → /usr/local/bin/hmapdev → /root/go/bin/hmapdev 查找",
|
||||||
|
Category: p.name,
|
||||||
|
})
|
||||||
|
s.Settings().RegisterDef(sdk.ConfigDef{
|
||||||
|
Key: "workspace_dir", Type: "string", DisplayName: "工程工作区",
|
||||||
|
Description: "plugindev_init 生成工程的默认目录。留空则用 <data_dir>/plugindev",
|
||||||
|
Category: p.name,
|
||||||
|
})
|
||||||
|
s.Settings().RegisterDef(sdk.ConfigDef{
|
||||||
|
Key: "build_timeout_sec", Default: 600, Type: "int", DisplayName: "构建超时(秒)",
|
||||||
|
Description: "单次 hmapdev 调用的超时上限",
|
||||||
|
Category: p.name,
|
||||||
|
})
|
||||||
|
|
||||||
|
p.hmapdev = p.resolveHmapdev()
|
||||||
|
p.workspace = p.resolveWorkspace()
|
||||||
|
p.timeout = defaultBuildTimeout
|
||||||
|
if v, _ := s.Settings().Get("build_timeout_sec"); v != nil {
|
||||||
|
if n, ok := toInt(v); ok && n > 0 {
|
||||||
|
p.timeout = time.Duration(n) * time.Second
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s.RegisterTool("plugindev_status", sdk.ToolDef{
|
||||||
|
Name: "plugindev_status",
|
||||||
|
Description: "查看插件开发工具链状态:hmapdev 是否可用、版本、当前 SDK 版本与路径、工程工作区目录。排查\"为什么不能构建插件\"时先用它。",
|
||||||
|
Parameters: map[string]interface{}{"type": "object", "properties": map[string]interface{}{}},
|
||||||
|
}, p.handleStatus)
|
||||||
|
|
||||||
|
s.RegisterTool("plugindev_init", sdk.ToolDef{
|
||||||
|
Name: "plugindev_init",
|
||||||
|
Description: "生成一个新的插件工程骨架(等价于 `hmapdev init <name> [--lua]`)。生成后在返回的目录里改 plugin.go,再用 plugindev_build 构建。",
|
||||||
|
Parameters: map[string]interface{}{
|
||||||
|
"type": "object",
|
||||||
|
"properties": map[string]interface{}{
|
||||||
|
"name": map[string]interface{}{
|
||||||
|
"type": "string",
|
||||||
|
"description": "插件名(也是工程目录名):只允许字母数字下划线短横,长度 1-64。例:my_plugin",
|
||||||
|
},
|
||||||
|
"lang": map[string]interface{}{
|
||||||
|
"type": "string", "description": "go(默认)或 lua",
|
||||||
|
},
|
||||||
|
"dir": map[string]interface{}{
|
||||||
|
"type": "string", "description": "在哪个目录下生成(默认工作区)。必须是已存在的目录",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": []string{"name"},
|
||||||
|
},
|
||||||
|
}, p.handleInit)
|
||||||
|
|
||||||
|
s.RegisterTool("plugindev_build", sdk.ToolDef{
|
||||||
|
Name: "plugindev_build",
|
||||||
|
Description: "构建并打包一个插件工程(等价于在该工程目录里执行 `hmapdev build [target]`),产物是 dist/*.hmap。构建成功后用 plugin_install 安装(本地路径)。",
|
||||||
|
Parameters: map[string]interface{}{
|
||||||
|
"type": "object",
|
||||||
|
"properties": map[string]interface{}{
|
||||||
|
"dir": map[string]interface{}{
|
||||||
|
"type": "string", "description": "插件工程目录(必须含 plg.json)",
|
||||||
|
},
|
||||||
|
"target": map[string]interface{}{
|
||||||
|
"type": "string", "description": "构建目标,留空 = native(当前平台)。例:linux/amd64",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": []string{"dir"},
|
||||||
|
},
|
||||||
|
}, p.handleBuild)
|
||||||
|
|
||||||
|
s.RegisterTool("plugindev_sdk", sdk.ToolDef{
|
||||||
|
Name: "plugindev_sdk",
|
||||||
|
Description: "管理插件 SDK 版本(hmapdev sdk 子命令):list 列出已安装、current 当前版本、path 当前路径、latest 远端最新、install 安装某版本(可用 from 指定本地源码目录)、use 切换版本。构建插件报\"SDK 缺少某能力\"时用它升级 SDK。",
|
||||||
|
Parameters: map[string]interface{}{
|
||||||
|
"type": "object",
|
||||||
|
"properties": map[string]interface{}{
|
||||||
|
"action": map[string]interface{}{
|
||||||
|
"type": "string", "description": "list | current | path | latest | install | use",
|
||||||
|
},
|
||||||
|
"version": map[string]interface{}{
|
||||||
|
"type": "string", "description": "install/use 的版本号,如 v1.3.0",
|
||||||
|
},
|
||||||
|
"from": map[string]interface{}{
|
||||||
|
"type": "string", "description": "install 时用本地 SDK 源码目录(开发中的 SDK 用这个)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": []string{"action"},
|
||||||
|
},
|
||||||
|
}, p.handleSDK)
|
||||||
|
|
||||||
|
s.RegisterTool("plugindev_projects", sdk.ToolDef{
|
||||||
|
Name: "plugindev_projects",
|
||||||
|
Description: "列出工作区里已有的插件工程(名字、版本、是否已构建出 dist 产物),用于接续之前的开发。",
|
||||||
|
Parameters: map[string]interface{}{"type": "object", "properties": map[string]interface{}{}},
|
||||||
|
}, p.handleProjects)
|
||||||
|
|
||||||
|
log.Printf("[plugindev] 就绪:hmapdev=%s 工作区=%s", fallback(p.hmapdev, "(未找到)"), p.workspace)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Plugin) Stop() error { return nil }
|
||||||
|
|
||||||
|
// ---------------- 工具实现 ----------------
|
||||||
|
|
||||||
|
func (p *Plugin) handleStatus(args map[string]interface{}) (interface{}, error) {
|
||||||
|
out := map[string]interface{}{
|
||||||
|
"hmapdev": fallback(p.hmapdev, ""),
|
||||||
|
"workspace": p.workspace,
|
||||||
|
}
|
||||||
|
if p.hmapdev == "" {
|
||||||
|
out["available"] = false
|
||||||
|
out["hint"] = "未找到 hmapdev。请安装:go build -o /usr/local/bin/hmapdev <sdk>/tools/hmapdev"
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
out["available"] = true
|
||||||
|
if txt, err := p.run(nil, ""); err == nil {
|
||||||
|
out["version"] = strings.TrimSpace(txt)
|
||||||
|
} else {
|
||||||
|
out["error"] = err.Error()
|
||||||
|
}
|
||||||
|
if txt, err := p.run([]string{"sdk", "current"}, ""); err == nil {
|
||||||
|
out["sdk_current"] = strings.TrimSpace(txt)
|
||||||
|
}
|
||||||
|
if txt, err := p.run([]string{"sdk", "path"}, ""); err == nil {
|
||||||
|
out["sdk_path"] = strings.TrimSpace(txt)
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Plugin) handleInit(args map[string]interface{}) (interface{}, error) {
|
||||||
|
name, _ := args["name"].(string)
|
||||||
|
name = strings.TrimSpace(name)
|
||||||
|
if !namePattern.MatchString(name) {
|
||||||
|
return map[string]interface{}{
|
||||||
|
"error": "插件名只允许 [a-zA-Z0-9_-],长度 1-64(它会进 LLM 工具名,违规会让整条请求被上游拒绝)",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
dir, _ := args["dir"].(string)
|
||||||
|
if dir == "" {
|
||||||
|
dir = p.workspace
|
||||||
|
}
|
||||||
|
if st, err := os.Stat(dir); err != nil || !st.IsDir() {
|
||||||
|
return map[string]interface{}{"error": fmt.Sprintf("目录不存在: %s", dir)}, nil
|
||||||
|
}
|
||||||
|
cmd := []string{"init", name}
|
||||||
|
if lang, _ := args["lang"].(string); strings.EqualFold(lang, "lua") {
|
||||||
|
cmd = append(cmd, "--lua")
|
||||||
|
}
|
||||||
|
txt, err := p.run(cmd, dir)
|
||||||
|
res := map[string]interface{}{"output": txt, "project_dir": filepath.Join(dir, name)}
|
||||||
|
if err != nil {
|
||||||
|
res["error"] = err.Error()
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Plugin) handleBuild(args map[string]interface{}) (interface{}, error) {
|
||||||
|
dir, _ := args["dir"].(string)
|
||||||
|
if dir == "" {
|
||||||
|
return map[string]interface{}{"error": "dir 不能为空"}, nil
|
||||||
|
}
|
||||||
|
abs, err := filepath.Abs(dir)
|
||||||
|
if err != nil {
|
||||||
|
return map[string]interface{}{"error": err.Error()}, nil
|
||||||
|
}
|
||||||
|
// 只在"插件工程"里构建:必须存在 plg.json。这样这个工具不会变成对任意目录跑构建。
|
||||||
|
manifest := filepath.Join(abs, "plg.json")
|
||||||
|
if _, err := os.Stat(manifest); err != nil {
|
||||||
|
return map[string]interface{}{
|
||||||
|
"error": fmt.Sprintf("%s 不是插件工程(缺 plg.json);用 plugindev_init 先建一个", abs),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
cmd := []string{"build"}
|
||||||
|
if target, _ := args["target"].(string); strings.TrimSpace(target) != "" {
|
||||||
|
cmd = append(cmd, strings.TrimSpace(target))
|
||||||
|
}
|
||||||
|
txt, runErr := p.run(cmd, abs)
|
||||||
|
res := map[string]interface{}{"output": txt, "project_dir": abs}
|
||||||
|
if pkgs := listHmap(filepath.Join(abs, "dist")); len(pkgs) > 0 {
|
||||||
|
res["artifacts"] = pkgs
|
||||||
|
res["next"] = "用 plugin_install 安装本地产物(path 指向上面 artifacts 里的 .hmap),然后 plgreload"
|
||||||
|
}
|
||||||
|
if runErr != nil {
|
||||||
|
res["error"] = runErr.Error()
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Plugin) handleSDK(args map[string]interface{}) (interface{}, error) {
|
||||||
|
action, _ := args["action"].(string)
|
||||||
|
action = strings.TrimSpace(action)
|
||||||
|
switch action {
|
||||||
|
case "list", "current", "path", "latest":
|
||||||
|
txt, err := p.run([]string{"sdk", action}, "")
|
||||||
|
res := map[string]interface{}{"output": txt}
|
||||||
|
if err != nil {
|
||||||
|
res["error"] = err.Error()
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
case "install":
|
||||||
|
version, _ := args["version"].(string)
|
||||||
|
from, _ := args["from"].(string)
|
||||||
|
cmd := []string{"sdk", "install"}
|
||||||
|
if strings.TrimSpace(from) != "" {
|
||||||
|
cmd = append(cmd, "--from", strings.TrimSpace(from))
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(version) != "" {
|
||||||
|
cmd = append(cmd, strings.TrimSpace(version))
|
||||||
|
} else if strings.TrimSpace(from) == "" {
|
||||||
|
cmd = append(cmd, "latest")
|
||||||
|
}
|
||||||
|
txt, err := p.run(cmd, "")
|
||||||
|
res := map[string]interface{}{"output": txt}
|
||||||
|
if err != nil {
|
||||||
|
res["error"] = err.Error()
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
case "use":
|
||||||
|
version, _ := args["version"].(string)
|
||||||
|
if strings.TrimSpace(version) == "" {
|
||||||
|
return map[string]interface{}{"error": "use 需要 version"}, nil
|
||||||
|
}
|
||||||
|
txt, err := p.run([]string{"sdk", "use", strings.TrimSpace(version)}, "")
|
||||||
|
res := map[string]interface{}{"output": txt}
|
||||||
|
if err != nil {
|
||||||
|
res["error"] = err.Error()
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
default:
|
||||||
|
return map[string]interface{}{"error": "action 只能是 list/current/path/latest/install/use"}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Plugin) handleProjects(args map[string]interface{}) (interface{}, error) {
|
||||||
|
entries, err := os.ReadDir(p.workspace)
|
||||||
|
if err != nil {
|
||||||
|
return map[string]interface{}{"error": err.Error(), "workspace": p.workspace}, nil
|
||||||
|
}
|
||||||
|
var out []map[string]interface{}
|
||||||
|
for _, e := range entries {
|
||||||
|
if !e.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
dir := filepath.Join(p.workspace, e.Name())
|
||||||
|
projects := []string{dir}
|
||||||
|
// 有些工程会被生成到子目录里(hmapdev init 支持指定目录),这里只看一层
|
||||||
|
for _, sub := range projects {
|
||||||
|
if _, err := os.Stat(filepath.Join(sub, "plg.json")); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
item := map[string]interface{}{"name": e.Name(), "dir": sub}
|
||||||
|
if v := readPlgVersion(filepath.Join(sub, "plg.json")); v != "" {
|
||||||
|
item["version"] = v
|
||||||
|
}
|
||||||
|
if pkgs := listHmap(filepath.Join(sub, "dist")); len(pkgs) > 0 {
|
||||||
|
item["artifacts"] = pkgs
|
||||||
|
}
|
||||||
|
out = append(out, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Slice(out, func(i, j int) bool { return out[i]["name"].(string) < out[j]["name"].(string) })
|
||||||
|
return map[string]interface{}{"workspace": p.workspace, "projects": out}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- 基础设施 ----------------
|
||||||
|
|
||||||
|
// run 执行一次 hmapdev。args 为空时执行 `hmapdev version`(用于探活)。
|
||||||
|
func (p *Plugin) run(args []string, dir string) (string, error) {
|
||||||
|
if p.hmapdev == "" {
|
||||||
|
return "", fmt.Errorf("未找到 hmapdev 可执行文件")
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), p.timeout)
|
||||||
|
defer cancel()
|
||||||
|
cmd := exec.CommandContext(ctx, p.hmapdev, args...)
|
||||||
|
if dir != "" {
|
||||||
|
cmd.Dir = dir
|
||||||
|
}
|
||||||
|
// 继承环境(Go 工具链需要 GOCACHE/GOPATH/PATH 等)。
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
txt := truncateOutput(string(out))
|
||||||
|
if ctx.Err() == context.DeadlineExceeded {
|
||||||
|
return txt, fmt.Errorf("hmapdev %s 超时(%s)", strings.Join(args, " "), p.timeout)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return txt, fmt.Errorf("hmapdev %s 失败: %v", strings.Join(args, " "), err)
|
||||||
|
}
|
||||||
|
return txt, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolveHmapdev 依次尝试:配置项 → PATH → 常见安装位置。
|
||||||
|
func (p *Plugin) resolveHmapdev() string {
|
||||||
|
if v, _ := p.sdk.Settings().Get("hmapdev_path"); v != nil {
|
||||||
|
if s, _ := v.(string); strings.TrimSpace(s) != "" {
|
||||||
|
if _, err := os.Stat(strings.TrimSpace(s)); err == nil {
|
||||||
|
return strings.TrimSpace(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if path, err := exec.LookPath("hmapdev"); err == nil {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
for _, cand := range []string{"/usr/local/bin/hmapdev", "/root/go/bin/hmapdev"} {
|
||||||
|
if _, err := os.Stat(cand); err == nil {
|
||||||
|
return cand
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolveWorkspace:配置项 → <data_dir>/plugindev → ./plugindev。
|
||||||
|
func (p *Plugin) resolveWorkspace() string {
|
||||||
|
if v, _ := p.sdk.Settings().Get("workspace_dir"); v != nil {
|
||||||
|
if s, _ := v.(string); strings.TrimSpace(s) != "" {
|
||||||
|
ws := strings.TrimSpace(s)
|
||||||
|
_ = os.MkdirAll(ws, 0o755)
|
||||||
|
return ws
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if v, err := p.sdk.Settings().GetCore("core.daemon.data_dir"); err == nil {
|
||||||
|
if dd, _ := v.(string); dd != "" {
|
||||||
|
ws := filepath.Join(dd, "plugindev")
|
||||||
|
_ = os.MkdirAll(ws, 0o755)
|
||||||
|
return ws
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ws := "plugindev"
|
||||||
|
_ = os.MkdirAll(ws, 0o755)
|
||||||
|
return ws
|
||||||
|
}
|
||||||
|
|
||||||
|
// truncateOutput 截断长输出:构建日志动辄几百 KB,直接返回会灌爆模型上下文。
|
||||||
|
func truncateOutput(s string) string {
|
||||||
|
if len(s) <= maxOutputChars {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
head := s[:maxOutputChars/2]
|
||||||
|
tail := s[len(s)-maxOutputChars/2:]
|
||||||
|
return fmt.Sprintf("%s\n…(输出被截断,共 %d 字节)…\n%s", head, len(s), tail)
|
||||||
|
}
|
||||||
|
|
||||||
|
// listHmap 列出目录下的 .hmap 产物(按名字排序,稳定输出)。
|
||||||
|
func listHmap(dir string) []string {
|
||||||
|
entries, err := os.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var out []string
|
||||||
|
for _, e := range entries {
|
||||||
|
if e.IsDir() || !strings.HasSuffix(e.Name(), ".hmap") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, filepath.Join(dir, e.Name()))
|
||||||
|
}
|
||||||
|
sort.Strings(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func readPlgVersion(path string) string {
|
||||||
|
b, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
s := string(b)
|
||||||
|
i := strings.Index(s, `"version"`)
|
||||||
|
if i < 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
rest := s[i:]
|
||||||
|
j := strings.Index(rest, ":")
|
||||||
|
if j < 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
rest = strings.TrimSpace(rest[j+1:])
|
||||||
|
rest = strings.TrimPrefix(rest, `"`)
|
||||||
|
if k := strings.Index(rest, `"`); k > 0 {
|
||||||
|
return rest[:k]
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func toInt(v interface{}) (int, bool) {
|
||||||
|
switch n := v.(type) {
|
||||||
|
case int:
|
||||||
|
return n, true
|
||||||
|
case int64:
|
||||||
|
return int(n), true
|
||||||
|
case float64:
|
||||||
|
return int(n), true
|
||||||
|
}
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func fallback(s, def string) string {
|
||||||
|
if strings.TrimSpace(s) == "" {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
@ -2,7 +2,7 @@
|
|||||||
"name": "qq",
|
"name": "qq",
|
||||||
"name_zh": "QQ消息",
|
"name_zh": "QQ消息",
|
||||||
"name_en": "qq",
|
"name_en": "qq",
|
||||||
"version": "1.4.0",
|
"version": "1.4.1",
|
||||||
"description": "QQ 消息收发插件,通过 NapCat 协议桥接",
|
"description": "QQ 消息收发插件,通过 NapCat 协议桥接",
|
||||||
"author": "HomeAgent",
|
"author": "HomeAgent",
|
||||||
"entry": "plugin.so",
|
"entry": "plugin.so",
|
||||||
|
|||||||
@ -925,6 +925,19 @@ func (p *Plugin) sessionToolArgsAllowed(name string, args map[string]interface{}
|
|||||||
if !auth.active || auth.owner {
|
if !auth.active || auth.owner {
|
||||||
return true, ""
|
return true, ""
|
||||||
}
|
}
|
||||||
|
// 输出工具**不受"当前会话"身份限制**(先于身份判据返回)。
|
||||||
|
//
|
||||||
|
// 为什么:输出是 agent 的**主动调用**,发到哪个会话由它自己给的 meta
|
||||||
|
// (group_id / user_id)决定 —— handleChannelOutput 会强制要求该字段存在,
|
||||||
|
// 缺了会得到明确的报错。这里再要求"本轮能精确匹配可信 OneBot 事件"是多余的门,
|
||||||
|
// 而且会把合法发送一起拒掉:现场(被子的中断唤醒的一轮)父带齐 meta 也发不出去,
|
||||||
|
// 报「可信 QQ 会话身份不完整」。
|
||||||
|
// 「只能访问当前会话」这类限制只对**读取类**工具(get_history / mark_read /
|
||||||
|
// get_message)成立 —— 那才是真的不能跨会话读。
|
||||||
|
if name == "output_send__"+p.name {
|
||||||
|
return true, ""
|
||||||
|
}
|
||||||
|
|
||||||
currentPeer := auth.userID
|
currentPeer := auth.userID
|
||||||
if auth.isGroup {
|
if auth.isGroup {
|
||||||
currentPeer = auth.groupID
|
currentPeer = auth.groupID
|
||||||
|
|||||||
@ -202,3 +202,36 @@ func TestZeroLimitsMeanUnlimited(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 降权(本轮无法精确匹配可信 OneBot 事件 ⇒ auth={active:true}、无 peer、非 owner)时,
|
||||||
|
// **输出仍必须放行**:发到哪个会话由 agent 自己给的 meta 决定,
|
||||||
|
// 不该被「当前会话身份」挡住。现场:被子的中断唤醒的一轮里,父带齐 meta 也发不出去
|
||||||
|
// (报「可信 QQ 会话身份不完整」)。
|
||||||
|
//
|
||||||
|
// 反之,**读取类**工具在降权时仍受当前会话限制 —— 那才是真的不能跨会话读。
|
||||||
|
func TestDowngradedAuthStillAllowsQQOutput(t *testing.T) {
|
||||||
|
p := newPermissionTestPlugin(t)
|
||||||
|
p.auth = qqAuthContext{active: true}
|
||||||
|
p.privateToolAllowlist = []string{"output_send__qq", "qq_get_history"}
|
||||||
|
p.groupToolAllowlists = map[int64][]string{0: {"output_send__qq", "qq_get_history"}}
|
||||||
|
|
||||||
|
ctx := toolCallContext("output_send__qq", map[string]interface{}{
|
||||||
|
"payload": "带齐 meta 的主动发送",
|
||||||
|
"type": "text",
|
||||||
|
"meta": `{"user_id":2198972886}`,
|
||||||
|
})
|
||||||
|
if err := p.beforeToolcall(ctx); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if ctx.Response != nil {
|
||||||
|
t.Fatalf("降权时输出被拒: %s", *ctx.Response)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx2 := toolCallContext("qq_get_history", map[string]interface{}{"group_id": 1027993713})
|
||||||
|
if err := p.beforeToolcall(ctx2); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if ctx2.Response == nil || !strings.Contains(*ctx2.Response, "可信 QQ 会话身份不完整") {
|
||||||
|
t.Fatalf("读取类工具在降权时应被当前会话限制挡住: %#v", ctx2.Response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -480,7 +480,15 @@ func (s *PluginSDK) RegisterPluginAPI(name string) error {
|
|||||||
// 入站(谁会往 <name> 注入输入)是另一件事,用 RegisterInputChannel 声明。
|
// 入站(谁会往 <name> 注入输入)是另一件事,用 RegisterInputChannel 声明。
|
||||||
// 若该通道同时也是你的注入入口,两个都要登记。
|
// 若该通道同时也是你的注入入口,两个都要登记。
|
||||||
//
|
//
|
||||||
// name: channel name (e.g. "qq", "webui")
|
// name: channel name (e.g. "qq", "webui")。
|
||||||
|
//
|
||||||
|
// ❗**命名约束**:内核会把通道名拼进 LLM 的函数名(`output_send__<name>`),
|
||||||
|
// 而上游对函数名的规范是 `^[a-zA-Z0-9_-]{1,64}$`。违反的后果不是"这个工具不可用",
|
||||||
|
// 而是**整条请求被上游 400 拒绝**(`Invalid 'tools[N].function.name'`),
|
||||||
|
// 网关的 auto tier 会全链条失败 —— 表现成"整个 agent 不说话了"。
|
||||||
|
// 所以通道名只能用 `[A-Za-z0-9_-]`,且总长要留出 `output_send__`(13 字符)的余量。
|
||||||
|
// 若通道名来自外部输入(设备自报 id 之类),请**在插件侧派生一个合规且唯一的名字**,
|
||||||
|
// 而不是把原始值直接当通道名。
|
||||||
// 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)
|
// def: 通道在记忆计算层的行为(NoMemory/Cleaner)
|
||||||
|
|||||||
Reference in New Issue
Block a user