mirror of
https://gitcode.com/JianFeeeee/homeagent-sdk.git
synced 2026-09-24 19:08:08 +00:00
docs: 插件 SDK 文档站(API 参考从源码生成 + 自建检索)
为插件作者建一个文档站,重点是**能按描述搜到 API**,以及**明确能力边界**。
## 为什么 API 参考要生成而不是手写
公开 API 面有 115 个符号、11 个接口。手抄必然与代码漂移——这是文档站最常见的
死法(本仓 README 里已经有过几处「文档说一套、代码是另一套」)。
所以 `tools/apidoc` 直接从 `sdk/*.go` 提取签名、文档注释与代码块示例,渲染成
`docs/api/*.md`。发现文档不对时改的是**源码注释**,不是生成物。生成页首行带
「勿手改」标记,防止有人改了下次构建白改。
- `extract.go`:go/ast + go/doc 提取(只用标准库,离线可跑,不引入依赖)
- `gensite/`:渲染 Markdown + 检索索引
- `gensite/usages.go`:从 `example/` 21 个示例插件里反查**真实调用点**,
贴在每个 API 下(源码注释里几乎没有可运行示例,但示例插件都是能编译跑的真代码)
## 能力边界:写这个站时查出的三处文档错误
这是本次最有价值的部分。原以为「公开 SDK 里有的 API 外部插件都能用」,
实测对照桥接模板后发现三处不符,站内已更正:
1. **`PluginMgr()` 被写成「仅内置可用」——错的。** 桥接模板第 692 行显式
`base.SetPluginMgrAPI(procPluginMgr{})`,公开 `PluginMgrAPI` 注释也写「外部插件可调用」。
真正的区别是**方法数**:公开面 3 个(ReloadOne/ListLoadedPlugins/IsPluginDisabled),
内部面 9 个。容易混淆是因为两个包里有同名但不同的接口。
2. **`Events()` 外部插件恒为 nil。** `SetEventSubscriber` 全仓只有定义、无调用点,
故 subscriber 从未被注入。外部插件的事件订阅实际由生成的运行时走
`events.subscribe` RPC 完成——旧文档把它当成可用入口,会让人写出必然失效的代码。
3. **`UnregisterOutputChannel` 是静默无效,不是报错。** 桥接只注入 registrar、
不注入 unregistrar,于是 `regOutputUnreg == nil`,函数命中 else 分支**直接返回 nil**
(sdk/plugin.go:539-549)——不报错、通道也没注销。
每条裁定的依据写进 `tools/apidoc/tiers.json`(文件:行号 或 grep 结论),
站上以告警框呈现,读者可自行核对。判断依据三源:桥接模板的 `base.Set*` 注入点、
`internal/sdk` 完整面、`internal/plugin/proc/protocol.go` 的 RPC 表。
## 检索(用户的核心诉求)
两套互补:
- **MkDocs 内置搜索**:全文,中文走 jieba 分词。
- **自建 API 检索**(`docs/javascripts/api-search.js` + `assets/api-index.json`):
支持四类查询——按名称、**按功能描述**(「注册工具」→ RegisterTool、
「崩溃」→ SetAutoRestart)、按 `限定符.方法`(`memory.recall` → MemoryAPI.Recall)、
按签名片段(`(string) error`)。并标出「仅内置」,避免外部插件作者踩空。
自建的理由:Material 内置搜索按整页文本索引,搜 `InjectText` 会列出所有提到它的
页面,但分不清哪条是它的定义;而且它要等 mkdocs build 才更新。
## 文档结构
- `docs/guide/`:快速开始、Go/Lua 首个插件、能力边界、打包发布、多平台、受限 SDK 与安全
- `docs/api/`:10 个按「你想做什么」划分的章节(工具/阶段/记忆/通道/配置/生命周期/
事件/LLM/常量/桥接)+ 仅内置汇总页
- `docs/versions.md`:SDK 版本语义(跟随内核中版本、patch 恒为 .0)、
1.0.0 是唯一破坏性变更、RPC 协议版本
## 验证
- `mkdocs build --strict` 零告警
- 23 个页面的全部站内链接与锚点可达(自动校验)
- 1440 / 768 / 390px 三视口:无横向溢出、无控制台错误
- 四种检索模式实测有结果且跳转锚点正确
- 构建产物 `site_build/` 已 gitignore
用法:`tools/apidoc/build.sh`(生成+构建)、`tools/apidoc/build.sh serve`(预览)。
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@ -35,3 +35,8 @@ z_entry.c
|
||||
|
||||
# plugindev binary in tools/
|
||||
tools/plugindev/plugindev
|
||||
|
||||
# 文档站构建产物(由 tools/apidoc/build.sh 生成)
|
||||
site_build/
|
||||
# mkdocs 缓存
|
||||
.cache/
|
||||
|
||||
204
docs/api/bridge.md
Normal file
204
docs/api/bridge.md
Normal file
@ -0,0 +1,204 @@
|
||||
<!-- 本页由 tools/apidoc/gensite 从源码生成,请勿手改;要改文档就改 sdk/*.go 的注释。 -->
|
||||
|
||||
# 桥接装配点(Bridge)
|
||||
|
||||
以下方法**不是给插件业务代码调的**——它们由 `hmapdev` 生成的运行时在启动时调用,用来把内核能力注入到 SDK 实例。列在这里是为了让「公开 API 面」完整,并说明每个注入点对应什么能力。
|
||||
|
||||
### `APIRegistrar`
|
||||
|
||||
```go
|
||||
type APIRegistrar func(name string) error
|
||||
```
|
||||
|
||||
APIRegistrar registers a plugin API for external access.
|
||||
|
||||
<small>`plugin.go:311`</small>
|
||||
|
||||
### `InputChannelRegistrar`
|
||||
|
||||
```go
|
||||
type InputChannelRegistrar func(name string, def ChannelDef) error
|
||||
```
|
||||
|
||||
InputChannelRegistrar registers an input channel with its memory behavior.
|
||||
|
||||
<small>`plugin.go:314`</small>
|
||||
|
||||
### `OutputChannelRegistrar`
|
||||
|
||||
```go
|
||||
type OutputChannelRegistrar func(name string, caps int, desc string, def ChannelDef, handler ToolHandler) error
|
||||
```
|
||||
|
||||
OutputChannelRegistrar registers an output channel that the output_send tool can use.
|
||||
|
||||
<small>`plugin.go:317`</small>
|
||||
|
||||
### `OutputChannelUnregistrar`
|
||||
|
||||
```go
|
||||
type OutputChannelUnregistrar func(name string) error
|
||||
```
|
||||
|
||||
OutputChannelUnregistrar 注销一个输出通道。
|
||||
|
||||
为什么需要它:输出通道不止有"启动时注册一次"的静态通道,还有**随外部资源生灭**的
|
||||
动态通道 —— 典型是远程设备:`device/<id>` 只在设备在线期间存在,设备掉线后
|
||||
必须注销,否则 output_list_channels 会一直列着它、模型会往一个死通道发消息。
|
||||
|
||||
<small>`plugin.go:324`</small>
|
||||
|
||||
### `PluginSDK.SetDocMemoryAPI`
|
||||
|
||||
!!! info "桥接装配点"
|
||||
桥接运行时注入点(由 hmapdev 生成的 proc_main 调用,插件业务代码不调用)
|
||||
|
||||
```go
|
||||
func (s *PluginSDK) SetDocMemoryAPI(dm DocMemoryAPI)
|
||||
```
|
||||
|
||||
<small>`plugin.go:614`</small>
|
||||
|
||||
### `PluginSDK.SetEventSubscriber`
|
||||
|
||||
!!! warning "仅内核内置插件可用"
|
||||
同上:无调用点。标 builtin 而非 bridge,因为连桥接运行时都不注入它——外部插件无法用它获得事件订阅能力。
|
||||
|
||||
```go
|
||||
func (s *PluginSDK) SetEventSubscriber(es EventSubscriber)
|
||||
```
|
||||
|
||||
<small>`plugin.go:638`</small>
|
||||
|
||||
### `PluginSDK.SetIOInjector`
|
||||
|
||||
!!! info "桥接装配点"
|
||||
桥接运行时注入点(由 hmapdev 生成的 proc_main 调用,插件业务代码不调用)
|
||||
|
||||
```go
|
||||
func (s *PluginSDK) SetIOInjector(io IOInjector)
|
||||
```
|
||||
|
||||
SetIOInjector sets the IO injector (called by the core at startup).
|
||||
|
||||
<small>`plugin.go:595`</small>
|
||||
|
||||
### `PluginSDK.SetInputChannelRegistrar`
|
||||
|
||||
!!! info "桥接装配点"
|
||||
桥接运行时注入点(由 hmapdev 生成的 proc_main 调用,插件业务代码不调用)
|
||||
|
||||
```go
|
||||
func (s *PluginSDK) SetInputChannelRegistrar(r InputChannelRegistrar)
|
||||
```
|
||||
|
||||
SetInputChannelRegistrar sets the input channel registrar (called by the core at startup).
|
||||
|
||||
<small>`plugin.go:588`</small>
|
||||
|
||||
### `PluginSDK.SetKnowledgeAPI`
|
||||
|
||||
!!! info "桥接装配点"
|
||||
桥接运行时注入点(由 hmapdev 生成的 proc_main 调用,插件业务代码不调用)
|
||||
|
||||
```go
|
||||
func (s *PluginSDK) SetKnowledgeAPI(kn KnowledgeAPI)
|
||||
```
|
||||
|
||||
<small>`plugin.go:620`</small>
|
||||
|
||||
### `PluginSDK.SetLLMAPI`
|
||||
|
||||
!!! info "桥接装配点"
|
||||
桥接运行时注入点(由 hmapdev 生成的 proc_main 调用,插件业务代码不调用)
|
||||
|
||||
```go
|
||||
func (s *PluginSDK) SetLLMAPI(llm LLMAPI)
|
||||
```
|
||||
|
||||
<small>`plugin.go:626`</small>
|
||||
|
||||
### `PluginSDK.SetMemoryAPI`
|
||||
|
||||
!!! info "桥接装配点"
|
||||
桥接运行时注入点(由 hmapdev 生成的 proc_main 调用,插件业务代码不调用)
|
||||
|
||||
```go
|
||||
func (s *PluginSDK) SetMemoryAPI(mem MemoryAPI)
|
||||
```
|
||||
|
||||
SetMemoryAPI sets the memory API (called by the core at startup).
|
||||
|
||||
<small>`plugin.go:602`</small>
|
||||
|
||||
### `PluginSDK.SetOutputChannelRegistrar`
|
||||
|
||||
!!! info "桥接装配点"
|
||||
桥接运行时注入点(由 hmapdev 生成的 proc_main 调用,插件业务代码不调用)
|
||||
|
||||
```go
|
||||
func (s *PluginSDK) SetOutputChannelRegistrar(r OutputChannelRegistrar)
|
||||
```
|
||||
|
||||
SetOutputChannelRegistrar sets the output channel registrar (called by the core at startup).
|
||||
|
||||
<small>`plugin.go:574`</small>
|
||||
|
||||
### `PluginSDK.SetOutputChannelUnregistrar`
|
||||
|
||||
!!! warning "仅内核内置插件可用"
|
||||
同上,桥接模板不注入。
|
||||
|
||||
```go
|
||||
func (s *PluginSDK) SetOutputChannelUnregistrar(r OutputChannelUnregistrar)
|
||||
```
|
||||
|
||||
SetOutputChannelUnregistrar sets the output channel unregistrar (called by the core at startup).
|
||||
|
||||
<small>`plugin.go:581`</small>
|
||||
|
||||
### `PluginSDK.SetPluginMgrAPI`
|
||||
|
||||
!!! info "桥接装配点"
|
||||
桥接运行时注入点(由 hmapdev 生成的 proc_main 调用,插件业务代码不调用)
|
||||
|
||||
```go
|
||||
func (s *PluginSDK) SetPluginMgrAPI(pm PluginMgrAPI)
|
||||
```
|
||||
|
||||
SetPluginMgrAPI sets the plugin manager API (called by the bridge at startup).
|
||||
|
||||
<small>`plugin.go:645`</small>
|
||||
|
||||
### `PluginSDK.SetSocialAPI`
|
||||
|
||||
!!! info "桥接装配点"
|
||||
桥接运行时注入点(由 hmapdev 生成的 proc_main 调用,插件业务代码不调用)
|
||||
|
||||
```go
|
||||
func (s *PluginSDK) SetSocialAPI(social SocialAPI)
|
||||
```
|
||||
|
||||
<small>`plugin.go:632`</small>
|
||||
|
||||
### `PluginSDK.SetTextMemoryAPI`
|
||||
|
||||
!!! info "桥接装配点"
|
||||
桥接运行时注入点(由 hmapdev 生成的 proc_main 调用,插件业务代码不调用)
|
||||
|
||||
```go
|
||||
func (s *PluginSDK) SetTextMemoryAPI(tm TextMemoryAPI)
|
||||
```
|
||||
|
||||
<small>`plugin.go:608`</small>
|
||||
|
||||
### `ToolRegistrar`
|
||||
|
||||
```go
|
||||
type ToolRegistrar func(name string, def ToolDef, handler ToolHandler) error
|
||||
```
|
||||
|
||||
ToolRegistrar registers a tool dynamically.
|
||||
|
||||
<small>`plugin.go:305`</small>
|
||||
|
||||
79
docs/api/builtin-only.md
Normal file
79
docs/api/builtin-only.md
Normal file
@ -0,0 +1,79 @@
|
||||
<!-- 本页由 tools/apidoc/gensite 从源码生成,请勿手改;要改文档就改 sdk/*.go 的注释。 -->
|
||||
|
||||
# 仅内置插件可用的 API
|
||||
|
||||
这些 API **存在于公开 SDK 包里**,但在外部(第三方)插件的运行路径上不可用:要么桥接运行时根本不注入它(拿到 nil),要么内核会拒绝/降级。列在这里是为了让边界显式——而不是让你在运行时才发现拿不到。
|
||||
|
||||
判断依据全部来自源码与 `hmapdev` 桥接模板,逐条记在每条说明里。
|
||||
|
||||
### `PriorityL4`
|
||||
|
||||
!!! warning "仅内核内置插件可用"
|
||||
sdk/plugin.go:119-126 明确:L1..L3 任何插件可声明,L4 只有内核级插件可用,外部插件声明会被内核夹到 L3(内核侧有 priority_clamp_test.go 守着)。外部插件文档应说明「声明 L4 无效」。
|
||||
|
||||
```go
|
||||
const PriorityL4
|
||||
```
|
||||
|
||||
PriorityL4 仅内核级(内置)插件可用;外部插件声明会被夹到 L3。
|
||||
|
||||
<small>`plugin.go:127`</small>
|
||||
|
||||
### `PluginSDK.Events`
|
||||
|
||||
!!! warning "仅内核内置插件可用"
|
||||
实测全仓 SetEventSubscriber 只有定义、无任何调用点(grep -rn SetEventSubscriber --include=*.go 仅命中 sdk/plugin.go 的定义与 lua_plugin.go 的一句注释)。外部插件拿到的 Events() 恒为 nil。外部插件订阅事件走的是桥接运行时的 events.subscribe RPC(proc_main.go.tmpl:1421 在插件侧分发、内核 protocol.go MethodEventsSubscribe),Lua 插件则走内部 SDK 的 Subscribe。这正是 PLUGIN_DEV.md 里没有说清的一处。
|
||||
|
||||
```go
|
||||
func (s *PluginSDK) Events() EventSubscriber
|
||||
```
|
||||
|
||||
Events returns the event subscriber for listening to kernel events (may be nil if not available).
|
||||
|
||||
<small>`plugin.go:446`</small>
|
||||
|
||||
### `PluginSDK.SetEventSubscriber`
|
||||
|
||||
!!! warning "仅内核内置插件可用"
|
||||
同上:无调用点。标 builtin 而非 bridge,因为连桥接运行时都不注入它——外部插件无法用它获得事件订阅能力。
|
||||
|
||||
```go
|
||||
func (s *PluginSDK) SetEventSubscriber(es EventSubscriber)
|
||||
```
|
||||
|
||||
<small>`plugin.go:638`</small>
|
||||
|
||||
### `PluginSDK.SetOutputChannelUnregistrar`
|
||||
|
||||
!!! warning "仅内核内置插件可用"
|
||||
同上,桥接模板不注入。
|
||||
|
||||
```go
|
||||
func (s *PluginSDK) SetOutputChannelUnregistrar(r OutputChannelUnregistrar)
|
||||
```
|
||||
|
||||
SetOutputChannelUnregistrar sets the output channel unregistrar (called by the core at startup).
|
||||
|
||||
<small>`plugin.go:581`</small>
|
||||
|
||||
### `PluginSDK.UnregisterOutputChannel`
|
||||
|
||||
!!! warning "仅内核内置插件可用"
|
||||
外部插件的桥接模板只注入 SetOutputChannelRegistrar(proc_main.go.tmpl:693-705),**未**注入 SetOutputChannelUnregistrar(grep Unregistrar 在 templates/ 下无命中)。因此外部插件的 regOutputUnreg 为 nil,UnregisterOutputChannel 会命中 `if reg != nil` 的 else 分支**直接返回 nil**(sdk/plugin.go:539-549)——即**静默无效**:不报错、通道也没注销。内置插件由 internal/sdk/plugin.go:330 注入 cfg.RegOutputUnreg,真正生效。外部插件要让通道下线,只能重载插件。
|
||||
|
||||
```go
|
||||
func (s *PluginSDK) UnregisterOutputChannel(name string) error
|
||||
```
|
||||
|
||||
UnregisterOutputChannel 注销一个输出通道(动态通道随资源生灭时必须调用)。
|
||||
|
||||
<small>`plugin.go:539`</small>
|
||||
|
||||
### `EventSubscriber.Subscribe`
|
||||
|
||||
!!! warning "仅内核内置插件可用"
|
||||
|
||||
```go
|
||||
Subscribe(eventType EventType, handler EventHandler) func()
|
||||
```
|
||||
|
||||
661
docs/api/channels.md
Normal file
661
docs/api/channels.md
Normal file
@ -0,0 +1,661 @@
|
||||
<!-- 本页由 tools/apidoc/gensite 从源码生成,请勿手改;要改文档就改 sdk/*.go 的注释。 -->
|
||||
|
||||
# 输入 / 输出通道
|
||||
|
||||
通道是插件与外界(设备、其他 Agent、外部系统)交换消息的入口。**输入通道**接收外部消息,**输出通道**把消息投递出去。
|
||||
|
||||
## `IOInjector`
|
||||
|
||||
IOInjector provides methods for injecting input and interrupts into the agent pipeline.
|
||||
All methods accept (source, channel) where channel is the target output channel
|
||||
for routing the agent's response.
|
||||
|
||||
| 方法 | 说明 |
|
||||
|---|---|
|
||||
| [`InjectInputMedia`](#ioinjectorinjectinputmedia) | |
|
||||
| [`InjectInputMediaOpts`](#ioinjectorinjectinputmediaopts) | |
|
||||
| [`InjectInputMediaSync`](#ioinjectorinjectinputmediasync) | |
|
||||
| [`InjectInputMediaSyncOpts`](#ioinjectorinjectinputmediasyncopts) | |
|
||||
| [`InjectInputSync`](#ioinjectorinjectinputsync) | InjectInputSync 注入输入事件并同步等待 agent 回复,返回回复文本(无回复时返回空串)。 |
|
||||
| [`InjectInputSyncOpts`](#ioinjectorinjectinputsyncopts) | |
|
||||
| [`InjectInterruptMedia`](#ioinjectorinjectinterruptmedia) | |
|
||||
| [`InjectInterruptMediaOpts`](#ioinjectorinjectinterruptmediaopts) | |
|
||||
| [`InjectInterruptText`](#ioinjectorinjectinterrupttext) | |
|
||||
| [`InjectInterruptTextOpts`](#ioinjectorinjectinterrupttextopts) | |
|
||||
| [`InjectText`](#ioinjectorinjecttext) | |
|
||||
| [`InjectTextNoMemory`](#ioinjectorinjecttextnomemory) | |
|
||||
| [`InjectTextOpts`](#ioinjectorinjecttextopts) | 以下 Opts 变体让调用点在**这一次注入**上声明记忆与裁剪行为。 |
|
||||
| [`SetToolBlocks`](#ioinjectorsettoolblocks) | SetToolBlocks 插件工具注入多模态内容块(image_url/audio_url),内核在下一条 |
|
||||
|
||||
### `IOInjector.InjectInputMedia`
|
||||
|
||||
```go
|
||||
InjectInputMedia(source, channel, text string, blocks []ContentBlock)
|
||||
```
|
||||
|
||||
<small>`plugin.go:230`</small>
|
||||
|
||||
### `IOInjector.InjectInputMediaOpts`
|
||||
|
||||
```go
|
||||
InjectInputMediaOpts(source, channel, text string, blocks []ContentBlock, opts InjectOptions)
|
||||
```
|
||||
|
||||
<small>`plugin.go:241`</small>
|
||||
|
||||
### `IOInjector.InjectInputMediaSync`
|
||||
|
||||
```go
|
||||
InjectInputMediaSync(source, channel, text string, blocks []ContentBlock) string
|
||||
```
|
||||
|
||||
<small>`plugin.go:231`</small>
|
||||
|
||||
### `IOInjector.InjectInputMediaSyncOpts`
|
||||
|
||||
```go
|
||||
InjectInputMediaSyncOpts(source, channel, text string, blocks []ContentBlock, opts InjectOptions) string
|
||||
```
|
||||
|
||||
<small>`plugin.go:242`</small>
|
||||
|
||||
### `IOInjector.InjectInputSync`
|
||||
|
||||
```go
|
||||
InjectInputSync(source, channel, text string) string
|
||||
```
|
||||
|
||||
InjectInputSync 注入输入事件并同步等待 agent 回复,返回回复文本(无回复时返回空串)。
|
||||
用于通道消息的完整闭环:收到入站 → agent 处理 → 回复取回 → 送回通道。
|
||||
|
||||
**示例插件里的真实用法**
|
||||
|
||||
| 插件 | 位置 | 代码 |
|
||||
|---|---|---|
|
||||
| [`a2a`](../examples/index.md#a2a) | `example/a2a/plugin.go:345` | `reply := p.sdk.InjectInputSync(p.name, p.name,` |
|
||||
| [`acp`](../examples/index.md#acp) | `example/acp/plugin.go:237` | `reply = p.sdk.InjectInputSync(p.name, p.name,` |
|
||||
|
||||
<small>`plugin.go:226`</small>
|
||||
|
||||
### `IOInjector.InjectInputSyncOpts`
|
||||
|
||||
```go
|
||||
InjectInputSyncOpts(source, channel, text string, opts InjectOptions) string
|
||||
```
|
||||
|
||||
<small>`plugin.go:240`</small>
|
||||
|
||||
### `IOInjector.InjectInterruptMedia`
|
||||
|
||||
```go
|
||||
InjectInterruptMedia(source, channel, text string, blocks []ContentBlock)
|
||||
```
|
||||
|
||||
<small>`plugin.go:232`</small>
|
||||
|
||||
### `IOInjector.InjectInterruptMediaOpts`
|
||||
|
||||
```go
|
||||
InjectInterruptMediaOpts(source, channel, text string, blocks []ContentBlock, opts InjectOptions)
|
||||
```
|
||||
|
||||
<small>`plugin.go:243`</small>
|
||||
|
||||
### `IOInjector.InjectInterruptText`
|
||||
|
||||
```go
|
||||
InjectInterruptText(source, channel, text string)
|
||||
```
|
||||
|
||||
<small>`plugin.go:221`</small>
|
||||
|
||||
### `IOInjector.InjectInterruptTextOpts`
|
||||
|
||||
```go
|
||||
InjectInterruptTextOpts(source, channel, text string, opts InjectOptions)
|
||||
```
|
||||
|
||||
**示例插件里的真实用法**
|
||||
|
||||
| 插件 | 位置 | 代码 |
|
||||
|---|---|---|
|
||||
| [`browser`](../examples/index.md#browser) | `example/browser/plugin.go:1319` | `p.sdk.InjectInterruptTextOpts(p.name, p.name,` |
|
||||
| [`calendar`](../examples/index.md#calendar) | `example/calendar/plugin.go:527` | `p.sdk.InjectInterruptTextOpts("calendar", "calendar", msg, sdk.InjectOptions{NoMemory: true})` |
|
||||
| [`memo`](../examples/index.md#memo) | `example/memo/plugin.go:299` | `p.sdk.InjectInterruptTextOpts(p.name, p.name,` |
|
||||
| [`qq`](../examples/index.md#qq) | `example/qq/plugin.go:1493` | `p.sdk.InjectInterruptTextOpts(p.name, p.name, text, sdk.InjectOptions{` |
|
||||
|
||||
<small>`plugin.go:239`</small>
|
||||
|
||||
### `IOInjector.InjectText`
|
||||
|
||||
```go
|
||||
InjectText(source, channel, text string)
|
||||
```
|
||||
|
||||
<small>`plugin.go:222`</small>
|
||||
|
||||
### `IOInjector.InjectTextNoMemory`
|
||||
|
||||
```go
|
||||
InjectTextNoMemory(source, channel, text string)
|
||||
```
|
||||
|
||||
**示例插件里的真实用法**
|
||||
|
||||
| 插件 | 位置 | 代码 |
|
||||
|---|---|---|
|
||||
| [`browser`](../examples/index.md#browser) | `example/browser/plugin.go:1114` | `p.sdk.InjectTextNoMemory(p.name, p.name, fmt.Sprintf("[浏览器 %s 已导航到 %s]", id, rawURL))` |
|
||||
|
||||
<small>`plugin.go:223`</small>
|
||||
|
||||
### `IOInjector.InjectTextOpts`
|
||||
|
||||
```go
|
||||
InjectTextOpts(source, channel, text string, opts InjectOptions)
|
||||
```
|
||||
|
||||
以下 Opts 变体让调用点在**这一次注入**上声明记忆与裁剪行为。
|
||||
|
||||
上面那些不带 opts 的方法等价于传零值 InjectOptions(记入记忆 + 不裁剪),
|
||||
保留它们是为了不破坏已有插件;新代码应当用 Opts 变体把意图写清楚。
|
||||
|
||||
<small>`plugin.go:238`</small>
|
||||
|
||||
### `IOInjector.SetToolBlocks`
|
||||
|
||||
```go
|
||||
SetToolBlocks(blocks []ContentBlock)
|
||||
```
|
||||
|
||||
SetToolBlocks 插件工具注入多模态内容块(image_url/audio_url),内核在下一条
|
||||
tool message 的 content 数组里带上这些块,让模型在后续轮次看到图/听到音频。
|
||||
|
||||
<small>`plugin.go:229`</small>
|
||||
|
||||
### `CapAudio`
|
||||
|
||||
```go
|
||||
const CapAudio
|
||||
```
|
||||
|
||||
Output capability flags
|
||||
|
||||
<small>`plugin.go:331`</small>
|
||||
|
||||
### `CapFile`
|
||||
|
||||
```go
|
||||
const CapFile
|
||||
```
|
||||
|
||||
Output capability flags
|
||||
|
||||
<small>`plugin.go:329`</small>
|
||||
|
||||
### `CapImage`
|
||||
|
||||
```go
|
||||
const CapImage
|
||||
```
|
||||
|
||||
Output capability flags
|
||||
|
||||
<small>`plugin.go:330`</small>
|
||||
|
||||
### `CapStructured`
|
||||
|
||||
```go
|
||||
const CapStructured
|
||||
```
|
||||
|
||||
Output capability flags
|
||||
|
||||
<small>`plugin.go:332`</small>
|
||||
|
||||
### `CapText`
|
||||
|
||||
```go
|
||||
const CapText
|
||||
```
|
||||
|
||||
Output capability flags
|
||||
|
||||
<small>`plugin.go:328`</small>
|
||||
|
||||
### `ChannelDef`
|
||||
|
||||
```go
|
||||
type ChannelDef struct { NoMemory bool `json:"no_memory,omitempty"` Cleaner func(string) string `json:"-"` ContextPolicy string `json: …
|
||||
```
|
||||
|
||||
ChannelDef 描述通道在记忆计算层的行为,与 ToolDef.NoMemory/Cleaner 语义一致。
|
||||
NoMemory: 此通道输入/输出不参与记忆计算(向量化/关键词提取/蒸馏),但原文保留在上下文中
|
||||
Cleaner: 计算层过滤函数,不改原文;仅在向量化/jieba/蒸馏/存档提取关键词时调用
|
||||
ContextPolicy: 此通道的输入到达后是否据此裁剪上下文,默认 none(不裁剪)
|
||||
RecallPolicy: 此通道的输入到达后是否据此召回相关记忆,默认 auto(召回)
|
||||
|
||||
JSON tag 是必需的:通道定义要跨进程传给内核,而 Cleaner 是函数(必须忽略)。
|
||||
没有 tag 时既无法整体 marshal(func 不支持),又会诱使调用方手写字段白名单——
|
||||
那样新增字段会被静默丢掉。
|
||||
|
||||
<small>`plugin.go:139`</small>
|
||||
|
||||
### `ContextPolicyNone`
|
||||
|
||||
```go
|
||||
const ContextPolicyNone
|
||||
```
|
||||
|
||||
上下文策略:决定一次工具调用/输入/注入是否依据其内容裁剪上下文。
|
||||
|
||||
默认(空串或 ContextPolicyNone)**不裁剪**:裁剪会归档丢弃低相关事件,
|
||||
必须由工具/通道/注入点显式声明才发生——否则一个只想往上下文里塞内容的
|
||||
插件会在背后把别人的内容挤掉,且看不出是谁干的。
|
||||
|
||||
<small>`plugin.go:44`</small>
|
||||
|
||||
### `ContextPolicyPrune`
|
||||
|
||||
```go
|
||||
const ContextPolicyPrune
|
||||
```
|
||||
|
||||
上下文策略:决定一次工具调用/输入/注入是否依据其内容裁剪上下文。
|
||||
|
||||
默认(空串或 ContextPolicyNone)**不裁剪**:裁剪会归档丢弃低相关事件,
|
||||
必须由工具/通道/注入点显式声明才发生——否则一个只想往上下文里塞内容的
|
||||
插件会在背后把别人的内容挤掉,且看不出是谁干的。
|
||||
|
||||
<small>`plugin.go:45`</small>
|
||||
|
||||
### `IOInjector`
|
||||
|
||||
```go
|
||||
type IOInjector interface { InjectInterruptText(source, channel, text string) InjectText(source, channel, text string) InjectTextNoMemory(source, channel, text string) // I …
|
||||
```
|
||||
|
||||
IOInjector provides methods for injecting input and interrupts into the agent pipeline.
|
||||
All methods accept (source, channel) where channel is the target output channel
|
||||
for routing the agent's response.
|
||||
|
||||
<small>`plugin.go:220`</small>
|
||||
|
||||
### `PluginSDK.InjectInputMedia`
|
||||
|
||||
```go
|
||||
func (s *PluginSDK) InjectInputMedia(source, channel, text string, blocks []ContentBlock)
|
||||
```
|
||||
|
||||
InjectInputMedia 注入带媒体内容块(image_url/audio_url)的输入。
|
||||
blocks 会落进媒体存储被记忆引用捕获,同时作为当前轮 content 数组
|
||||
发给 LLM,让模型在「本轮」就看到图/听到音频——区别于 SetToolBlocks
|
||||
的「下一轮 tool message」语义。
|
||||
等价于 InjectInputMediaOpts(..., InjectOptions{})。
|
||||
|
||||
<small>`plugin.go:701`</small>
|
||||
|
||||
### `PluginSDK.InjectInputMediaOpts`
|
||||
|
||||
```go
|
||||
func (s *PluginSDK) InjectInputMediaOpts(source, channel, text string, blocks []ContentBlock, opts InjectOptions)
|
||||
```
|
||||
|
||||
InjectInputMediaOpts 注入带媒体块的输入,并声明记忆/裁剪行为。
|
||||
|
||||
<small>`plugin.go:740`</small>
|
||||
|
||||
### `PluginSDK.InjectInputMediaSync`
|
||||
|
||||
```go
|
||||
func (s *PluginSDK) InjectInputMediaSync(source, channel, text string, blocks []ContentBlock) string
|
||||
```
|
||||
|
||||
InjectInputMediaSync 注入带媒体内容块的输入并同步等待 agent 回复。
|
||||
等价于 InjectInputMediaSyncOpts(..., InjectOptions{})。
|
||||
|
||||
<small>`plugin.go:707`</small>
|
||||
|
||||
### `PluginSDK.InjectInputMediaSyncOpts`
|
||||
|
||||
```go
|
||||
func (s *PluginSDK) InjectInputMediaSyncOpts(source, channel, text string, blocks []ContentBlock, opts InjectOptions) string
|
||||
```
|
||||
|
||||
InjectInputMediaSyncOpts 注入带媒体块的输入并同步等待回复,同时声明记忆/裁剪行为。
|
||||
|
||||
<small>`plugin.go:747`</small>
|
||||
|
||||
### `PluginSDK.InjectInputSync`
|
||||
|
||||
```go
|
||||
func (s *PluginSDK) InjectInputSync(source, channel, text string) string
|
||||
```
|
||||
|
||||
InjectInputSync injects a text message and synchronously waits for the agent reply,
|
||||
returning the reply text (empty string if none). Replies must be dispatched back
|
||||
to the source channel by the caller.
|
||||
|
||||
**示例插件里的真实用法**
|
||||
|
||||
| 插件 | 位置 | 代码 |
|
||||
|---|---|---|
|
||||
| [`a2a`](../examples/index.md#a2a) | `example/a2a/plugin.go:345` | `reply := p.sdk.InjectInputSync(p.name, p.name,` |
|
||||
| [`acp`](../examples/index.md#acp) | `example/acp/plugin.go:237` | `reply = p.sdk.InjectInputSync(p.name, p.name,` |
|
||||
|
||||
<small>`plugin.go:692`</small>
|
||||
|
||||
### `PluginSDK.InjectInputSyncOpts`
|
||||
|
||||
```go
|
||||
func (s *PluginSDK) InjectInputSyncOpts(source, channel, text string, opts InjectOptions) string
|
||||
```
|
||||
|
||||
InjectInputSyncOpts 注入输入并同步等待回复,同时在这次注入上声明记忆/裁剪行为。
|
||||
|
||||
<small>`plugin.go:731`</small>
|
||||
|
||||
### `PluginSDK.InjectInterruptMedia`
|
||||
|
||||
```go
|
||||
func (s *PluginSDK) InjectInterruptMedia(source, channel, text string, blocks []ContentBlock)
|
||||
```
|
||||
|
||||
InjectInterruptMedia 注入带媒体内容块的中断,可抢占当前 LLM 处理。
|
||||
blocks 随中断消息一起发给模型。
|
||||
|
||||
<small>`plugin.go:764`</small>
|
||||
|
||||
### `PluginSDK.InjectInterruptMediaOpts`
|
||||
|
||||
```go
|
||||
func (s *PluginSDK) InjectInterruptMediaOpts(source, channel, text string, blocks []ContentBlock, opts InjectOptions)
|
||||
```
|
||||
|
||||
InjectInterruptMediaOpts 注入带媒体块的中断,并声明记忆/裁剪行为。
|
||||
|
||||
<small>`plugin.go:756`</small>
|
||||
|
||||
### `PluginSDK.InjectInterruptText`
|
||||
|
||||
```go
|
||||
func (s *PluginSDK) InjectInterruptText(source, channel, text string)
|
||||
```
|
||||
|
||||
InjectInterruptText injects a text interrupt that can preempt current LLM processing.
|
||||
等价于 InjectInterruptTextOpts(..., InjectOptions{}):记入记忆、不裁剪。
|
||||
|
||||
<small>`plugin.go:673`</small>
|
||||
|
||||
### `PluginSDK.InjectInterruptTextOpts`
|
||||
|
||||
```go
|
||||
func (s *PluginSDK) InjectInterruptTextOpts(source, channel, text string, opts InjectOptions)
|
||||
```
|
||||
|
||||
InjectInterruptTextOpts 注入可抢占当前处理的中断文本。
|
||||
|
||||
中断也允许声明 ContextPolicyPrune:中断同样携带内容进入上下文,
|
||||
是否需要据此裁剪由调用方决定(默认不裁剪)。
|
||||
|
||||
**示例插件里的真实用法**
|
||||
|
||||
| 插件 | 位置 | 代码 |
|
||||
|---|---|---|
|
||||
| [`browser`](../examples/index.md#browser) | `example/browser/plugin.go:1319` | `p.sdk.InjectInterruptTextOpts(p.name, p.name,` |
|
||||
| [`calendar`](../examples/index.md#calendar) | `example/calendar/plugin.go:527` | `p.sdk.InjectInterruptTextOpts("calendar", "calendar", msg, sdk.InjectOptions{NoMemory: true})` |
|
||||
| [`memo`](../examples/index.md#memo) | `example/memo/plugin.go:299` | `p.sdk.InjectInterruptTextOpts(p.name, p.name,` |
|
||||
| [`qq`](../examples/index.md#qq) | `example/qq/plugin.go:1493` | `p.sdk.InjectInterruptTextOpts(p.name, p.name, text, sdk.InjectOptions{` |
|
||||
|
||||
<small>`plugin.go:724`</small>
|
||||
|
||||
### `InjectOptions`
|
||||
|
||||
```go
|
||||
type InjectOptions struct { NoMemory bool ContextPolicy string // RecallPolicy 声明此次注入是否据其内容召回相关记忆。 // 空串 = 默认(输入/注<><E6B3A8> …
|
||||
```
|
||||
|
||||
InjectOptions 声明一次注入行为在记忆层与上下文层的表现。
|
||||
|
||||
零值 = 记入记忆 + 不裁剪上下文,与历史行为(三参数注入方法)完全一致,
|
||||
因此调用方只有在确实需要改变行为时才需要填它。
|
||||
|
||||
为什么注入也要这两个标志:注入的内容来源千差万别——轮询到的频道消息
|
||||
属于真实对话(该记),而“任务还在跑”“连接已重连”这类提醒不该污染记忆,
|
||||
也不该把上下文按它的内容裁一遍。按调用点声明比按通道一刀切准确。
|
||||
|
||||
NoMemory: 此次注入不参与记忆计算(向量化/关键词提取/蒸馏),原文仍留在上下文
|
||||
ContextPolicy: 此次注入后是否依据(清洗后的)内容裁剪上下文;默认不裁剪。
|
||||
RecallPolicy: 此次注入是否依据(清洗后的)内容召回相关记忆;默认 auto(召回)。
|
||||
|
||||
中断注入也允许声明 prune——它同样会携带内容进入上下文。
|
||||
|
||||
CleanerName: 此次注入的内容用哪个**已注册的通道 cleaner** 清洗。
|
||||
|
||||
空串 = 按注入的 source 查通道定义(既有行为)。
|
||||
为什么要能显式指定:注入的 source 未必是注册过的输入通道名,
|
||||
而注入内容往往带 ANSI/JSON 包装,需要清洗后才是有效内容;
|
||||
不指定就只能退到「按 source 查不到就不清洗」。
|
||||
|
||||
<small>`plugin.go:98`</small>
|
||||
|
||||
### `PluginSDK.InjectText`
|
||||
|
||||
```go
|
||||
func (s *PluginSDK) InjectText(source, channel, text string)
|
||||
```
|
||||
|
||||
InjectText injects a text message into the agent pipeline.
|
||||
等价于 InjectTextOpts(..., InjectOptions{}):记入记忆、不裁剪。
|
||||
|
||||
<small>`plugin.go:679`</small>
|
||||
|
||||
### `PluginSDK.InjectTextNoMemory`
|
||||
|
||||
```go
|
||||
func (s *PluginSDK) InjectTextNoMemory(source, channel, text string)
|
||||
```
|
||||
|
||||
InjectTextNoMemory injects a text message without generating memory.
|
||||
等价于 InjectTextOpts(..., InjectOptions{NoMemory: true})。
|
||||
|
||||
**示例插件里的真实用法**
|
||||
|
||||
| 插件 | 位置 | 代码 |
|
||||
|---|---|---|
|
||||
| [`browser`](../examples/index.md#browser) | `example/browser/plugin.go:1114` | `p.sdk.InjectTextNoMemory(p.name, p.name, fmt.Sprintf("[浏览器 %s 已导航到 %s]", id, rawURL))` |
|
||||
|
||||
<small>`plugin.go:685`</small>
|
||||
|
||||
### `PluginSDK.InjectTextOpts`
|
||||
|
||||
```go
|
||||
func (s *PluginSDK) InjectTextOpts(source, channel, text string, opts InjectOptions)
|
||||
```
|
||||
|
||||
InjectTextOpts 注入文本到 agent,并在这一次注入上声明记忆与裁剪行为。
|
||||
|
||||
<small>`plugin.go:714`</small>
|
||||
|
||||
### `PriorityL1`
|
||||
|
||||
```go
|
||||
const PriorityL1
|
||||
```
|
||||
|
||||
中断优先级取值。
|
||||
|
||||
L1..L3 任何插件都可声明;**L4 只有内核级插件**(编译期内置插件,
|
||||
如 cli/webui/timer)才能声明——它用于实现真正的“立即打断”能力,
|
||||
例如 WebUI 的终止按钮。外部插件(走 proc 桥)声明 L4 会被内核夹到 L3。
|
||||
|
||||
<small>`plugin.go:123`</small>
|
||||
|
||||
### `PriorityL2`
|
||||
|
||||
```go
|
||||
const PriorityL2
|
||||
```
|
||||
|
||||
中断优先级取值。
|
||||
|
||||
L1..L3 任何插件都可声明;**L4 只有内核级插件**(编译期内置插件,
|
||||
如 cli/webui/timer)才能声明——它用于实现真正的“立即打断”能力,
|
||||
例如 WebUI 的终止按钮。外部插件(走 proc 桥)声明 L4 会被内核夹到 L3。
|
||||
|
||||
<small>`plugin.go:124`</small>
|
||||
|
||||
### `PriorityL3`
|
||||
|
||||
```go
|
||||
const PriorityL3
|
||||
```
|
||||
|
||||
中断优先级取值。
|
||||
|
||||
L1..L3 任何插件都可声明;**L4 只有内核级插件**(编译期内置插件,
|
||||
如 cli/webui/timer)才能声明——它用于实现真正的“立即打断”能力,
|
||||
例如 WebUI 的终止按钮。外部插件(走 proc 桥)声明 L4 会被内核夹到 L3。
|
||||
|
||||
<small>`plugin.go:125`</small>
|
||||
|
||||
### `PriorityL4`
|
||||
|
||||
!!! warning "仅内核内置插件可用"
|
||||
sdk/plugin.go:119-126 明确:L1..L3 任何插件可声明,L4 只有内核级插件可用,外部插件声明会被内核夹到 L3(内核侧有 priority_clamp_test.go 守着)。外部插件文档应说明「声明 L4 无效」。
|
||||
|
||||
```go
|
||||
const PriorityL4
|
||||
```
|
||||
|
||||
PriorityL4 仅内核级(内置)插件可用;外部插件声明会被夹到 L3。
|
||||
|
||||
<small>`plugin.go:127`</small>
|
||||
|
||||
### `RecallPolicyAuto`
|
||||
|
||||
```go
|
||||
const RecallPolicyAuto
|
||||
```
|
||||
|
||||
召回策略:决定一次工具调用/输入/注入是否据其内容**召回**(注入)相关记忆。
|
||||
|
||||
与 ContextPolicy **正交**:ContextPolicy 管「裁剪」(把低相关 L0 事件归档),
|
||||
RecallPolicy 管「召回」(把 L2/L3 的相关记忆注入本轮)。两者默认值刻意相反——
|
||||
裁剪是破坏性的,默认关(必须显式声明);召回是只读增量、日常对话本就需要,
|
||||
默认 auto(输入/注入),仅**工具**默认 none(工具输出多为噪声,按需声明)。
|
||||
|
||||
<small>`plugin.go:65`</small>
|
||||
|
||||
### `RecallPolicyNone`
|
||||
|
||||
```go
|
||||
const RecallPolicyNone
|
||||
```
|
||||
|
||||
召回策略:决定一次工具调用/输入/注入是否据其内容**召回**(注入)相关记忆。
|
||||
|
||||
与 ContextPolicy **正交**:ContextPolicy 管「裁剪」(把低相关 L0 事件归档),
|
||||
RecallPolicy 管「召回」(把 L2/L3 的相关记忆注入本轮)。两者默认值刻意相反——
|
||||
裁剪是破坏性的,默认关(必须显式声明);召回是只读增量、日常对话本就需要,
|
||||
默认 auto(输入/注入),仅**工具**默认 none(工具输出多为噪声,按需声明)。
|
||||
|
||||
<small>`plugin.go:64`</small>
|
||||
|
||||
### `PluginSDK.RegisterInputChannel`
|
||||
|
||||
```go
|
||||
func (s *PluginSDK) RegisterInputChannel(name string, def ChannelDef) error
|
||||
```
|
||||
|
||||
RegisterInputChannel registers an input channel with its memory behavior.
|
||||
|
||||
契约:**凡是用 InjectText*/InjectInput*/InjectInterrupt*(source, "<name>", ...)
|
||||
注入的通道名,都应当在这里登记**。inputch 是内核里最基本的**输入路由单位**:
|
||||
只有登记过的通道才能在 inputch 登记表里出现,父 agent 才能"把某个 inputch 划给驻留子";
|
||||
没登记就划分会直接失败(`inputch 未注册`)。
|
||||
|
||||
只登记输出通道(RegisterOutputChannel)而没登记输入通道时,内核会兜底登记同名
|
||||
inputch 并打告警日志 —— 兜底只为兼容老插件,新插件请显式登记。
|
||||
|
||||
def.NoMemory: 此通道输入不参与记忆计算
|
||||
def.Cleaner: 计算层对输入文本清洗后(不改原文)再向量化/提关键词
|
||||
|
||||
**示例插件里的真实用法**
|
||||
|
||||
| 插件 | 位置 | 代码 |
|
||||
|---|---|---|
|
||||
| [`a2a`](../examples/index.md#a2a) | `example/a2a/plugin.go:52` | `_ = s.RegisterInputChannel(p.name, sdk.ChannelDef{})` |
|
||||
| [`acp`](../examples/index.md#acp) | `example/acp/plugin.go:52` | `_ = s.RegisterInputChannel(p.name, sdk.ChannelDef{})` |
|
||||
| [`browser`](../examples/index.md#browser) | `example/browser/plugin.go:209` | `_ = s.RegisterInputChannel(p.name, sdk.ChannelDef{})` |
|
||||
| [`calendar`](../examples/index.md#calendar) | `example/calendar/plugin.go:281` | `_ = s.RegisterInputChannel("calendar", sdk.ChannelDef{NoMemory: true})` |
|
||||
|
||||
<small>`plugin.go:561`</small>
|
||||
|
||||
### `PluginSDK.RegisterOutputChannel`
|
||||
|
||||
```go
|
||||
func (s *PluginSDK) RegisterOutputChannel(name string, caps int, desc string, def ChannelDef, handler ToolHandler) error
|
||||
```
|
||||
|
||||
RegisterOutputChannel registers an output channel that the output_send tool can route to.
|
||||
|
||||
与 RegisterInputChannel 的分工:本函数声明**出站**(output_send__<name> 的回复发给谁);
|
||||
入站(谁会往 <name> 注入输入)是另一件事,用 RegisterInputChannel 声明。
|
||||
若该通道同时也是你的注入入口,两个都要登记。
|
||||
|
||||
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.)
|
||||
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)
|
||||
|
||||
**示例插件里的真实用法**
|
||||
|
||||
| 插件 | 位置 | 代码 |
|
||||
|---|---|---|
|
||||
| [`a2a`](../examples/index.md#a2a) | `example/a2a/plugin.go:57` | `if err := s.RegisterOutputChannel(p.name, 1, "A2A Agent 互联通道(外部 agent 查询的回复由此返回)", sdk.ChannelDef{}, func(args…` |
|
||||
| [`acp`](../examples/index.md#acp) | `example/acp/plugin.go:57` | `s.RegisterOutputChannel(p.name, 1, "ACP Agent 互联通道(外部 agent 会话的回复由此返回)", sdk.ChannelDef{}, func(args map[strin…` |
|
||||
| [`qq`](../examples/index.md#qq) | `example/qq/plugin.go:411` | `s.RegisterOutputChannel("qq", sdk.CapText\|sdk.CapFile\|sdk.CapImage\|sdk.CapAudio,` |
|
||||
| [`weather`](../examples/index.md#weather) | `example/weather/plugin.go:104` | `if err := s.RegisterOutputChannel(tp+"weather_out", 0, "push weather to user", sdk.ChannelDef{` |
|
||||
|
||||
<small>`plugin.go:528`</small>
|
||||
|
||||
### `PluginSDK.SetToolBlocks`
|
||||
|
||||
```go
|
||||
func (s *PluginSDK) SetToolBlocks(blocks []ContentBlock)
|
||||
```
|
||||
|
||||
SetToolBlocks 在工具处理函数内注入多模态内容块,内核在下一条 tool message
|
||||
的 content 数组里带上它们。需要「本轮就让模型看到」时用 InjectInputMedia。
|
||||
|
||||
<small>`plugin.go:772`</small>
|
||||
|
||||
### `ValidContextPolicy`
|
||||
|
||||
```go
|
||||
func ValidContextPolicy(policy string) bool
|
||||
```
|
||||
|
||||
ValidContextPolicy 校验策略取值;空串等价于 ContextPolicyNone。
|
||||
|
||||
<small>`plugin.go:49`</small>
|
||||
|
||||
### `ValidRecallPolicy`
|
||||
|
||||
```go
|
||||
func ValidRecallPolicy(policy string) bool
|
||||
```
|
||||
|
||||
ValidRecallPolicy 校验召回策略取值;空串按调用面取默认值。
|
||||
|
||||
<small>`plugin.go:69`</small>
|
||||
|
||||
72
docs/api/constants.md
Normal file
72
docs/api/constants.md
Normal file
@ -0,0 +1,72 @@
|
||||
<!-- 本页由 tools/apidoc/gensite 从源码生成,请勿手改;要改文档就改 sdk/*.go 的注释。 -->
|
||||
|
||||
# 常量与枚举
|
||||
|
||||
SDK 里的取值枚举。其中带「仅内置」标注的取值在内核侧会被夹到较低级别。
|
||||
|
||||
## StageOnInput 等
|
||||
|
||||
| 名称 | 说明 |
|
||||
|---|---|
|
||||
| `StageOnInput` | |
|
||||
| `StagePreAction` | |
|
||||
| `StagePostAction` | |
|
||||
| `StageBeforeToolcall` | |
|
||||
| `StageAfterToolcall` | |
|
||||
| `StageBeforeOutput` | |
|
||||
| `StageAfterOutput` | |
|
||||
|
||||
## ContextPolicyNone 等
|
||||
|
||||
| 名称 | 说明 |
|
||||
|---|---|
|
||||
| `ContextPolicyNone` | |
|
||||
| `ContextPolicyPrune` | |
|
||||
|
||||
## RecallPolicyNone 等
|
||||
|
||||
| 名称 | 说明 |
|
||||
|---|---|
|
||||
| `RecallPolicyNone` | |
|
||||
| `RecallPolicyAuto` | |
|
||||
|
||||
## PriorityL1 等
|
||||
|
||||
| 名称 | 说明 |
|
||||
|---|---|
|
||||
| `PriorityL1` | |
|
||||
| `PriorityL2` | |
|
||||
| `PriorityL3` | |
|
||||
| `PriorityL4` | PriorityL4 仅内核级(内置)插件可用;外部插件声明会被夹到 L3。 |
|
||||
|
||||
## EventRawInput 等
|
||||
|
||||
| 名称 | 说明 |
|
||||
|---|---|
|
||||
| `EventRawInput` | |
|
||||
| `EventAgentOutput` | |
|
||||
| `EventAgentLLMChain` | |
|
||||
| `EventToolCall` | |
|
||||
| `EventReasoning` | |
|
||||
| `EventStage` | |
|
||||
| `EventSystem` | |
|
||||
| `EventReasoningDelta` | 流式增量事件(token 级):核心 process() 流式化后每收到一个增量块发布。 |
|
||||
| `EventContentDelta` | |
|
||||
|
||||
## StageScopeGlobal 等
|
||||
|
||||
| 名称 | 说明 |
|
||||
|---|---|
|
||||
| `StageScopeGlobal` | StageScopeGlobal receives all stage events (default). |
|
||||
| `StageScopeOwnTools` | StageScopeOwnTools only receives events for this plugin's own tool calls |
|
||||
|
||||
## CapText 等
|
||||
|
||||
| 名称 | 说明 |
|
||||
|---|---|
|
||||
| `CapText` | |
|
||||
| `CapFile` | |
|
||||
| `CapImage` | |
|
||||
| `CapAudio` | |
|
||||
| `CapStructured` | |
|
||||
|
||||
81
docs/api/events.md
Normal file
81
docs/api/events.md
Normal file
@ -0,0 +1,81 @@
|
||||
<!-- 本页由 tools/apidoc/gensite 从源码生成,请勿手改;要改文档就改 sdk/*.go 的注释。 -->
|
||||
|
||||
# 事件(Events)
|
||||
|
||||
订阅内核事件。**注意**:外部分布式插件的事件订阅不走 `Events()`(该接口在外部插件路径上未被注入,恒为 nil),而是由 `hmapdev` 生成的运行时通过 `events.subscribe` 完成。详见下方说明。
|
||||
|
||||
## `EventSubscriber`
|
||||
|
||||
EventSubscriber allows plugins to subscribe to kernel events.
|
||||
This is a restricted interface: plugins can subscribe but the kernel
|
||||
controls which events are delivered.
|
||||
|
||||
| 方法 | 说明 |
|
||||
|---|---|
|
||||
| [`Subscribe`](#eventsubscribersubscribe) | |
|
||||
|
||||
### `EventSubscriber.Subscribe`
|
||||
|
||||
!!! warning "仅内核内置插件可用"
|
||||
|
||||
```go
|
||||
Subscribe(eventType EventType, handler EventHandler) func()
|
||||
```
|
||||
|
||||
<small>`plugin.go:279`</small>
|
||||
|
||||
### `Event`
|
||||
|
||||
```go
|
||||
type Event struct { Type EventType `json:"type"` Source string `json:"source"` Payload map[string]interface{} `json:"payload"` T …
|
||||
```
|
||||
|
||||
Event represents a system event published by the kernel.
|
||||
|
||||
<small>`plugin.go:265`</small>
|
||||
|
||||
### `EventHandler`
|
||||
|
||||
```go
|
||||
type EventHandler func(evt *Event)
|
||||
```
|
||||
|
||||
EventHandler processes a system event.
|
||||
|
||||
<small>`plugin.go:273`</small>
|
||||
|
||||
### `EventSubscriber`
|
||||
|
||||
```go
|
||||
type EventSubscriber interface { Subscribe(eventType EventType, handler EventHandler) func() }
|
||||
```
|
||||
|
||||
EventSubscriber allows plugins to subscribe to kernel events.
|
||||
This is a restricted interface: plugins can subscribe but the kernel
|
||||
controls which events are delivered.
|
||||
|
||||
<small>`plugin.go:278`</small>
|
||||
|
||||
### `EventType`
|
||||
|
||||
```go
|
||||
type EventType string
|
||||
```
|
||||
|
||||
EventType identifies the kind of system event.
|
||||
|
||||
<small>`plugin.go:247`</small>
|
||||
|
||||
### `PluginSDK.Events`
|
||||
|
||||
!!! warning "仅内核内置插件可用"
|
||||
实测全仓 SetEventSubscriber 只有定义、无任何调用点(grep -rn SetEventSubscriber --include=*.go 仅命中 sdk/plugin.go 的定义与 lua_plugin.go 的一句注释)。外部插件拿到的 Events() 恒为 nil。外部插件订阅事件走的是桥接运行时的 events.subscribe RPC(proc_main.go.tmpl:1421 在插件侧分发、内核 protocol.go MethodEventsSubscribe),Lua 插件则走内部 SDK 的 Subscribe。这正是 PLUGIN_DEV.md 里没有说清的一处。
|
||||
|
||||
```go
|
||||
func (s *PluginSDK) Events() EventSubscriber
|
||||
```
|
||||
|
||||
Events returns the event subscriber for listening to kernel events (may be nil if not available).
|
||||
|
||||
<small>`plugin.go:446`</small>
|
||||
|
||||
39
docs/api/index.md
Normal file
39
docs/api/index.md
Normal file
@ -0,0 +1,39 @@
|
||||
# API 参考
|
||||
|
||||
本页所有内容**从源码生成**(`tools/apidoc`),签名与说明直接取自 `sdk/*.go` 的
|
||||
文档注释。因此不存在「文档写了一套、代码是另一套」的情况——发现不一致时,
|
||||
改的是源码注释,不是这里。
|
||||
|
||||
## 怎么找 API
|
||||
|
||||
<div id="api-search"></div>
|
||||
|
||||
用上面的搜索框可以:
|
||||
|
||||
- **按名称搜**:`InjectText`、`RegisterTool`、`memory.recall`
|
||||
- **按描述搜**:`注册工具`、`注入`、`重载`、`崩溃`
|
||||
- **按签名搜**:`(string) error`、`[]ContentBlock`
|
||||
- 带 <span class="api-badge api-badge-builtin">仅内置</span>
|
||||
标记的条目在**外部插件里拿不到**,多数情况下你不需要它
|
||||
|
||||
## 章节划分
|
||||
|
||||
按「你想做什么」组织,不是按 Go 的符号类别:
|
||||
|
||||
| 章节 | 内容 |
|
||||
|---|---|
|
||||
| [工具(Tools)](tools.md) | 注册 LLM 可调用的工具——插件最常用的能力形态 |
|
||||
| [阶段钩子(Stages)](stages.md) | 在处理管道的固定点位插入逻辑 |
|
||||
| [记忆(Memory)](memory.md) | 三层记忆的读写:图 / 文档 / 文本,以及知识库 |
|
||||
| [输入/输出通道](channels.md) | 与外界交换消息,以及往流水线里注入内容 |
|
||||
| [配置(Settings)](settings.md) | 声明插件配置项,内核渲染到 WebUI |
|
||||
| [生命周期(Lifecycle)](lifecycle.md) | 启动、停止、卸载、自动重启 |
|
||||
| [事件(Events)](events.md) | 订阅内核事件 |
|
||||
| [LLM 调用](llm.md) | 插件主动调用模型 |
|
||||
| [常量与枚举](constants.md) | 取值枚举 |
|
||||
| [桥接装配点](bridge.md) | 由 `hmapdev` 生成的运行时调用,插件业务代码不碰 |
|
||||
| [仅内置插件可用](builtin-only.md) | 边界汇总——外部插件拿不到的 API 全在这里 |
|
||||
|
||||
!!! tip "先看「能力边界」能省很多时间"
|
||||
如果你正在设计插件,先读 [能力边界](../guide/capability-boundary.md):
|
||||
它说明哪些能力外部插件有、哪些没有,以及**为什么**。
|
||||
230
docs/api/lifecycle.md
Normal file
230
docs/api/lifecycle.md
Normal file
@ -0,0 +1,230 @@
|
||||
<!-- 本页由 tools/apidoc/gensite 从源码生成,请勿手改;要改文档就改 sdk/*.go 的注释。 -->
|
||||
|
||||
# 生命周期(Lifecycle)
|
||||
|
||||
插件的启动、停止与卸载回调。停止与卸载是两件事:**停止**是进程/加载状态变化,**卸载**(onRemove)是插件被删除前的清理机会。
|
||||
|
||||
## `Plugin`
|
||||
|
||||
Plugin is the interface every plugin must implement.
|
||||
|
||||
| 方法 | 说明 |
|
||||
|---|---|
|
||||
| [`Name`](#pluginname) | |
|
||||
| [`Start`](#pluginstart) | |
|
||||
| [`Stop`](#pluginstop) | |
|
||||
|
||||
### `Plugin.Name`
|
||||
|
||||
```go
|
||||
Name() string
|
||||
```
|
||||
|
||||
<small>`plugin.go:14`</small>
|
||||
|
||||
### `Plugin.Start`
|
||||
|
||||
```go
|
||||
Start(sdk *PluginSDK) error
|
||||
```
|
||||
|
||||
<small>`plugin.go:15`</small>
|
||||
|
||||
### `Plugin.Stop`
|
||||
|
||||
```go
|
||||
Stop() error
|
||||
```
|
||||
|
||||
<small>`plugin.go:16`</small>
|
||||
|
||||
## `PluginMgrAPI`
|
||||
|
||||
PluginMgrAPI 提供插件管理能力(外部插件可调用)。
|
||||
由 bridge 注入 dispatch 实现,走 C ABI CORE_PLUGIN_RELOAD_ONE 等。
|
||||
|
||||
| 方法 | 说明 |
|
||||
|---|---|
|
||||
| [`IsPluginDisabled`](#pluginmgrapiisplugindisabled) | IsPluginDisabled 查询插件是否被禁用。 |
|
||||
| [`ListLoadedPlugins`](#pluginmgrapilistloadedplugins) | ListLoadedPlugins 列出已加载插件。 |
|
||||
| [`ReloadOne`](#pluginmgrapireloadone) | ReloadOne 重载单个插件(停止后重新加载)。 |
|
||||
|
||||
### `PluginMgrAPI.IsPluginDisabled`
|
||||
|
||||
```go
|
||||
IsPluginDisabled(name string) bool
|
||||
```
|
||||
|
||||
IsPluginDisabled 查询插件是否被禁用。
|
||||
|
||||
<small>`plugin.go:290`</small>
|
||||
|
||||
### `PluginMgrAPI.ListLoadedPlugins`
|
||||
|
||||
```go
|
||||
ListLoadedPlugins() []string
|
||||
```
|
||||
|
||||
ListLoadedPlugins 列出已加载插件。
|
||||
|
||||
<small>`plugin.go:288`</small>
|
||||
|
||||
### `PluginMgrAPI.ReloadOne`
|
||||
|
||||
```go
|
||||
ReloadOne(name string) error
|
||||
```
|
||||
|
||||
ReloadOne 重载单个插件(停止后重新加载)。
|
||||
|
||||
<small>`plugin.go:286`</small>
|
||||
|
||||
### `PluginSDK.AutoRestart`
|
||||
|
||||
```go
|
||||
func (s *PluginSDK) AutoRestart() bool
|
||||
```
|
||||
|
||||
AutoRestart 返回插件是否允许自动重启。
|
||||
|
||||
<small>`plugin.go:791`</small>
|
||||
|
||||
### `Plugin`
|
||||
|
||||
```go
|
||||
type Plugin interface { Name() string Start(sdk *PluginSDK) error Stop() error }
|
||||
```
|
||||
|
||||
Plugin is the interface every plugin must implement.
|
||||
|
||||
<small>`plugin.go:13`</small>
|
||||
|
||||
### `PluginSDK.PluginMgr`
|
||||
|
||||
```go
|
||||
func (s *PluginSDK) PluginMgr() PluginMgrAPI
|
||||
```
|
||||
|
||||
PluginMgr returns the plugin manager API (ReloadOne / ReloadPlugins / list).
|
||||
May be nil if the host did not wire it.
|
||||
|
||||
<small>`plugin.go:653`</small>
|
||||
|
||||
### `PluginMgrAPI`
|
||||
|
||||
```go
|
||||
type PluginMgrAPI interface { // ReloadOne 重载单个插件(停止后重新加载)。 ReloadOne(name string) error // ListLoadedPlugins 列出已加载插件。 ListLoa …
|
||||
```
|
||||
|
||||
PluginMgrAPI 提供插件管理能力(外部插件可调用)。
|
||||
由 bridge 注入 dispatch 实现,走 C ABI CORE_PLUGIN_RELOAD_ONE 等。
|
||||
|
||||
<small>`plugin.go:284`</small>
|
||||
|
||||
### `PluginSDK.PluginName`
|
||||
|
||||
```go
|
||||
func (s *PluginSDK) PluginName() string
|
||||
```
|
||||
|
||||
PluginName returns the name of the plugin.
|
||||
|
||||
<small>`plugin.go:397`</small>
|
||||
|
||||
### `PluginSDK.RegisterOnRemoveHandler`
|
||||
|
||||
```go
|
||||
func (s *PluginSDK) RegisterOnRemoveHandler(fn func())
|
||||
```
|
||||
|
||||
RegisterOnRemoveHandler 注册插件被删除(卸载)时的清理回调。
|
||||
注册的 handler 会在插件目录被移除前按"后注册先执行"的顺序调用,
|
||||
适用于清理外部资源、删除配置表、下线状态等删除后处理。
|
||||
可注册多个;执行后清空(一次删除只执行一次)。
|
||||
|
||||
**示例插件里的真实用法**
|
||||
|
||||
| 插件 | 位置 | 代码 |
|
||||
|---|---|---|
|
||||
| [`calendar`](../examples/index.md#calendar) | `example/calendar/plugin.go:296` | `s.RegisterOnRemoveHandler(p.cleanupData)` |
|
||||
| [`memo`](../examples/index.md#memo) | `example/memo/plugin.go:69` | `s.RegisterOnRemoveHandler(p.cleanupData)` |
|
||||
| [`rss`](../examples/index.md#rss) | `example/rss/plugin.go:127` | `s.RegisterOnRemoveHandler(p.cleanupData)` |
|
||||
|
||||
<small>`plugin.go:826`</small>
|
||||
|
||||
### `PluginSDK.RegisterPluginAPI`
|
||||
|
||||
```go
|
||||
func (s *PluginSDK) RegisterPluginAPI(name string) error
|
||||
```
|
||||
|
||||
RegisterPluginAPI registers this plugin's API for access by other plugins.
|
||||
|
||||
<small>`plugin.go:502`</small>
|
||||
|
||||
### `PluginSDK.RegisterStopHandler`
|
||||
|
||||
```go
|
||||
func (s *PluginSDK) RegisterStopHandler(fn func())
|
||||
```
|
||||
|
||||
RegisterStopHandler 注册插件停止阶段的清理回调。
|
||||
注册的 handler 会在插件 Stop() 之前按"后注册先执行"的顺序调用,
|
||||
适用于释放资源、落盘状态、关闭子进程等停止时清理操作。
|
||||
可注册多个;执行后清空(进程停止前只执行一次)。
|
||||
|
||||
**示例插件里的真实用法**
|
||||
|
||||
| 插件 | 位置 | 代码 |
|
||||
|---|---|---|
|
||||
| [`calendar`](../examples/index.md#calendar) | `example/calendar/plugin.go:294` | `s.RegisterStopHandler(p.saveEvents)` |
|
||||
| [`deepsearch`](../examples/index.md#deepsearch) | `example/deepsearch/plugin.go:695` | `s.RegisterStopHandler(func() { p.shutdownSearxng() })` |
|
||||
|
||||
<small>`plugin.go:801`</small>
|
||||
|
||||
### `PluginSDK.RunOnRemoveHandlers`
|
||||
|
||||
```go
|
||||
func (s *PluginSDK) RunOnRemoveHandlers()
|
||||
```
|
||||
|
||||
RunOnRemoveHandlers 执行全部已注册的 onRemove handler(后注册先执行,执行后清空,幂等)。
|
||||
由内核在卸载插件(registry.RemovePlugin)时、插件 Stop() 之后执行。
|
||||
|
||||
<small>`plugin.go:837`</small>
|
||||
|
||||
### `PluginSDK.RunStopHandlers`
|
||||
|
||||
```go
|
||||
func (s *PluginSDK) RunStopHandlers()
|
||||
```
|
||||
|
||||
RunStopHandlers 执行全部已注册的 stop handler(后注册先执行,执行后清空,幂等)。
|
||||
由内核(内置插件)或插件桥接层(外部插件 z_bridge 的 StopPlugin)在调用插件 Stop() 前执行。
|
||||
|
||||
<small>`plugin.go:812`</small>
|
||||
|
||||
### `PluginSDK.SetAutoRestart`
|
||||
|
||||
```go
|
||||
func (s *PluginSDK) SetAutoRestart(enabled bool)
|
||||
```
|
||||
|
||||
SetAutoRestart 设置插件崩溃后内核是否自动重启它。
|
||||
默认 true。如果插件有无法恢复的状态(如外部连接),应设为 false。
|
||||
|
||||
重启是有限度的:线性退避(第 n 次等 n×1s,即 1s→2s→3s),
|
||||
且同一 5 分钟窗口内第 4 次崩溃就停下不再拉起(详见 README)。
|
||||
注意这与「重载」(换 plugin.bin 后重新加载)是两回事。
|
||||
|
||||
**示例插件里的真实用法**
|
||||
|
||||
| 插件 | 位置 | 代码 |
|
||||
|---|---|---|
|
||||
| [`a2a`](../examples/index.md#a2a) | `example/a2a/plugin.go:47` | `s.SetAutoRestart(true)` |
|
||||
| [`acp`](../examples/index.md#acp) | `example/acp/plugin.go:47` | `s.SetAutoRestart(true)` |
|
||||
| [`ai_image`](../examples/index.md#ai_image) | `example/ai_image/plugin.go:110` | `s.SetAutoRestart(true)` |
|
||||
| [`bili`](../examples/index.md#bili) | `example/bili/plugin.go:25` | `s.SetAutoRestart(true)` |
|
||||
|
||||
<small>`plugin.go:784`</small>
|
||||
|
||||
60
docs/api/llm.md
Normal file
60
docs/api/llm.md
Normal file
@ -0,0 +1,60 @@
|
||||
<!-- 本页由 tools/apidoc/gensite 从源码生成,请勿手改;要改文档就改 sdk/*.go 的注释。 -->
|
||||
|
||||
# LLM 调用
|
||||
|
||||
让插件自己调用模型(而不是只等模型来调你)。
|
||||
|
||||
## `LLMAPI`
|
||||
|
||||
LLMAPI provides access to the LLM provider manager.
|
||||
|
||||
| 方法 | 说明 |
|
||||
|---|---|
|
||||
| [`CurrentSource`](#llmapicurrentsource) | |
|
||||
| [`ListSources`](#llmapilistsources) | |
|
||||
| [`SetSource`](#llmapisetsource) | |
|
||||
|
||||
### `LLMAPI.CurrentSource`
|
||||
|
||||
```go
|
||||
CurrentSource() string
|
||||
```
|
||||
|
||||
<small>`llm.go:7`</small>
|
||||
|
||||
### `LLMAPI.ListSources`
|
||||
|
||||
```go
|
||||
ListSources() []string
|
||||
```
|
||||
|
||||
<small>`llm.go:5`</small>
|
||||
|
||||
### `LLMAPI.SetSource`
|
||||
|
||||
```go
|
||||
SetSource(name string) error
|
||||
```
|
||||
|
||||
<small>`llm.go:6`</small>
|
||||
|
||||
### `PluginSDK.LLM`
|
||||
|
||||
```go
|
||||
func (s *PluginSDK) LLM() LLMAPI
|
||||
```
|
||||
|
||||
LLM returns the LLM provider API (may be nil if not available).
|
||||
|
||||
<small>`plugin.go:432`</small>
|
||||
|
||||
### `LLMAPI`
|
||||
|
||||
```go
|
||||
type LLMAPI interface { ListSources() []string SetSource(name string) error CurrentSource() string }
|
||||
```
|
||||
|
||||
LLMAPI provides access to the LLM provider manager.
|
||||
|
||||
<small>`llm.go:4`</small>
|
||||
|
||||
434
docs/api/memory.md
Normal file
434
docs/api/memory.md
Normal file
@ -0,0 +1,434 @@
|
||||
<!-- 本页由 tools/apidoc/gensite 从源码生成,请勿手改;要改文档就改 sdk/*.go 的注释。 -->
|
||||
|
||||
# 记忆(Memory)
|
||||
|
||||
三层记忆的读写接口:**图记忆**(三元组关系)、**文档记忆**(带元数据的文档)、**文本记忆**(事件流水)。以及知识库。
|
||||
|
||||
## `DocMemoryAPI`
|
||||
|
||||
DocMemoryAPI provides access to the document vector store.
|
||||
|
||||
| 方法 | 说明 |
|
||||
|---|---|
|
||||
| [`Insert`](#docmemoryapiinsert) | |
|
||||
| [`InsertWithMedia`](#docmemoryapiinsertwithmedia) | InsertWithMedia 写入文档并关联媒体。attachments 里带 Data 的会落进 |
|
||||
| [`Query`](#docmemoryapiquery) | |
|
||||
| [`Remove`](#docmemoryapiremove) | |
|
||||
| [`Stats`](#docmemoryapistats) | |
|
||||
|
||||
### `DocMemoryAPI.Insert`
|
||||
|
||||
```go
|
||||
Insert(doc *Doc) error
|
||||
```
|
||||
|
||||
<small>`memory.go:76`</small>
|
||||
|
||||
### `DocMemoryAPI.InsertWithMedia`
|
||||
|
||||
```go
|
||||
InsertWithMedia(doc *Doc, attachments []MediaAttachment) error
|
||||
```
|
||||
|
||||
InsertWithMedia 写入文档并关联媒体。attachments 里带 Data 的会落进
|
||||
内容寻址存储(相同字节只存一份),只带 Digest 的直接引用已有内容。
|
||||
媒体成为文档直接持有的一等记忆块:文档向量会融合它们的原生向量,
|
||||
因此图片按自己的向量被召回,不依赖任何生成的描述文本。
|
||||
|
||||
<small>`memory.go:81`</small>
|
||||
|
||||
### `DocMemoryAPI.Query`
|
||||
|
||||
```go
|
||||
Query(text string, topK int) []*Doc
|
||||
```
|
||||
|
||||
<small>`memory.go:75`</small>
|
||||
|
||||
### `DocMemoryAPI.Remove`
|
||||
|
||||
```go
|
||||
Remove(id string)
|
||||
```
|
||||
|
||||
<small>`memory.go:82`</small>
|
||||
|
||||
### `DocMemoryAPI.Stats`
|
||||
|
||||
```go
|
||||
Stats() map[string]interface{}
|
||||
```
|
||||
|
||||
<small>`memory.go:83`</small>
|
||||
|
||||
## `KnowledgeAPI`
|
||||
|
||||
KnowledgeAPI provides access to the knowledge store.
|
||||
|
||||
| 方法 | 说明 |
|
||||
|---|---|
|
||||
| [`Add`](#knowledgeapiadd) | |
|
||||
| [`List`](#knowledgeapilist) | |
|
||||
| [`Search`](#knowledgeapisearch) | |
|
||||
|
||||
### `KnowledgeAPI.Add`
|
||||
|
||||
```go
|
||||
Add(name, content string) error
|
||||
```
|
||||
|
||||
<small>`knowledge.go:6`</small>
|
||||
|
||||
### `KnowledgeAPI.List`
|
||||
|
||||
```go
|
||||
List() ([]string, error)
|
||||
```
|
||||
|
||||
<small>`knowledge.go:7`</small>
|
||||
|
||||
### `KnowledgeAPI.Search`
|
||||
|
||||
```go
|
||||
Search(query string, topK int) ([]*Knowledge, error)
|
||||
```
|
||||
|
||||
<small>`knowledge.go:5`</small>
|
||||
|
||||
## `MemoryAPI`
|
||||
|
||||
MemoryAPI provides access to the graph memory (entity-relation store).
|
||||
|
||||
| 方法 | 说明 |
|
||||
|---|---|
|
||||
| [`Commit`](#memoryapicommit) | |
|
||||
| [`Introspect`](#memoryapiintrospect) | |
|
||||
| [`MergeEntities`](#memoryapimergeentities) | |
|
||||
| [`Purge`](#memoryapipurge) | |
|
||||
| [`Recall`](#memoryapirecall) | |
|
||||
|
||||
### `MemoryAPI.Commit`
|
||||
|
||||
```go
|
||||
Commit(triples []Triple) error
|
||||
```
|
||||
|
||||
<small>`memory.go:6`</small>
|
||||
|
||||
### `MemoryAPI.Introspect`
|
||||
|
||||
```go
|
||||
Introspect() (map[string]interface{}, error)
|
||||
```
|
||||
|
||||
<small>`memory.go:7`</small>
|
||||
|
||||
### `MemoryAPI.MergeEntities`
|
||||
|
||||
```go
|
||||
MergeEntities(source, target string) (int, error)
|
||||
```
|
||||
|
||||
<small>`memory.go:8`</small>
|
||||
|
||||
### `MemoryAPI.Purge`
|
||||
|
||||
```go
|
||||
Purge(criteria map[string]string, mode string) (int, error)
|
||||
```
|
||||
|
||||
<small>`memory.go:9`</small>
|
||||
|
||||
### `MemoryAPI.Recall`
|
||||
|
||||
```go
|
||||
Recall(query []string, depth int) ([]Entity, []Relation, error)
|
||||
```
|
||||
|
||||
<small>`memory.go:5`</small>
|
||||
|
||||
## `SocialAPI`
|
||||
|
||||
SocialAPI provides read-only access to the social graph (person profiles and relationships).
|
||||
External plugins can query person traits and social networks but cannot modify them.
|
||||
|
||||
| 方法 | 说明 |
|
||||
|---|---|
|
||||
| [`GetNetwork`](#socialapigetnetwork) | |
|
||||
| [`GetPerson`](#socialapigetperson) | |
|
||||
| [`GetRelations`](#socialapigetrelations) | |
|
||||
| [`GetTrait`](#socialapigettrait) | |
|
||||
| [`ListPersons`](#socialapilistpersons) | |
|
||||
|
||||
### `SocialAPI.GetNetwork`
|
||||
|
||||
```go
|
||||
GetNetwork(name string, depth int) ([]*PersonProfile, error)
|
||||
```
|
||||
|
||||
<small>`memory.go:104`</small>
|
||||
|
||||
### `SocialAPI.GetPerson`
|
||||
|
||||
```go
|
||||
GetPerson(name string) (*PersonProfile, error)
|
||||
```
|
||||
|
||||
<small>`memory.go:101`</small>
|
||||
|
||||
### `SocialAPI.GetRelations`
|
||||
|
||||
```go
|
||||
GetRelations(name string) ([]SocialRelation, error)
|
||||
```
|
||||
|
||||
<small>`memory.go:103`</small>
|
||||
|
||||
### `SocialAPI.GetTrait`
|
||||
|
||||
```go
|
||||
GetTrait(name, trait string) (string, bool)
|
||||
```
|
||||
|
||||
<small>`memory.go:102`</small>
|
||||
|
||||
### `SocialAPI.ListPersons`
|
||||
|
||||
```go
|
||||
ListPersons() ([]string, error)
|
||||
```
|
||||
|
||||
<small>`memory.go:105`</small>
|
||||
|
||||
## `TextMemoryAPI`
|
||||
|
||||
TextMemoryAPI provides access to chronological text event storage.
|
||||
|
||||
| 方法 | 说明 |
|
||||
|---|---|
|
||||
| [`Append`](#textmemoryapiappend) | |
|
||||
|
||||
### `TextMemoryAPI.Append`
|
||||
|
||||
```go
|
||||
Append(evt TextEvent) error
|
||||
```
|
||||
|
||||
<small>`memory.go:44`</small>
|
||||
|
||||
### `Doc`
|
||||
|
||||
```go
|
||||
type Doc struct { ID string `json:"id"` Title string `json:"title"` Content string `json:"content"` Score …
|
||||
```
|
||||
|
||||
Doc represents a document in the document store.
|
||||
|
||||
MediaDigests / Attachments 在 Query 返回时由内核填充(仅元数据,不带字节)。
|
||||
|
||||
<small>`memory.go:89`</small>
|
||||
|
||||
### `PluginSDK.DocMemory`
|
||||
|
||||
```go
|
||||
func (s *PluginSDK) DocMemory() DocMemoryAPI
|
||||
```
|
||||
|
||||
DocMemory returns the document memory API (may be nil if not available).
|
||||
|
||||
<small>`plugin.go:418`</small>
|
||||
|
||||
### `DocMemoryAPI`
|
||||
|
||||
```go
|
||||
type DocMemoryAPI interface { Query(text string, topK int) []*Doc Insert(doc *Doc) error // InsertWithMedia 写入文档并关联媒体。attachments 里带 Data 的会落进 …
|
||||
```
|
||||
|
||||
DocMemoryAPI provides access to the document vector store.
|
||||
|
||||
<small>`memory.go:74`</small>
|
||||
|
||||
### `Entity`
|
||||
|
||||
```go
|
||||
type Entity struct { Name string `json:"name"` Type string `json:"type"` MentionCount int `json:"mention_count"` }
|
||||
```
|
||||
|
||||
Entity represents a named entity in the knowledge graph.
|
||||
|
||||
<small>`memory.go:13`</small>
|
||||
|
||||
### `PluginSDK.Knowledge`
|
||||
|
||||
```go
|
||||
func (s *PluginSDK) Knowledge() KnowledgeAPI
|
||||
```
|
||||
|
||||
Knowledge returns the knowledge store API (may be nil if not available).
|
||||
|
||||
**示例插件里的真实用法**
|
||||
|
||||
| 插件 | 位置 | 代码 |
|
||||
|---|---|---|
|
||||
| [`recoverydiag`](../examples/index.md#recoverydiag) | `example/recoverydiag/plugin.go:978` | `if p.sdk != nil && p.sdk.Knowledge() != nil {` |
|
||||
|
||||
<small>`plugin.go:425`</small>
|
||||
|
||||
### `Knowledge`
|
||||
|
||||
```go
|
||||
type Knowledge struct { Name string `json:"name"` Content string `json:"content"` }
|
||||
```
|
||||
|
||||
Knowledge represents a knowledge entry.
|
||||
|
||||
**示例插件里的真实用法**
|
||||
|
||||
| 插件 | 位置 | 代码 |
|
||||
|---|---|---|
|
||||
| [`recoverydiag`](../examples/index.md#recoverydiag) | `example/recoverydiag/plugin.go:978` | `if p.sdk != nil && p.sdk.Knowledge() != nil {` |
|
||||
|
||||
<small>`knowledge.go:11`</small>
|
||||
|
||||
### `KnowledgeAPI`
|
||||
|
||||
```go
|
||||
type KnowledgeAPI interface { Search(query string, topK int) ([]*Knowledge, error) Add(name, content string) error List() ([]string, error) }
|
||||
```
|
||||
|
||||
KnowledgeAPI provides access to the knowledge store.
|
||||
|
||||
<small>`knowledge.go:4`</small>
|
||||
|
||||
### `MediaAttachment`
|
||||
|
||||
```go
|
||||
type MediaAttachment struct { Digest string `json:"digest,omitempty"` MIME string `json:"mime,omitempty"` Data []byte `json:"data,omitempty"` Name string `json:"name,omite …
|
||||
```
|
||||
|
||||
TextEvent represents a single text memory event.
|
||||
MediaAttachment 描述一份与记忆关联的媒体。
|
||||
|
||||
两个方向共用一个类型:
|
||||
- 写入(InsertWithMedia):给 Data + MIME 就是新内容;只给 Digest 则是引用已有内容。
|
||||
- 读出(Query):内核只填 Digest/MIME,**不回 Data**——
|
||||
一次检索可能命中几十张图,把字节全塞回插件会把 ABI 消息撑爆。
|
||||
需要字节时拿 Digest 单独取。
|
||||
|
||||
刻意没有 Description 字段:媒体不作为文本被索引,也不带任何生成的描述。
|
||||
它只按自己的原生向量被检索与召回;附加文字请写在文档 / 三元组的文本里。
|
||||
|
||||
<small>`memory.go:58`</small>
|
||||
|
||||
### `PluginSDK.Memory`
|
||||
|
||||
```go
|
||||
func (s *PluginSDK) Memory() MemoryAPI
|
||||
```
|
||||
|
||||
Memory returns the graph memory API (may be nil if not available).
|
||||
|
||||
<small>`plugin.go:404`</small>
|
||||
|
||||
### `MemoryAPI`
|
||||
|
||||
```go
|
||||
type MemoryAPI interface { Recall(query []string, depth int) ([]Entity, []Relation, error) Commit(triples []Triple) error Introspect() (map[string]interface{}, error) Merg …
|
||||
```
|
||||
|
||||
MemoryAPI provides access to the graph memory (entity-relation store).
|
||||
|
||||
<small>`memory.go:4`</small>
|
||||
|
||||
### `PersonProfile`
|
||||
|
||||
```go
|
||||
type PersonProfile struct { Name string `json:"name"` Traits map[string]string `json:"traits,omitempty"` Relations []SocialRelation `json:"relations,omitemp …
|
||||
```
|
||||
|
||||
PersonProfile represents a person's complete profile (traits + social relations).
|
||||
|
||||
<small>`memory.go:109`</small>
|
||||
|
||||
### `Relation`
|
||||
|
||||
```go
|
||||
type Relation struct { SourceName string `json:"source_name"` TargetName string `json:"target_name"` RelationType string `json:"relation_type"` Confidence float6 …
|
||||
```
|
||||
|
||||
Relation represents a relationship between two entities.
|
||||
|
||||
<small>`memory.go:20`</small>
|
||||
|
||||
### `PluginSDK.Social`
|
||||
|
||||
```go
|
||||
func (s *PluginSDK) Social() SocialAPI
|
||||
```
|
||||
|
||||
Social returns the social graph API (may be nil if not available).
|
||||
|
||||
<small>`plugin.go:439`</small>
|
||||
|
||||
### `SocialAPI`
|
||||
|
||||
```go
|
||||
type SocialAPI interface { GetPerson(name string) (*PersonProfile, error) GetTrait(name, trait string) (string, bool) GetRelations(name string) ([]SocialRelation, error) G …
|
||||
```
|
||||
|
||||
SocialAPI provides read-only access to the social graph (person profiles and relationships).
|
||||
External plugins can query person traits and social networks but cannot modify them.
|
||||
|
||||
<small>`memory.go:100`</small>
|
||||
|
||||
### `SocialRelation`
|
||||
|
||||
```go
|
||||
type SocialRelation struct { Person string `json:"person"` Relation string `json:"relation"` }
|
||||
```
|
||||
|
||||
SocialRelation represents a social relationship between two persons.
|
||||
|
||||
<small>`memory.go:116`</small>
|
||||
|
||||
### `TextEvent`
|
||||
|
||||
```go
|
||||
type TextEvent struct { Role string `json:"role"` Content string `json:"content"` Timestamp int64 `json:"timestamp"` Channel …
|
||||
```
|
||||
|
||||
<small>`memory.go:65`</small>
|
||||
|
||||
### `PluginSDK.TextMemory`
|
||||
|
||||
```go
|
||||
func (s *PluginSDK) TextMemory() TextMemoryAPI
|
||||
```
|
||||
|
||||
TextMemory returns the text memory API (may be nil if not available).
|
||||
|
||||
<small>`plugin.go:411`</small>
|
||||
|
||||
### `TextMemoryAPI`
|
||||
|
||||
```go
|
||||
type TextMemoryAPI interface { Append(evt TextEvent) error }
|
||||
```
|
||||
|
||||
TextMemoryAPI provides access to chronological text event storage.
|
||||
|
||||
<small>`memory.go:43`</small>
|
||||
|
||||
### `Triple`
|
||||
|
||||
```go
|
||||
type Triple struct { Subject string `json:"subject"` Relation string `json:"relation"` Object string `json:"object"` Confidence float64 `json:"c …
|
||||
```
|
||||
|
||||
Triple represents a subject-relation-object triple for the knowledge graph.
|
||||
|
||||
SentenceText 是这条三元组的原句,会写进 sentences 表;媒体引用挂在句子上,
|
||||
所以 MediaDigests 非空时内核会保证句子存在(不给就自动合成一句)。
|
||||
|
||||
<small>`memory.go:31`</small>
|
||||
|
||||
736
docs/api/misc.md
Normal file
736
docs/api/misc.md
Normal file
@ -0,0 +1,736 @@
|
||||
<!-- 本页由 tools/apidoc/gensite 从源码生成,请勿手改;要改文档就改 sdk/*.go 的注释。 -->
|
||||
|
||||
# 其他类型
|
||||
|
||||
剩余的类型与方法:`PluginSDK` 本体的访问器、`StageContext` 的并发控制,以及多模态辅助类型。没有归入上面任何一个主题,但可能仍会用到。
|
||||
|
||||
## `DocMemoryAPI`
|
||||
|
||||
DocMemoryAPI provides access to the document vector store.
|
||||
|
||||
| 方法 | 说明 |
|
||||
|---|---|
|
||||
| [`Insert`](#docmemoryapiinsert) | |
|
||||
| [`InsertWithMedia`](#docmemoryapiinsertwithmedia) | InsertWithMedia 写入文档并关联媒体。attachments 里带 Data 的会落进 |
|
||||
| [`Query`](#docmemoryapiquery) | |
|
||||
| [`Remove`](#docmemoryapiremove) | |
|
||||
| [`Stats`](#docmemoryapistats) | |
|
||||
|
||||
### `DocMemoryAPI.Insert`
|
||||
|
||||
```go
|
||||
Insert(doc *Doc) error
|
||||
```
|
||||
|
||||
<small>`memory.go:76`</small>
|
||||
|
||||
### `DocMemoryAPI.InsertWithMedia`
|
||||
|
||||
```go
|
||||
InsertWithMedia(doc *Doc, attachments []MediaAttachment) error
|
||||
```
|
||||
|
||||
InsertWithMedia 写入文档并关联媒体。attachments 里带 Data 的会落进
|
||||
内容寻址存储(相同字节只存一份),只带 Digest 的直接引用已有内容。
|
||||
媒体成为文档直接持有的一等记忆块:文档向量会融合它们的原生向量,
|
||||
因此图片按自己的向量被召回,不依赖任何生成的描述文本。
|
||||
|
||||
<small>`memory.go:81`</small>
|
||||
|
||||
### `DocMemoryAPI.Query`
|
||||
|
||||
```go
|
||||
Query(text string, topK int) []*Doc
|
||||
```
|
||||
|
||||
<small>`memory.go:75`</small>
|
||||
|
||||
### `DocMemoryAPI.Remove`
|
||||
|
||||
```go
|
||||
Remove(id string)
|
||||
```
|
||||
|
||||
<small>`memory.go:82`</small>
|
||||
|
||||
### `DocMemoryAPI.Stats`
|
||||
|
||||
```go
|
||||
Stats() map[string]interface{}
|
||||
```
|
||||
|
||||
<small>`memory.go:83`</small>
|
||||
|
||||
## `EventSubscriber`
|
||||
|
||||
EventSubscriber allows plugins to subscribe to kernel events.
|
||||
This is a restricted interface: plugins can subscribe but the kernel
|
||||
controls which events are delivered.
|
||||
|
||||
| 方法 | 说明 |
|
||||
|---|---|
|
||||
| [`Subscribe`](#eventsubscribersubscribe) | |
|
||||
|
||||
### `EventSubscriber.Subscribe`
|
||||
|
||||
!!! warning "仅内核内置插件可用"
|
||||
|
||||
```go
|
||||
Subscribe(eventType EventType, handler EventHandler) func()
|
||||
```
|
||||
|
||||
<small>`plugin.go:279`</small>
|
||||
|
||||
## `IOInjector`
|
||||
|
||||
IOInjector provides methods for injecting input and interrupts into the agent pipeline.
|
||||
All methods accept (source, channel) where channel is the target output channel
|
||||
for routing the agent's response.
|
||||
|
||||
| 方法 | 说明 |
|
||||
|---|---|
|
||||
| [`InjectInputMedia`](#ioinjectorinjectinputmedia) | |
|
||||
| [`InjectInputMediaOpts`](#ioinjectorinjectinputmediaopts) | |
|
||||
| [`InjectInputMediaSync`](#ioinjectorinjectinputmediasync) | |
|
||||
| [`InjectInputMediaSyncOpts`](#ioinjectorinjectinputmediasyncopts) | |
|
||||
| [`InjectInputSync`](#ioinjectorinjectinputsync) | InjectInputSync 注入输入事件并同步等待 agent 回复,返回回复文本(无回复时返回空串)。 |
|
||||
| [`InjectInputSyncOpts`](#ioinjectorinjectinputsyncopts) | |
|
||||
| [`InjectInterruptMedia`](#ioinjectorinjectinterruptmedia) | |
|
||||
| [`InjectInterruptMediaOpts`](#ioinjectorinjectinterruptmediaopts) | |
|
||||
| [`InjectInterruptText`](#ioinjectorinjectinterrupttext) | |
|
||||
| [`InjectInterruptTextOpts`](#ioinjectorinjectinterrupttextopts) | |
|
||||
| [`InjectText`](#ioinjectorinjecttext) | |
|
||||
| [`InjectTextNoMemory`](#ioinjectorinjecttextnomemory) | |
|
||||
| [`InjectTextOpts`](#ioinjectorinjecttextopts) | 以下 Opts 变体让调用点在**这一次注入**上声明记忆与裁剪行为。 |
|
||||
| [`SetToolBlocks`](#ioinjectorsettoolblocks) | SetToolBlocks 插件工具注入多模态内容块(image_url/audio_url),内核在下一条 |
|
||||
|
||||
### `IOInjector.InjectInputMedia`
|
||||
|
||||
```go
|
||||
InjectInputMedia(source, channel, text string, blocks []ContentBlock)
|
||||
```
|
||||
|
||||
<small>`plugin.go:230`</small>
|
||||
|
||||
### `IOInjector.InjectInputMediaOpts`
|
||||
|
||||
```go
|
||||
InjectInputMediaOpts(source, channel, text string, blocks []ContentBlock, opts InjectOptions)
|
||||
```
|
||||
|
||||
<small>`plugin.go:241`</small>
|
||||
|
||||
### `IOInjector.InjectInputMediaSync`
|
||||
|
||||
```go
|
||||
InjectInputMediaSync(source, channel, text string, blocks []ContentBlock) string
|
||||
```
|
||||
|
||||
<small>`plugin.go:231`</small>
|
||||
|
||||
### `IOInjector.InjectInputMediaSyncOpts`
|
||||
|
||||
```go
|
||||
InjectInputMediaSyncOpts(source, channel, text string, blocks []ContentBlock, opts InjectOptions) string
|
||||
```
|
||||
|
||||
<small>`plugin.go:242`</small>
|
||||
|
||||
### `IOInjector.InjectInputSync`
|
||||
|
||||
```go
|
||||
InjectInputSync(source, channel, text string) string
|
||||
```
|
||||
|
||||
InjectInputSync 注入输入事件并同步等待 agent 回复,返回回复文本(无回复时返回空串)。
|
||||
用于通道消息的完整闭环:收到入站 → agent 处理 → 回复取回 → 送回通道。
|
||||
|
||||
**示例插件里的真实用法**
|
||||
|
||||
| 插件 | 位置 | 代码 |
|
||||
|---|---|---|
|
||||
| [`a2a`](../examples/index.md#a2a) | `example/a2a/plugin.go:345` | `reply := p.sdk.InjectInputSync(p.name, p.name,` |
|
||||
| [`acp`](../examples/index.md#acp) | `example/acp/plugin.go:237` | `reply = p.sdk.InjectInputSync(p.name, p.name,` |
|
||||
|
||||
<small>`plugin.go:226`</small>
|
||||
|
||||
### `IOInjector.InjectInputSyncOpts`
|
||||
|
||||
```go
|
||||
InjectInputSyncOpts(source, channel, text string, opts InjectOptions) string
|
||||
```
|
||||
|
||||
<small>`plugin.go:240`</small>
|
||||
|
||||
### `IOInjector.InjectInterruptMedia`
|
||||
|
||||
```go
|
||||
InjectInterruptMedia(source, channel, text string, blocks []ContentBlock)
|
||||
```
|
||||
|
||||
<small>`plugin.go:232`</small>
|
||||
|
||||
### `IOInjector.InjectInterruptMediaOpts`
|
||||
|
||||
```go
|
||||
InjectInterruptMediaOpts(source, channel, text string, blocks []ContentBlock, opts InjectOptions)
|
||||
```
|
||||
|
||||
<small>`plugin.go:243`</small>
|
||||
|
||||
### `IOInjector.InjectInterruptText`
|
||||
|
||||
```go
|
||||
InjectInterruptText(source, channel, text string)
|
||||
```
|
||||
|
||||
<small>`plugin.go:221`</small>
|
||||
|
||||
### `IOInjector.InjectInterruptTextOpts`
|
||||
|
||||
```go
|
||||
InjectInterruptTextOpts(source, channel, text string, opts InjectOptions)
|
||||
```
|
||||
|
||||
**示例插件里的真实用法**
|
||||
|
||||
| 插件 | 位置 | 代码 |
|
||||
|---|---|---|
|
||||
| [`browser`](../examples/index.md#browser) | `example/browser/plugin.go:1319` | `p.sdk.InjectInterruptTextOpts(p.name, p.name,` |
|
||||
| [`calendar`](../examples/index.md#calendar) | `example/calendar/plugin.go:527` | `p.sdk.InjectInterruptTextOpts("calendar", "calendar", msg, sdk.InjectOptions{NoMemory: true})` |
|
||||
| [`memo`](../examples/index.md#memo) | `example/memo/plugin.go:299` | `p.sdk.InjectInterruptTextOpts(p.name, p.name,` |
|
||||
| [`qq`](../examples/index.md#qq) | `example/qq/plugin.go:1493` | `p.sdk.InjectInterruptTextOpts(p.name, p.name, text, sdk.InjectOptions{` |
|
||||
|
||||
<small>`plugin.go:239`</small>
|
||||
|
||||
### `IOInjector.InjectText`
|
||||
|
||||
```go
|
||||
InjectText(source, channel, text string)
|
||||
```
|
||||
|
||||
<small>`plugin.go:222`</small>
|
||||
|
||||
### `IOInjector.InjectTextNoMemory`
|
||||
|
||||
```go
|
||||
InjectTextNoMemory(source, channel, text string)
|
||||
```
|
||||
|
||||
**示例插件里的真实用法**
|
||||
|
||||
| 插件 | 位置 | 代码 |
|
||||
|---|---|---|
|
||||
| [`browser`](../examples/index.md#browser) | `example/browser/plugin.go:1114` | `p.sdk.InjectTextNoMemory(p.name, p.name, fmt.Sprintf("[浏览器 %s 已导航到 %s]", id, rawURL))` |
|
||||
|
||||
<small>`plugin.go:223`</small>
|
||||
|
||||
### `IOInjector.InjectTextOpts`
|
||||
|
||||
```go
|
||||
InjectTextOpts(source, channel, text string, opts InjectOptions)
|
||||
```
|
||||
|
||||
以下 Opts 变体让调用点在**这一次注入**上声明记忆与裁剪行为。
|
||||
|
||||
上面那些不带 opts 的方法等价于传零值 InjectOptions(记入记忆 + 不裁剪),
|
||||
保留它们是为了不破坏已有插件;新代码应当用 Opts 变体把意图写清楚。
|
||||
|
||||
<small>`plugin.go:238`</small>
|
||||
|
||||
### `IOInjector.SetToolBlocks`
|
||||
|
||||
```go
|
||||
SetToolBlocks(blocks []ContentBlock)
|
||||
```
|
||||
|
||||
SetToolBlocks 插件工具注入多模态内容块(image_url/audio_url),内核在下一条
|
||||
tool message 的 content 数组里带上这些块,让模型在后续轮次看到图/听到音频。
|
||||
|
||||
<small>`plugin.go:229`</small>
|
||||
|
||||
## `KnowledgeAPI`
|
||||
|
||||
KnowledgeAPI provides access to the knowledge store.
|
||||
|
||||
| 方法 | 说明 |
|
||||
|---|---|
|
||||
| [`Add`](#knowledgeapiadd) | |
|
||||
| [`List`](#knowledgeapilist) | |
|
||||
| [`Search`](#knowledgeapisearch) | |
|
||||
|
||||
### `KnowledgeAPI.Add`
|
||||
|
||||
```go
|
||||
Add(name, content string) error
|
||||
```
|
||||
|
||||
<small>`knowledge.go:6`</small>
|
||||
|
||||
### `KnowledgeAPI.List`
|
||||
|
||||
```go
|
||||
List() ([]string, error)
|
||||
```
|
||||
|
||||
<small>`knowledge.go:7`</small>
|
||||
|
||||
### `KnowledgeAPI.Search`
|
||||
|
||||
```go
|
||||
Search(query string, topK int) ([]*Knowledge, error)
|
||||
```
|
||||
|
||||
<small>`knowledge.go:5`</small>
|
||||
|
||||
## `LLMAPI`
|
||||
|
||||
LLMAPI provides access to the LLM provider manager.
|
||||
|
||||
| 方法 | 说明 |
|
||||
|---|---|
|
||||
| [`CurrentSource`](#llmapicurrentsource) | |
|
||||
| [`ListSources`](#llmapilistsources) | |
|
||||
| [`SetSource`](#llmapisetsource) | |
|
||||
|
||||
### `LLMAPI.CurrentSource`
|
||||
|
||||
```go
|
||||
CurrentSource() string
|
||||
```
|
||||
|
||||
<small>`llm.go:7`</small>
|
||||
|
||||
### `LLMAPI.ListSources`
|
||||
|
||||
```go
|
||||
ListSources() []string
|
||||
```
|
||||
|
||||
<small>`llm.go:5`</small>
|
||||
|
||||
### `LLMAPI.SetSource`
|
||||
|
||||
```go
|
||||
SetSource(name string) error
|
||||
```
|
||||
|
||||
<small>`llm.go:6`</small>
|
||||
|
||||
## `MemoryAPI`
|
||||
|
||||
MemoryAPI provides access to the graph memory (entity-relation store).
|
||||
|
||||
| 方法 | 说明 |
|
||||
|---|---|
|
||||
| [`Commit`](#memoryapicommit) | |
|
||||
| [`Introspect`](#memoryapiintrospect) | |
|
||||
| [`MergeEntities`](#memoryapimergeentities) | |
|
||||
| [`Purge`](#memoryapipurge) | |
|
||||
| [`Recall`](#memoryapirecall) | |
|
||||
|
||||
### `MemoryAPI.Commit`
|
||||
|
||||
```go
|
||||
Commit(triples []Triple) error
|
||||
```
|
||||
|
||||
<small>`memory.go:6`</small>
|
||||
|
||||
### `MemoryAPI.Introspect`
|
||||
|
||||
```go
|
||||
Introspect() (map[string]interface{}, error)
|
||||
```
|
||||
|
||||
<small>`memory.go:7`</small>
|
||||
|
||||
### `MemoryAPI.MergeEntities`
|
||||
|
||||
```go
|
||||
MergeEntities(source, target string) (int, error)
|
||||
```
|
||||
|
||||
<small>`memory.go:8`</small>
|
||||
|
||||
### `MemoryAPI.Purge`
|
||||
|
||||
```go
|
||||
Purge(criteria map[string]string, mode string) (int, error)
|
||||
```
|
||||
|
||||
<small>`memory.go:9`</small>
|
||||
|
||||
### `MemoryAPI.Recall`
|
||||
|
||||
```go
|
||||
Recall(query []string, depth int) ([]Entity, []Relation, error)
|
||||
```
|
||||
|
||||
<small>`memory.go:5`</small>
|
||||
|
||||
## `Plugin`
|
||||
|
||||
Plugin is the interface every plugin must implement.
|
||||
|
||||
| 方法 | 说明 |
|
||||
|---|---|
|
||||
| [`Name`](#pluginname) | |
|
||||
| [`Start`](#pluginstart) | |
|
||||
| [`Stop`](#pluginstop) | |
|
||||
|
||||
### `Plugin.Name`
|
||||
|
||||
```go
|
||||
Name() string
|
||||
```
|
||||
|
||||
<small>`plugin.go:14`</small>
|
||||
|
||||
### `Plugin.Start`
|
||||
|
||||
```go
|
||||
Start(sdk *PluginSDK) error
|
||||
```
|
||||
|
||||
<small>`plugin.go:15`</small>
|
||||
|
||||
### `Plugin.Stop`
|
||||
|
||||
```go
|
||||
Stop() error
|
||||
```
|
||||
|
||||
<small>`plugin.go:16`</small>
|
||||
|
||||
## `PluginMgrAPI`
|
||||
|
||||
PluginMgrAPI 提供插件管理能力(外部插件可调用)。
|
||||
由 bridge 注入 dispatch 实现,走 C ABI CORE_PLUGIN_RELOAD_ONE 等。
|
||||
|
||||
| 方法 | 说明 |
|
||||
|---|---|
|
||||
| [`IsPluginDisabled`](#pluginmgrapiisplugindisabled) | IsPluginDisabled 查询插件是否被禁用。 |
|
||||
| [`ListLoadedPlugins`](#pluginmgrapilistloadedplugins) | ListLoadedPlugins 列出已加载插件。 |
|
||||
| [`ReloadOne`](#pluginmgrapireloadone) | ReloadOne 重载单个插件(停止后重新加载)。 |
|
||||
|
||||
### `PluginMgrAPI.IsPluginDisabled`
|
||||
|
||||
```go
|
||||
IsPluginDisabled(name string) bool
|
||||
```
|
||||
|
||||
IsPluginDisabled 查询插件是否被禁用。
|
||||
|
||||
<small>`plugin.go:290`</small>
|
||||
|
||||
### `PluginMgrAPI.ListLoadedPlugins`
|
||||
|
||||
```go
|
||||
ListLoadedPlugins() []string
|
||||
```
|
||||
|
||||
ListLoadedPlugins 列出已加载插件。
|
||||
|
||||
<small>`plugin.go:288`</small>
|
||||
|
||||
### `PluginMgrAPI.ReloadOne`
|
||||
|
||||
```go
|
||||
ReloadOne(name string) error
|
||||
```
|
||||
|
||||
ReloadOne 重载单个插件(停止后重新加载)。
|
||||
|
||||
<small>`plugin.go:286`</small>
|
||||
|
||||
## `SettingsAPI`
|
||||
|
||||
| 方法 | 说明 |
|
||||
|---|---|
|
||||
| [`DataDir`](#settingsapidatadir) | DataDir returns the plugin-specific data directory (guaranteed to exist): |
|
||||
| [`Defs`](#settingsapidefs) | Defs returns config definitions matching the prefix. |
|
||||
| [`Dump`](#settingsapidump) | Dump returns all config values. |
|
||||
| [`Get`](#settingsapiget) | Get reads the plugin's own config value (config_<name> table). |
|
||||
| [`GetCore`](#settingsapigetcore) | GetCore reads the core config table. |
|
||||
| [`GetPlugin`](#settingsapigetplugin) | GetPlugin reads another plugin's config table. |
|
||||
| [`List`](#settingsapilist) | List returns all keys matching the given prefix. |
|
||||
| [`ListCore`](#settingsapilistcore) | ListCore lists core config keys matching the prefix. |
|
||||
| [`ListPlugin`](#settingsapilistplugin) | ListPlugin lists another plugin's config keys matching the prefix. |
|
||||
| [`Plugins`](#settingsapiplugins) | Plugins returns a list of all plugin config namespaces. |
|
||||
| [`RegisterDef`](#settingsapiregisterdef) | RegisterDef registers a config definition for UI display. |
|
||||
| [`Set`](#settingsapiset) | Set writes a config value to the plugin's own config table. |
|
||||
| [`SetCore`](#settingsapisetcore) | SetCore writes to the core config table. |
|
||||
| [`SetPlugin`](#settingsapisetplugin) | SetPlugin writes to another plugin's config table. |
|
||||
|
||||
### `SettingsAPI.DataDir`
|
||||
|
||||
```go
|
||||
DataDir() string
|
||||
```
|
||||
|
||||
DataDir returns the plugin-specific data directory (guaranteed to exist):
|
||||
<daemon data>/plugin_data/<plugin_name>. Plugins should persist any
|
||||
runtime files (generated images, caches, downloads) here.
|
||||
|
||||
<small>`settings.go:25`</small>
|
||||
|
||||
### `SettingsAPI.Defs`
|
||||
|
||||
```go
|
||||
Defs(prefix string) []*ConfigDef
|
||||
```
|
||||
|
||||
Defs returns config definitions matching the prefix.
|
||||
|
||||
<small>`settings.go:40`</small>
|
||||
|
||||
### `SettingsAPI.Dump`
|
||||
|
||||
```go
|
||||
Dump() map[string]interface{}
|
||||
```
|
||||
|
||||
Dump returns all config values.
|
||||
|
||||
<small>`settings.go:43`</small>
|
||||
|
||||
### `SettingsAPI.Get`
|
||||
|
||||
```go
|
||||
Get(key string) (interface{}, error)
|
||||
```
|
||||
|
||||
Get reads the plugin's own config value (config_<name> table).
|
||||
|
||||
<small>`settings.go:5`</small>
|
||||
|
||||
### `SettingsAPI.GetCore`
|
||||
|
||||
```go
|
||||
GetCore(key string) (interface{}, error)
|
||||
```
|
||||
|
||||
GetCore reads the core config table.
|
||||
|
||||
<small>`settings.go:14`</small>
|
||||
|
||||
### `SettingsAPI.GetPlugin`
|
||||
|
||||
```go
|
||||
GetPlugin(plugin, key string) (interface{}, error)
|
||||
```
|
||||
|
||||
GetPlugin reads another plugin's config table.
|
||||
|
||||
<small>`settings.go:28`</small>
|
||||
|
||||
### `SettingsAPI.List`
|
||||
|
||||
```go
|
||||
List(prefix string) ([]string, error)
|
||||
```
|
||||
|
||||
List returns all keys matching the given prefix.
|
||||
|
||||
<small>`settings.go:11`</small>
|
||||
|
||||
### `SettingsAPI.ListCore`
|
||||
|
||||
```go
|
||||
ListCore(prefix string) ([]string, error)
|
||||
```
|
||||
|
||||
ListCore lists core config keys matching the prefix.
|
||||
|
||||
<small>`settings.go:20`</small>
|
||||
|
||||
### `SettingsAPI.ListPlugin`
|
||||
|
||||
```go
|
||||
ListPlugin(plugin, prefix string) ([]string, error)
|
||||
```
|
||||
|
||||
ListPlugin lists another plugin's config keys matching the prefix.
|
||||
|
||||
<small>`settings.go:34`</small>
|
||||
|
||||
### `SettingsAPI.Plugins`
|
||||
|
||||
```go
|
||||
Plugins() []string
|
||||
```
|
||||
|
||||
Plugins returns a list of all plugin config namespaces.
|
||||
|
||||
<small>`settings.go:46`</small>
|
||||
|
||||
### `SettingsAPI.RegisterDef`
|
||||
|
||||
```go
|
||||
RegisterDef(def ConfigDef)
|
||||
```
|
||||
|
||||
RegisterDef registers a config definition for UI display.
|
||||
|
||||
<small>`settings.go:37`</small>
|
||||
|
||||
### `SettingsAPI.Set`
|
||||
|
||||
```go
|
||||
Set(key string, value interface{}) error
|
||||
```
|
||||
|
||||
Set writes a config value to the plugin's own config table.
|
||||
|
||||
<small>`settings.go:8`</small>
|
||||
|
||||
### `SettingsAPI.SetCore`
|
||||
|
||||
```go
|
||||
SetCore(key string, value interface{}) error
|
||||
```
|
||||
|
||||
SetCore writes to the core config table.
|
||||
|
||||
<small>`settings.go:17`</small>
|
||||
|
||||
### `SettingsAPI.SetPlugin`
|
||||
|
||||
```go
|
||||
SetPlugin(plugin, key string, value interface{}) error
|
||||
```
|
||||
|
||||
SetPlugin writes to another plugin's config table.
|
||||
|
||||
<small>`settings.go:31`</small>
|
||||
|
||||
## `SocialAPI`
|
||||
|
||||
SocialAPI provides read-only access to the social graph (person profiles and relationships).
|
||||
External plugins can query person traits and social networks but cannot modify them.
|
||||
|
||||
| 方法 | 说明 |
|
||||
|---|---|
|
||||
| [`GetNetwork`](#socialapigetnetwork) | |
|
||||
| [`GetPerson`](#socialapigetperson) | |
|
||||
| [`GetRelations`](#socialapigetrelations) | |
|
||||
| [`GetTrait`](#socialapigettrait) | |
|
||||
| [`ListPersons`](#socialapilistpersons) | |
|
||||
|
||||
### `SocialAPI.GetNetwork`
|
||||
|
||||
```go
|
||||
GetNetwork(name string, depth int) ([]*PersonProfile, error)
|
||||
```
|
||||
|
||||
<small>`memory.go:104`</small>
|
||||
|
||||
### `SocialAPI.GetPerson`
|
||||
|
||||
```go
|
||||
GetPerson(name string) (*PersonProfile, error)
|
||||
```
|
||||
|
||||
<small>`memory.go:101`</small>
|
||||
|
||||
### `SocialAPI.GetRelations`
|
||||
|
||||
```go
|
||||
GetRelations(name string) ([]SocialRelation, error)
|
||||
```
|
||||
|
||||
<small>`memory.go:103`</small>
|
||||
|
||||
### `SocialAPI.GetTrait`
|
||||
|
||||
```go
|
||||
GetTrait(name, trait string) (string, bool)
|
||||
```
|
||||
|
||||
<small>`memory.go:102`</small>
|
||||
|
||||
### `SocialAPI.ListPersons`
|
||||
|
||||
```go
|
||||
ListPersons() ([]string, error)
|
||||
```
|
||||
|
||||
<small>`memory.go:105`</small>
|
||||
|
||||
## `TextMemoryAPI`
|
||||
|
||||
TextMemoryAPI provides access to chronological text event storage.
|
||||
|
||||
| 方法 | 说明 |
|
||||
|---|---|
|
||||
| [`Append`](#textmemoryapiappend) | |
|
||||
|
||||
### `TextMemoryAPI.Append`
|
||||
|
||||
```go
|
||||
Append(evt TextEvent) error
|
||||
```
|
||||
|
||||
<small>`memory.go:44`</small>
|
||||
|
||||
### `AudioURL`
|
||||
|
||||
```go
|
||||
type AudioURL struct { URL string `json:"url"` }
|
||||
```
|
||||
|
||||
<small>`plugin.go:862`</small>
|
||||
|
||||
### `ImageURL`
|
||||
|
||||
```go
|
||||
type ImageURL struct { URL string `json:"url"` Detail string `json:"detail,omitempty"` }
|
||||
```
|
||||
|
||||
<small>`plugin.go:857`</small>
|
||||
|
||||
### `MemItem`
|
||||
|
||||
```go
|
||||
type MemItem struct { Role string `json:"role"` Content string `json:"content"` Score float64 `json:"score"` }
|
||||
```
|
||||
|
||||
MemItem represents a memory item in stage context.
|
||||
|
||||
<small>`plugin.go:179`</small>
|
||||
|
||||
### `PluginSDK`
|
||||
|
||||
```go
|
||||
type PluginSDK struct { name string regTool ToolRegistrar regStage StageRegistrar regAPI APIRegistrar regOutput OutputChannelRegistrar …
|
||||
```
|
||||
|
||||
PluginSDK is the main API surface provided to plugins at runtime.
|
||||
It wraps tool registration, settings, memory, knowledge, LLM, and IO injection.
|
||||
|
||||
<small>`plugin.go:337`</small>
|
||||
|
||||
### `SDKVersion`
|
||||
|
||||
```go
|
||||
var SDKVersion
|
||||
```
|
||||
|
||||
SDKVersion 是对外暴露的 SDK 版本号。
|
||||
|
||||
<small>`plugin.go:10`</small>
|
||||
|
||||
### `PluginSDK.UnregisterOutputChannel`
|
||||
|
||||
!!! warning "仅内核内置插件可用"
|
||||
外部插件的桥接模板只注入 SetOutputChannelRegistrar(proc_main.go.tmpl:693-705),**未**注入 SetOutputChannelUnregistrar(grep Unregistrar 在 templates/ 下无命中)。因此外部插件的 regOutputUnreg 为 nil,UnregisterOutputChannel 会命中 `if reg != nil` 的 else 分支**直接返回 nil**(sdk/plugin.go:539-549)——即**静默无效**:不报错、通道也没注销。内置插件由 internal/sdk/plugin.go:330 注入 cfg.RegOutputUnreg,真正生效。外部插件要让通道下线,只能重载插件。
|
||||
|
||||
```go
|
||||
func (s *PluginSDK) UnregisterOutputChannel(name string) error
|
||||
```
|
||||
|
||||
UnregisterOutputChannel 注销一个输出通道(动态通道随资源生灭时必须调用)。
|
||||
|
||||
<small>`plugin.go:539`</small>
|
||||
|
||||
205
docs/api/settings.md
Normal file
205
docs/api/settings.md
Normal file
@ -0,0 +1,205 @@
|
||||
<!-- 本页由 tools/apidoc/gensite 从源码生成,请勿手改;要改文档就改 sdk/*.go 的注释。 -->
|
||||
|
||||
# 配置(Settings)
|
||||
|
||||
声明插件自己的配置项,内核会把它渲染到 WebUI 的设置页,并为每个插件维护独立的配置表。
|
||||
|
||||
## `SettingsAPI`
|
||||
|
||||
| 方法 | 说明 |
|
||||
|---|---|
|
||||
| [`DataDir`](#settingsapidatadir) | DataDir returns the plugin-specific data directory (guaranteed to exist): |
|
||||
| [`Defs`](#settingsapidefs) | Defs returns config definitions matching the prefix. |
|
||||
| [`Dump`](#settingsapidump) | Dump returns all config values. |
|
||||
| [`Get`](#settingsapiget) | Get reads the plugin's own config value (config_<name> table). |
|
||||
| [`GetCore`](#settingsapigetcore) | GetCore reads the core config table. |
|
||||
| [`GetPlugin`](#settingsapigetplugin) | GetPlugin reads another plugin's config table. |
|
||||
| [`List`](#settingsapilist) | List returns all keys matching the given prefix. |
|
||||
| [`ListCore`](#settingsapilistcore) | ListCore lists core config keys matching the prefix. |
|
||||
| [`ListPlugin`](#settingsapilistplugin) | ListPlugin lists another plugin's config keys matching the prefix. |
|
||||
| [`Plugins`](#settingsapiplugins) | Plugins returns a list of all plugin config namespaces. |
|
||||
| [`RegisterDef`](#settingsapiregisterdef) | RegisterDef registers a config definition for UI display. |
|
||||
| [`Set`](#settingsapiset) | Set writes a config value to the plugin's own config table. |
|
||||
| [`SetCore`](#settingsapisetcore) | SetCore writes to the core config table. |
|
||||
| [`SetPlugin`](#settingsapisetplugin) | SetPlugin writes to another plugin's config table. |
|
||||
|
||||
### `SettingsAPI.DataDir`
|
||||
|
||||
```go
|
||||
DataDir() string
|
||||
```
|
||||
|
||||
DataDir returns the plugin-specific data directory (guaranteed to exist):
|
||||
<daemon data>/plugin_data/<plugin_name>. Plugins should persist any
|
||||
runtime files (generated images, caches, downloads) here.
|
||||
|
||||
<small>`settings.go:25`</small>
|
||||
|
||||
### `SettingsAPI.Defs`
|
||||
|
||||
```go
|
||||
Defs(prefix string) []*ConfigDef
|
||||
```
|
||||
|
||||
Defs returns config definitions matching the prefix.
|
||||
|
||||
<small>`settings.go:40`</small>
|
||||
|
||||
### `SettingsAPI.Dump`
|
||||
|
||||
```go
|
||||
Dump() map[string]interface{}
|
||||
```
|
||||
|
||||
Dump returns all config values.
|
||||
|
||||
<small>`settings.go:43`</small>
|
||||
|
||||
### `SettingsAPI.Get`
|
||||
|
||||
```go
|
||||
Get(key string) (interface{}, error)
|
||||
```
|
||||
|
||||
Get reads the plugin's own config value (config_<name> table).
|
||||
|
||||
<small>`settings.go:5`</small>
|
||||
|
||||
### `SettingsAPI.GetCore`
|
||||
|
||||
```go
|
||||
GetCore(key string) (interface{}, error)
|
||||
```
|
||||
|
||||
GetCore reads the core config table.
|
||||
|
||||
<small>`settings.go:14`</small>
|
||||
|
||||
### `SettingsAPI.GetPlugin`
|
||||
|
||||
```go
|
||||
GetPlugin(plugin, key string) (interface{}, error)
|
||||
```
|
||||
|
||||
GetPlugin reads another plugin's config table.
|
||||
|
||||
<small>`settings.go:28`</small>
|
||||
|
||||
### `SettingsAPI.List`
|
||||
|
||||
```go
|
||||
List(prefix string) ([]string, error)
|
||||
```
|
||||
|
||||
List returns all keys matching the given prefix.
|
||||
|
||||
<small>`settings.go:11`</small>
|
||||
|
||||
### `SettingsAPI.ListCore`
|
||||
|
||||
```go
|
||||
ListCore(prefix string) ([]string, error)
|
||||
```
|
||||
|
||||
ListCore lists core config keys matching the prefix.
|
||||
|
||||
<small>`settings.go:20`</small>
|
||||
|
||||
### `SettingsAPI.ListPlugin`
|
||||
|
||||
```go
|
||||
ListPlugin(plugin, prefix string) ([]string, error)
|
||||
```
|
||||
|
||||
ListPlugin lists another plugin's config keys matching the prefix.
|
||||
|
||||
<small>`settings.go:34`</small>
|
||||
|
||||
### `SettingsAPI.Plugins`
|
||||
|
||||
```go
|
||||
Plugins() []string
|
||||
```
|
||||
|
||||
Plugins returns a list of all plugin config namespaces.
|
||||
|
||||
<small>`settings.go:46`</small>
|
||||
|
||||
### `SettingsAPI.RegisterDef`
|
||||
|
||||
```go
|
||||
RegisterDef(def ConfigDef)
|
||||
```
|
||||
|
||||
RegisterDef registers a config definition for UI display.
|
||||
|
||||
<small>`settings.go:37`</small>
|
||||
|
||||
### `SettingsAPI.Set`
|
||||
|
||||
```go
|
||||
Set(key string, value interface{}) error
|
||||
```
|
||||
|
||||
Set writes a config value to the plugin's own config table.
|
||||
|
||||
<small>`settings.go:8`</small>
|
||||
|
||||
### `SettingsAPI.SetCore`
|
||||
|
||||
```go
|
||||
SetCore(key string, value interface{}) error
|
||||
```
|
||||
|
||||
SetCore writes to the core config table.
|
||||
|
||||
<small>`settings.go:17`</small>
|
||||
|
||||
### `SettingsAPI.SetPlugin`
|
||||
|
||||
```go
|
||||
SetPlugin(plugin, key string, value interface{}) error
|
||||
```
|
||||
|
||||
SetPlugin writes to another plugin's config table.
|
||||
|
||||
<small>`settings.go:31`</small>
|
||||
|
||||
### `ConfigDef`
|
||||
|
||||
```go
|
||||
type ConfigDef struct { Key string `json:"key"` Default interface{} `json:"default,omitempty"` Type string `json:"type"` DisplayName string …
|
||||
```
|
||||
|
||||
ConfigDef describes a configuration field for the WebUI.
|
||||
|
||||
<small>`settings.go:50`</small>
|
||||
|
||||
### `PluginSDK.Settings`
|
||||
|
||||
```go
|
||||
func (s *PluginSDK) Settings() SettingsAPI
|
||||
```
|
||||
|
||||
Settings returns the settings API for reading/writing plugin configuration.
|
||||
sett 在 New 时一次性写入且无 setter,故不需要加锁。
|
||||
|
||||
**示例插件里的真实用法**
|
||||
|
||||
| 插件 | 位置 | 代码 |
|
||||
|---|---|---|
|
||||
| [`a2a`](../examples/index.md#a2a) | `example/a2a/plugin.go:68` | `s.Settings().RegisterDef(sdk.ConfigDef{` |
|
||||
| [`acp`](../examples/index.md#acp) | `example/acp/plugin.go:63` | `s.Settings().RegisterDef(sdk.ConfigDef{` |
|
||||
| [`ai_image`](../examples/index.md#ai_image) | `example/ai_image/plugin.go:114` | `s.Settings().RegisterDef(sdk.ConfigDef{` |
|
||||
| [`bili`](../examples/index.md#bili) | `example/bili/plugin.go:29` | `s.Settings().RegisterDef(sdk.ConfigDef{` |
|
||||
|
||||
<small>`plugin.go:401`</small>
|
||||
|
||||
### `SettingsAPI`
|
||||
|
||||
```go
|
||||
type SettingsAPI interface { // Get reads the plugin's own config value (config_<name> table). Get(key string) (interface{}, error) // Set writes a config value to the plugi …
|
||||
```
|
||||
|
||||
<small>`settings.go:3`</small>
|
||||
|
||||
154
docs/api/stages.md
Normal file
154
docs/api/stages.md
Normal file
@ -0,0 +1,154 @@
|
||||
<!-- 本页由 tools/apidoc/gensite 从源码生成,请勿手改;要改文档就改 sdk/*.go 的注释。 -->
|
||||
|
||||
# 阶段钩子(Stages)
|
||||
|
||||
在消息处理管道的固定点位插入自己的逻辑。阶段比工具更底层:工具是模型主动调用的,阶段是流程经过时必然触发的。
|
||||
|
||||
### `StageContext.IsResponded`
|
||||
|
||||
```go
|
||||
func (c *StageContext) IsResponded() bool
|
||||
```
|
||||
|
||||
<small>`plugin.go:172`</small>
|
||||
|
||||
### `StageContext.Lock`
|
||||
|
||||
```go
|
||||
func (c *StageContext) Lock()
|
||||
```
|
||||
|
||||
**示例插件里的真实用法**
|
||||
|
||||
| 插件 | 位置 | 代码 |
|
||||
|---|---|---|
|
||||
| [`a2a`](../examples/index.md#a2a) | `example/a2a/plugin.go:158` | `p.sessMu.Lock()` |
|
||||
| [`acp`](../examples/index.md#acp) | `example/acp/plugin.go:128` | `p.srvMu.Lock()` |
|
||||
| [`browser`](../examples/index.md#browser) | `example/browser/plugin.go:427` | `p.mu.Lock()` |
|
||||
| [`calendar`](../examples/index.md#calendar) | `example/calendar/plugin.go:433` | `p.mu.Lock()` |
|
||||
|
||||
<small>`plugin.go:170`</small>
|
||||
|
||||
### `StageContext.RLock`
|
||||
|
||||
```go
|
||||
func (c *StageContext) RLock()
|
||||
```
|
||||
|
||||
**示例插件里的真实用法**
|
||||
|
||||
| 插件 | 位置 | 代码 |
|
||||
|---|---|---|
|
||||
| [`acp`](../examples/index.md#acp) | `example/acp/plugin.go:267` | `p.mu.RLock()` |
|
||||
| [`calendar`](../examples/index.md#calendar) | `example/calendar/plugin.go:649` | `p.mu.RLock()` |
|
||||
| [`memo`](../examples/index.md#memo) | `example/memo/plugin.go:224` | `p.mu.RLock()` |
|
||||
| [`qq`](../examples/index.md#qq) | `example/qq/plugin.go:1152` | `ctx.RLock()` |
|
||||
|
||||
<small>`plugin.go:168`</small>
|
||||
|
||||
### `StageContext.RUnlock`
|
||||
|
||||
```go
|
||||
func (c *StageContext) RUnlock()
|
||||
```
|
||||
|
||||
**示例插件里的真实用法**
|
||||
|
||||
| 插件 | 位置 | 代码 |
|
||||
|---|---|---|
|
||||
| [`acp`](../examples/index.md#acp) | `example/acp/plugin.go:273` | `p.mu.RUnlock()` |
|
||||
| [`calendar`](../examples/index.md#calendar) | `example/calendar/plugin.go:650` | `defer p.mu.RUnlock()` |
|
||||
| [`memo`](../examples/index.md#memo) | `example/memo/plugin.go:229` | `p.mu.RUnlock()` |
|
||||
| [`qq`](../examples/index.md#qq) | `example/qq/plugin.go:1155` | `ctx.RUnlock()` |
|
||||
|
||||
<small>`plugin.go:169`</small>
|
||||
|
||||
### `PluginSDK.RegisterStage`
|
||||
|
||||
```go
|
||||
func (s *PluginSDK) RegisterStage(stage Stage, handler StageHandler, scope ...StageScope)
|
||||
```
|
||||
|
||||
RegisterStage registers a handler for a pipeline stage.
|
||||
|
||||
scope: StageScopeGlobal (default) — receives all stage events.
|
||||
StageScopeOwnTools — only before_toolcall/after_toolcall for this plugin's tools.
|
||||
|
||||
**示例插件里的真实用法**
|
||||
|
||||
| 插件 | 位置 | 代码 |
|
||||
|---|---|---|
|
||||
| [`memo`](../examples/index.md#memo) | `example/memo/plugin.go:152` | `s.RegisterStage(sdk.StagePreAction, p.stagePreAction)` |
|
||||
| [`qq`](../examples/index.md#qq) | `example/qq/plugin.go:719` | `s.RegisterStage(sdk.StageOnInput, p.onInputAuthContext, sdk.StageScopeGlobal)` |
|
||||
| [`sanitizer`](../examples/index.md#sanitizer) | `example/sanitizer/plugin.go:52` | `s.RegisterStage(sdk.StageOnInput, func(ctx *sdk.StageContext) error {` |
|
||||
| [`weather`](../examples/index.md#weather) | `example/weather/plugin.go:94` | `s.RegisterStage(sdk.StageAfterToolcall, func(ctx *sdk.StageContext) error {` |
|
||||
|
||||
<small>`plugin.go:467`</small>
|
||||
|
||||
### `Stage`
|
||||
|
||||
```go
|
||||
type Stage string
|
||||
```
|
||||
|
||||
Stage represents a point in the message processing pipeline.
|
||||
|
||||
<small>`plugin.go:26`</small>
|
||||
|
||||
### `StageContext`
|
||||
|
||||
```go
|
||||
type StageContext struct { mu sync.RWMutex RawMessage string UserID string GroupID string ContextMsgs []map[string]interface{} L …
|
||||
```
|
||||
|
||||
StageContext provides context for stage handlers.
|
||||
|
||||
<small>`plugin.go:148`</small>
|
||||
|
||||
### `StageHandler`
|
||||
|
||||
```go
|
||||
type StageHandler func(ctx *StageContext) error
|
||||
```
|
||||
|
||||
StageHandler is a function that handles a pipeline stage event.
|
||||
|
||||
<small>`plugin.go:23`</small>
|
||||
|
||||
### `StageRegistrar`
|
||||
|
||||
```go
|
||||
type StageRegistrar func(stage Stage, handler StageHandler)
|
||||
```
|
||||
|
||||
StageRegistrar registers a stage handler.
|
||||
|
||||
<small>`plugin.go:308`</small>
|
||||
|
||||
### `StageScope`
|
||||
|
||||
```go
|
||||
type StageScope int
|
||||
```
|
||||
|
||||
StageScope controls which events a stage handler receives.
|
||||
|
||||
<small>`plugin.go:294`</small>
|
||||
|
||||
### `StageContext.Unlock`
|
||||
|
||||
```go
|
||||
func (c *StageContext) Unlock()
|
||||
```
|
||||
|
||||
**示例插件里的真实用法**
|
||||
|
||||
| 插件 | 位置 | 代码 |
|
||||
|---|---|---|
|
||||
| [`a2a`](../examples/index.md#a2a) | `example/a2a/plugin.go:164` | `p.sessMu.Unlock()` |
|
||||
| [`acp`](../examples/index.md#acp) | `example/acp/plugin.go:129` | `defer p.srvMu.Unlock()` |
|
||||
| [`browser`](../examples/index.md#browser) | `example/browser/plugin.go:432` | `p.mu.Unlock()` |
|
||||
| [`calendar`](../examples/index.md#calendar) | `example/calendar/plugin.go:523` | `p.mu.Unlock()` |
|
||||
|
||||
<small>`plugin.go:171`</small>
|
||||
|
||||
77
docs/api/tools.md
Normal file
77
docs/api/tools.md
Normal file
@ -0,0 +1,77 @@
|
||||
<!-- 本页由 tools/apidoc/gensite 从源码生成,请勿手改;要改文档就改 sdk/*.go 的注释。 -->
|
||||
|
||||
# 工具(Tools)
|
||||
|
||||
注册 LLM 可调用的工具。工具是插件最主要的能力形态:模型看到 `ToolDef` 的说明后决定是否调用,调用时执行你的 `ToolHandler`。
|
||||
|
||||
### `ContentBlock`
|
||||
|
||||
```go
|
||||
type ContentBlock struct { Type string `json:"type"` Text string `json:"text,omitempty"` ImageURL *ImageURL `json:"image_url,omitempty"` AudioURL *AudioURL `jso …
|
||||
```
|
||||
|
||||
ContentBlock 是多模态内容块(OpenAI 格式:text/image_url/audio_url)。
|
||||
插件工具返回结果时可用 PluginSDK.SetToolBlocks 注入,让下一轮 LLM
|
||||
请求在 tool message 的 content 数组里带上图片/音频,实现"模型看图/听音频"。
|
||||
|
||||
<small>`plugin.go:850`</small>
|
||||
|
||||
### `PluginSDK.RegisterTool`
|
||||
|
||||
```go
|
||||
func (s *PluginSDK) RegisterTool(name string, def ToolDef, handler ToolHandler) error
|
||||
```
|
||||
|
||||
RegisterTool registers a tool that the LLM can call.
|
||||
|
||||
**示例插件里的真实用法**
|
||||
|
||||
| 插件 | 位置 | 代码 |
|
||||
|---|---|---|
|
||||
| [`a2a`](../examples/index.md#a2a) | `example/a2a/plugin.go:76` | `s.RegisterTool(tp+"a2a_query", sdk.ToolDef{` |
|
||||
| [`acp`](../examples/index.md#acp) | `example/acp/plugin.go:70` | `s.RegisterTool(tp+"acp_query", sdk.ToolDef{` |
|
||||
| [`ai_image`](../examples/index.md#ai_image) | `example/ai_image/plugin.go:159` | `s.RegisterTool(tp+"generate", sdk.ToolDef{` |
|
||||
| [`bili`](../examples/index.md#bili) | `example/bili/plugin.go:47` | `s.RegisterTool(tp+"video", sdk.ToolDef{` |
|
||||
|
||||
<small>`plugin.go:453`</small>
|
||||
|
||||
### `ToolCall`
|
||||
|
||||
```go
|
||||
type ToolCall struct { ID string `json:"id"` Name string `json:"name"` Plugin string `json:"plugin,omitempty …
|
||||
```
|
||||
|
||||
ToolCall represents a model's request to call a tool.
|
||||
|
||||
<small>`plugin.go:186`</small>
|
||||
|
||||
### `ToolDef`
|
||||
|
||||
```go
|
||||
type ToolDef struct { Name string `json:"name"` Plugin string `json:"plugin,omitempty"` Description string …
|
||||
```
|
||||
|
||||
ToolDef describes a tool that the plugin exposes.
|
||||
|
||||
<small>`plugin.go:203`</small>
|
||||
|
||||
### `ToolHandler`
|
||||
|
||||
```go
|
||||
type ToolHandler func(args map[string]interface{}) (interface{}, error)
|
||||
```
|
||||
|
||||
ToolHandler is a function that handles a tool call.
|
||||
|
||||
<small>`plugin.go:20`</small>
|
||||
|
||||
### `ToolResult`
|
||||
|
||||
```go
|
||||
type ToolResult struct { CallID string `json:"call_id"` Name string `json:"name"` Plugin string `json:"plugin,omitempty"` Success bool `json:"suc …
|
||||
```
|
||||
|
||||
ToolResult represents the result of a tool call.
|
||||
|
||||
<small>`plugin.go:194`</small>
|
||||
|
||||
2202
docs/assets/api-index.json
Normal file
2202
docs/assets/api-index.json
Normal file
File diff suppressed because it is too large
Load Diff
60
docs/examples/index.md
Normal file
60
docs/examples/index.md
Normal file
@ -0,0 +1,60 @@
|
||||
<!-- 本页由 tools/apidoc/gensite 从源码生成,请勿手改;要改文档就改 sdk/*.go 的注释。 -->
|
||||
|
||||
# 示例插件
|
||||
|
||||
SDK 仓 `example/` 下有多个**真实可编译**的示例插件,覆盖工具注册、通道、记忆读写、LLM 调用、生命周期等常见形态。
|
||||
|
||||
每个示例都能用 `hmapdev build` 打成 `.hmap` 装进内核直接跑。
|
||||
|
||||
## `a2a`
|
||||
|
||||
用到的 API:`InjectInputSync` · `Lock` · `RegisterInputChannel` · `RegisterOutputChannel` · `RegisterTool` · `SetAutoRestart` · `Settings` · `Unlock`
|
||||
|
||||
## `acp`
|
||||
|
||||
用到的 API:`InjectInputSync` · `Lock` · `RLock` · `RUnlock` · `RegisterInputChannel` · `RegisterOutputChannel` · `RegisterTool` · `SetAutoRestart` · `Settings` · `Unlock`
|
||||
|
||||
## `ai_image`
|
||||
|
||||
用到的 API:`RegisterTool` · `SetAutoRestart` · `Settings`
|
||||
|
||||
## `bili`
|
||||
|
||||
用到的 API:`RegisterTool` · `SetAutoRestart` · `Settings`
|
||||
|
||||
## `browser`
|
||||
|
||||
用到的 API:`InjectInterruptTextOpts` · `InjectTextNoMemory` · `Lock` · `RegisterInputChannel` · `Unlock`
|
||||
|
||||
## `calendar`
|
||||
|
||||
用到的 API:`InjectInterruptTextOpts` · `Lock` · `RLock` · `RUnlock` · `RegisterInputChannel` · `RegisterOnRemoveHandler` · `RegisterStopHandler` · `Unlock`
|
||||
|
||||
## `deepsearch`
|
||||
|
||||
用到的 API:`RegisterStopHandler`
|
||||
|
||||
## `memo`
|
||||
|
||||
用到的 API:`InjectInterruptTextOpts` · `RLock` · `RUnlock` · `RegisterOnRemoveHandler` · `RegisterStage`
|
||||
|
||||
## `qq`
|
||||
|
||||
用到的 API:`InjectInterruptTextOpts` · `RLock` · `RUnlock` · `RegisterOutputChannel` · `RegisterStage`
|
||||
|
||||
## `recoverydiag`
|
||||
|
||||
用到的 API:`Knowledge`
|
||||
|
||||
## `rss`
|
||||
|
||||
用到的 API:`RegisterOnRemoveHandler`
|
||||
|
||||
## `sanitizer`
|
||||
|
||||
用到的 API:`RegisterStage`
|
||||
|
||||
## `weather`
|
||||
|
||||
用到的 API:`RegisterOutputChannel` · `RegisterStage`
|
||||
|
||||
75
docs/guide/capability-boundary.md
Normal file
75
docs/guide/capability-boundary.md
Normal file
@ -0,0 +1,75 @@
|
||||
# 能力边界:哪些 API 外部插件能用
|
||||
|
||||
HomeAgent 有两类插件:
|
||||
|
||||
| 类型 | 说明 | 分发 |
|
||||
|---|---|---|
|
||||
| **外部插件** | 第三方开发,编译成 `.hmap` 后安装 | 独立分发,**可闭源** |
|
||||
| **内置插件** | 编译进内核,`init()` 自注册 | 随内核发行,需合入主仓 |
|
||||
|
||||
SDK 包是**同一个** `gitcode.com/JianFeeeee/homeagent-sdk/sdk`,但两类插件拿到的
|
||||
**能力不同**:外部插件跑在独立进程里,由内核通过桥接注入能力(IPC,不是共享内存里的直接调用)。
|
||||
|
||||
本页说明边界在哪、为什么,以及**怎么在写代码前就知道某个 API 是否可用**。
|
||||
|
||||
## 一句话规则
|
||||
|
||||
> **公开 SDK 包里声明的符号,不等于外部插件拿得到。**
|
||||
|
||||
原因是:有些能力只有进程内的内置插件才可能拥有(比如直接读事件发布通道、
|
||||
直接注入到内核 IO 层)。外部插件通过桥接运行时拿到的是一份**受注入的能力集合**。
|
||||
|
||||
## 外部插件**不可用**的 API
|
||||
|
||||
这些 API 在公开包里存在,但在外部插件路径上拿不到。文档里每条都带
|
||||
<span class="api-badge api-badge-builtin">仅内置</span> 标记,
|
||||
完整清单见 [仅内置插件可用](../api/builtin-only.md)。
|
||||
|
||||
| API | 外部插件的实际情况 | 该用什么 |
|
||||
|---|---|---|
|
||||
| `sdk.PluginSDK.Events()` | **恒为 nil**。桥接运行时不注入 event subscriber(`SetEventSubscriber` 全仓无调用点) | 桥接运行时已按你的声明完成 `events.subscribe`;Lua 插件用 `sdk.events.subscribe` |
|
||||
| `sdk.PluginSDK.SetEventSubscriber` | 无人调用 | 同上 |
|
||||
| `UnregisterOutputChannel` | 桥接只注入 registrar、**不注入 unregistrar**,调用是**静默无效**(返回 nil,不报错也不注销) | `RegisterOutputChannel` 可用;注销需重载插件 |
|
||||
| `SocialAPI` 的写操作 | 公开接口只有 6 个**只读**方法 | 读用 `s.GetPerson` 等;写需内置插件 |
|
||||
| `EventSubscriber.Publish` | 公开接口**刻意只有 Subscribe**,没有 Publish | 只订阅 |
|
||||
| `PriorityL4` | 声明会被内核**夹到 L3** | 用 L1–L3 |
|
||||
| `RegisterChannel` / `ListChannels` / `OutputChan` / `InjectInput` / `InjectInterrupt` | 只存在于内核内部 SDK | `RegisterInputChannel` / `RegisterOutputChannel` / `InjectText` 等公开方法 |
|
||||
| `PluginMgr()` 的完整能力 | 公开 `PluginMgrAPI` **只有 3 个方法**(`ReloadOne` / `ListLoadedPlugins` / `IsPluginDisabled`) | 就这 3 个;`ReloadPlugins`/`Disable`/`Remove` 属内部接口 |
|
||||
|
||||
!!! warning "两处常见的文档错误(本站已更正)"
|
||||
1. **`PluginMgr()` 不是「仅内置可用」**。桥接模板显式注入了它
|
||||
(`base.SetPluginMgrAPI(procPluginMgr{})`),公开 `PluginMgrAPI` 也注明
|
||||
「外部插件可调用」。真正的区别是**方法数量**:公开面 3 个,内部面 9 个。
|
||||
容易混淆是因为两个包里有**同名但不同**的接口:
|
||||
`sdk.PluginMgrAPI`(3 方法)与 `internal/sdk.PluginManager`(9 方法)。
|
||||
2. **`Events()` 恒为 nil 这件事以前没写清**。旧文档把 `Events()` 当作
|
||||
可用的订阅入口,但桥接运行时不注入 subscriber。外部插件的事件订阅
|
||||
实际由生成的运行时通过 `events.subscribe` 完成。
|
||||
|
||||
## 判定依据来自哪里
|
||||
|
||||
本站的「仅内置」标记不是猜的,逐条来自:
|
||||
|
||||
1. **`tools/hmapdev/templates/proc_main.go.tmpl`** —— 外部插件运行时**实际注入**
|
||||
哪些能力,看 `buildPluginSDK()` 里的 `base.Set*` 调用。
|
||||
2. **`internal/sdk`** —— 内置插件用的完整接口,与公开包对照。
|
||||
3. **内核 RPC 协议表**(`internal/plugin/proc/protocol.go`)—— 外部插件**能发哪些请求**。
|
||||
|
||||
每条裁定的具体依据写在该 API 的告警框里,可以直接核对。
|
||||
|
||||
## 怎么快速确认
|
||||
|
||||
- 用 [API 搜索](../api/index.md) 搜 API 名或功能描述,带
|
||||
<span class="api-badge api-badge-builtin">仅内置</span> 的就是外部不可用
|
||||
- 直接看 [仅内置插件可用](../api/builtin-only.md) 汇总页
|
||||
- 拿不准时,**读 `example/` 下的示例插件** —— 它们全是外部插件,
|
||||
能被它们编译通过的写法,外部就一定可用
|
||||
|
||||
## 为什么这样设计
|
||||
|
||||
不是为了限制,而是**IPC 边界决定了能力边界**:外部插件跑在独立进程里,
|
||||
内核只能通过显式的注入点把能力交过去。凡是需要「持有内核内部数据结构」
|
||||
的能力(事件发布通道、IO 通道、插件注册表全量操作),进程外都无法安全暴露。
|
||||
|
||||
这套边界同时带来好处:插件崩溃不会带崩内核(进程隔离),
|
||||
以及**插件可以闭源**(SDK 是 MIT,见[首页](../index.md#_3))。
|
||||
111
docs/guide/first-lua-plugin.md
Normal file
111
docs/guide/first-lua-plugin.md
Normal file
@ -0,0 +1,111 @@
|
||||
# 第一个 Lua 插件
|
||||
|
||||
Lua 插件适合**轻量、快速原型**:不需要 Go 编译环境,改完重启内核即可生效。
|
||||
但它有一个必须理解的限制 —— 执行模型是**被动回调**。
|
||||
|
||||
## 执行模型(先读这段)
|
||||
|
||||
Lua 插件跑在内核进程内的 gopher-lua 解释器里(单 Lua 状态 + 互斥锁):
|
||||
|
||||
- **被动回调**:`main.lua` 只在加载时执行一次。此后工具、阶段钩子、
|
||||
输入输出通道全部由内核事件驱动回调你的 Lua 函数。**插件不能自己启动后台任务。**
|
||||
- **没有并发**:Lua 侧没有 goroutine、协程调度,也没有 `os` / `io` 库和 socket 监听。
|
||||
唯一主动出站通道是 `sdk.http.get/post`(同步请求)。
|
||||
- **任何阻塞循环都会持锁卡死该插件的全部调用。**
|
||||
|
||||
!!! warning "要常驻服务就用 Go 插件"
|
||||
需要监听端口、后台轮询、定时任务的,请用 [Go 插件](first-plugin.md)
|
||||
(可自行启动 goroutine)。Lua 侧的等价做法是**事件驱动**:把逻辑挂在
|
||||
工具、阶段钩子或通道回调上。
|
||||
|
||||
## 生成工程
|
||||
|
||||
```bash
|
||||
hmapdev init myluaplugin --lua
|
||||
cd myluaplugin
|
||||
```
|
||||
|
||||
结构:
|
||||
|
||||
```
|
||||
myluaplugin/
|
||||
├── plg.json — entry: "main.lua", targets: "lua"
|
||||
├── main.lua — 插件实现
|
||||
├── sdk.lua — SDK 模拟层(支持独立测试)
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## 一个完整的插件
|
||||
|
||||
```lua
|
||||
-- main.lua
|
||||
local plugin = {
|
||||
name = "myluaplugin"
|
||||
}
|
||||
|
||||
function plugin.start(sdk)
|
||||
sdk.log("info", "myluaplugin starting...")
|
||||
|
||||
sdk.register_tool("myluaplugin_hello", {
|
||||
description = "向指定的人打招呼",
|
||||
parameters = {
|
||||
type = "object",
|
||||
properties = {
|
||||
who = { type = "string", description = "要打招呼的对象" }
|
||||
},
|
||||
required = { "who" }
|
||||
}
|
||||
}, function(args)
|
||||
return { content = "hello, " .. (args.who or "world") .. "!" }
|
||||
end)
|
||||
|
||||
sdk.log("info", "myluaplugin started")
|
||||
end
|
||||
|
||||
function plugin.stop()
|
||||
sdk.log("info", "myluaplugin stopped")
|
||||
end
|
||||
|
||||
return plugin
|
||||
```
|
||||
|
||||
## 本地测试
|
||||
|
||||
`sdk.lua` 是纯 Lua 的 SDK 模拟实现,可以直接用解释器跑:
|
||||
|
||||
```bash
|
||||
lua main.lua
|
||||
# [lua-plugin] info: myluaplugin starting...
|
||||
# [lua-plugin] register_tool: myluaplugin_hello
|
||||
# [lua-plugin] info: myluaplugin started
|
||||
```
|
||||
|
||||
在内核里运行时,`sdk.*` 由 Go 层注入,`sdk.lua` 里所有 `-- !impl` 标记的函数
|
||||
会被替换成真实实现。
|
||||
|
||||
## API 约定的两点
|
||||
|
||||
- **注册类函数调用即时报错**(抛 Lua error)—— 注册失败不会静默。
|
||||
- **数据类函数统一返回 `(result, err)`**,`err` 为 nil 表示成功。
|
||||
核心未装配的子系统(如 SocialAPI)返回空值而非报错。
|
||||
|
||||
Lua 侧的 `sdk.*` 能力与外部 Go 插件对齐至 SDK 1.3.0(需内核 1.4.0+)。
|
||||
|
||||
!!! note "历史提醒"
|
||||
1.1–1.3 期间,媒体 / 注入标志位 / 优先级能力只在 Go 侧有,Lua 侧静默缺失。
|
||||
现已全量对齐,并由 `internal/plugin/lua_surface_test.go` 的契约测试守住
|
||||
「`sdk.lua` 承诺的每个函数都有运行时绑定」。
|
||||
|
||||
## 构建
|
||||
|
||||
```bash
|
||||
hmapdev build # → dist/myluaplugin_lua.hmap
|
||||
```
|
||||
|
||||
Lua 插件直接打包源码,不经过编译。
|
||||
|
||||
## 下一步
|
||||
|
||||
- [能力边界](capability-boundary.md) —— Lua 与 Go 外部插件的能力面一致
|
||||
- [打包与发布](packaging.md)
|
||||
- [示例](../examples/index.md) —— `example/luademo` 是 Lua 版参考实现
|
||||
162
docs/guide/first-plugin.md
Normal file
162
docs/guide/first-plugin.md
Normal file
@ -0,0 +1,162 @@
|
||||
# 第一个 Go 插件
|
||||
|
||||
以下是一个**能直接跑起来**的最小插件:注册一个工具、声明一项配置、处理停止与卸载。
|
||||
|
||||
## 1. 生成工程
|
||||
|
||||
```bash
|
||||
hmapdev init myplugin
|
||||
cd myplugin
|
||||
```
|
||||
|
||||
生成的结构:
|
||||
|
||||
```
|
||||
myplugin/
|
||||
├── plg.json — 插件元信息(名称、版本、入口、目标平台)
|
||||
├── plugin.go — 插件实现
|
||||
├── go.mod — 模块定义
|
||||
├── README.md
|
||||
└── thirdpart/ — 外部源码存放目录(可选)
|
||||
```
|
||||
|
||||
`hmapdev build` 时会在构建目录自动生成子进程运行时(`z_proc_gen.go` 等),
|
||||
**不需要手工创建,也不要提交**。
|
||||
|
||||
## 2. 插件实现
|
||||
|
||||
插件的全部契约是一个 `Plugin` 接口([API 参考](../api/lifecycle.md#plugin)):
|
||||
|
||||
| 方法 | 何时调用 |
|
||||
|---|---|
|
||||
| `Name() string` | 内核需要标识这个插件时 |
|
||||
| `Start(*sdk.PluginSDK) error` | 插件加载后。**在这里注册工具、通道、配置** |
|
||||
| `Stop() error` | 插件停止时(重载、禁用、内核退出都会触发) |
|
||||
|
||||
再加一个工厂函数。**名字必须是 `NewPluginFactory`** —— 生成的运行时按这个名字调用:
|
||||
|
||||
```go
|
||||
func NewPluginFactory(name string, config map[string]interface{}) (sdk.Plugin, error) {
|
||||
return &Plugin{name: name}, nil
|
||||
}
|
||||
```
|
||||
|
||||
!!! warning "不要写成 `NewPlugin`"
|
||||
生成的子进程运行时调用的入口是 `NewPluginFactory`。仓库里有 3 个早期示例
|
||||
同时保留了两个名字(`NewPlugin` 只是遗留别名),但新插件只写
|
||||
`NewPluginFactory` 即可。写错名字的后果是**编译能过、加载时找不到入口**。
|
||||
|
||||
## 3. 一个完整的例子
|
||||
|
||||
这是一个「打招呼」工具,带一项配置:
|
||||
|
||||
```go
|
||||
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
|
||||
|
||||
// ① 声明配置项:内核会把它渲染到 WebUI 设置页
|
||||
s.Settings().RegisterDef(sdk.ConfigDef{
|
||||
Key: "plugin.myplugin.greeting",
|
||||
Default: "hello",
|
||||
Type: "string",
|
||||
DisplayName: "问候语",
|
||||
Description: "打招呼时使用的前缀",
|
||||
Category: "myplugin",
|
||||
})
|
||||
|
||||
// ② 注册工具:模型看到 Description 后决定是否调用
|
||||
tp := p.name + "_"
|
||||
s.RegisterTool(tp+"hello", sdk.ToolDef{
|
||||
Name: tp + "hello",
|
||||
Description: "向指定的人打招呼",
|
||||
Parameters: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"who": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "要打招呼的对象",
|
||||
},
|
||||
},
|
||||
"required": []string{"who"},
|
||||
},
|
||||
}, p.handleHello)
|
||||
|
||||
// ③ 卸载(插件被删除)前清理自己产生的数据。
|
||||
// 注意与 Stop 的区别:Stop 在每次重载时也会触发。
|
||||
s.RegisterOnRemoveHandler(func() {
|
||||
fmt.Printf("[%s] 清理数据\n", p.name)
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Plugin) Stop() error { return nil }
|
||||
|
||||
func (p *Plugin) handleHello(args map[string]interface{}) (interface{}, error) {
|
||||
who, _ := args["who"].(string)
|
||||
|
||||
greeting := "hello"
|
||||
if v, err := p.sdk.Settings().Get("plugin.myplugin.greeting"); err == nil && v != "" {
|
||||
greeting = v
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"content": fmt.Sprintf("%s, %s!", greeting, who),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewPluginFactory(name string, config map[string]interface{}) (sdk.Plugin, error) {
|
||||
return &Plugin{name: name}, nil
|
||||
}
|
||||
```
|
||||
|
||||
## 4. 工具返回值的两条约定
|
||||
|
||||
`ToolHandler` 返回 `(interface{}, error)`,模型侧看到的是一条 tool message:
|
||||
|
||||
- **正常结果**:返回一个 map,把要展示给模型的文本放在 `content` 字段。
|
||||
未识别的字段也会一并传给模型,可以放结构化数据。
|
||||
- **业务失败**:返回 `map[string]interface{}{"isError": true, "content": "原因"}`
|
||||
**并返回 nil error**。这样模型能看到失败原因并自行调整;
|
||||
若返回 Go 的 `error`,那是**工具调用本身出错**,语义不同。
|
||||
|
||||
```go
|
||||
func errorResult(msg string) map[string]interface{} {
|
||||
return map[string]interface{}{"isError": true, "content": msg}
|
||||
}
|
||||
```
|
||||
|
||||
## 5. 构建与安装
|
||||
|
||||
```bash
|
||||
hmapdev build # 默认产出多平台 bundle
|
||||
# → dist/myplugin_bundle.hmap
|
||||
|
||||
hmapdev build --no-bundle # 只构建当前平台
|
||||
# → dist/myplugin_linux_amd64.hmap
|
||||
```
|
||||
|
||||
安装到内核:在 WebUI 的插件管理页上传 `.hmap`,或从 URL / 本地路径安装。
|
||||
详见 [打包与发布](packaging.md)。
|
||||
|
||||
## 下一步
|
||||
|
||||
- [能力边界](capability-boundary.md) —— 哪些 API 外部插件能用
|
||||
- [工具(Tools)](../api/tools.md) —— `ToolDef` 的完整字段
|
||||
- [记忆(Memory)](../api/memory.md) —— 让插件读写长期记忆
|
||||
- [示例插件](../examples/index.md) —— `example/memo` 是个完整的可读实现
|
||||
62
docs/guide/getting-started.md
Normal file
62
docs/guide/getting-started.md
Normal file
@ -0,0 +1,62 @@
|
||||
# 环境与工具链
|
||||
|
||||
`hmapdev` 是 SDK 仓提供的统一插件开发工具链,Go 与 Lua 两种插件都用它,
|
||||
最终产出 `.hmap` 插件包(工具名即取自这个包格式)。
|
||||
|
||||
!!! note "改名说明"
|
||||
1.2.0 起工具链由 `plugindev` 更名为 `hmapdev`;SDK 存储目录同时由
|
||||
`~/.homeagent/plugindev/sdk` 迁到 `~/.homeagent/hmapdev/sdk`
|
||||
(旧目录会自动继续沿用)。
|
||||
|
||||
## 安装
|
||||
|
||||
从源码构建:
|
||||
|
||||
```bash
|
||||
git clone https://gitcode.com/JianFeeeee/homeagent-sdk
|
||||
cd homeagent-sdk/tools/hmapdev
|
||||
go build -o hmapdev
|
||||
# 把 hmapdev 放进 PATH,或直接用 ./hmapdev
|
||||
```
|
||||
|
||||
也可以从 SDK 的 release 附件下载预编译二进制(`hmapdev_linux_amd64` 等)。
|
||||
|
||||
## SDK 版本管理
|
||||
|
||||
`hmapdev` 会维护一份本地 SDK 存储,`init` 时按 `plg.json` 里的 `sdk` 字段
|
||||
选择版本。两者**必须**一致,否则编译出的插件与内核协议可能错配。
|
||||
|
||||
```bash
|
||||
hmapdev sdk list # 已安装的 SDK 版本
|
||||
hmapdev sdk current # 当前使用的版本
|
||||
hmapdev sdk latest # 最新可用版本
|
||||
hmapdev sdk install v1.2.0 # 安装指定版本
|
||||
hmapdev sdk use v1.2.0 # 切换版本
|
||||
hmapdev sdk path # 当前 SDK 路径
|
||||
```
|
||||
|
||||
存储在 `~/.homeagent/hmapdev/sdk/<version>/`。
|
||||
|
||||
!!! warning "版本未命中会**明确报错**"
|
||||
`plg.json` 声明的 `sdk` 版本若不在本地存储里,`hmapdev` 不会退回某个默认版本,
|
||||
而是报错并让你先 `hmapdev sdk install`。这是有意的:静默降级会产出与内核
|
||||
协议不匹配的插件,那种失败要到运行时才暴露。
|
||||
|
||||
## 源码调试
|
||||
|
||||
不编译直接跑插件源码,输出调用轨迹:
|
||||
|
||||
```bash
|
||||
hmapdev debug [dir] # dir 默认当前目录
|
||||
```
|
||||
|
||||
写 Lua 插件时更简单——`sdk.lua` 是 SDK 模拟层,可以直接用解释器跑:
|
||||
|
||||
```bash
|
||||
lua main.lua
|
||||
```
|
||||
|
||||
## 下一步
|
||||
|
||||
- [第一个 Go 插件](first-plugin.md)
|
||||
- [第一个 Lua 插件](first-lua-plugin.md)
|
||||
56
docs/guide/multi-platform.md
Normal file
56
docs/guide/multi-platform.md
Normal file
@ -0,0 +1,56 @@
|
||||
# 多平台构建
|
||||
|
||||
## 默认就是多平台
|
||||
|
||||
`hmapdev build` 默认 bundle 模式,一次产出含三个平台的单个 `.hmap`:
|
||||
|
||||
```
|
||||
dist/myplugin_bundle.hmap
|
||||
└── plugin.bin.linux.amd64
|
||||
└── plugin.bin.darwin.amd64
|
||||
└── plugin.bin.windows.amd64
|
||||
```
|
||||
|
||||
安装时内核挑当前平台那份,重命名为 `plugin.bin`。
|
||||
|
||||
## 逐平台构建
|
||||
|
||||
```bash
|
||||
hmapdev build --no-bundle # 按 plg.json 的 targets 构建
|
||||
hmapdev build --target linux/arm64 # 追加一个目标
|
||||
```
|
||||
|
||||
`plg.json` 里声明目标:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "myplugin",
|
||||
"version": "1.0.0",
|
||||
"targets": "linux/amd64,windows/amd64"
|
||||
}
|
||||
```
|
||||
|
||||
单平台输出文件名:`{name}_{os}_{arch}.hmap`。
|
||||
|
||||
## 交叉编译
|
||||
|
||||
子进程插件**不再需要 cgo**,所以交叉编译不需要目标平台的 C 工具链 ——
|
||||
这是 v1.0.0 的收益之一。
|
||||
|
||||
!!! note "bundle 模式忽略 `targets`"
|
||||
固定构建 linux/amd64、darwin/amd64、windows/amd64。如果你只需要其中一个,
|
||||
用 `--no-bundle` 更快。
|
||||
|
||||
## 平台能力差异
|
||||
|
||||
历史上有过一处真实的平台断层,现已消除:
|
||||
|
||||
- **v1.0.0 之前**:Windows 上插件只看到 **3 个 stage 字段、且无法写回**。
|
||||
- **v1.0.0 起**:Windows 与其他平台**共用同一套 RPC 实现**,16 字段全可见 + 写回。
|
||||
|
||||
因此**不必**为 Windows 写条件分支 —— 除非你的插件自己用了平台专有的外部命令。
|
||||
|
||||
## 下一步
|
||||
|
||||
- [打包与发布](packaging.md)
|
||||
- [环境与工具链](getting-started.md)
|
||||
114
docs/guide/packaging.md
Normal file
114
docs/guide/packaging.md
Normal file
@ -0,0 +1,114 @@
|
||||
# 打包与发布
|
||||
|
||||
`hmapdev build` 一次完成编译与打包,产出 `.hmap` 分发包(zip 格式,内含
|
||||
`plugin.json` 清单 + 二进制)。
|
||||
|
||||
## 命令
|
||||
|
||||
```bash
|
||||
hmapdev build # 默认 bundle(多平台合集)
|
||||
hmapdev build --no-bundle # 只构建 plg.json targets 里的平台
|
||||
hmapdev build --target linux/arm64 # 在 targets 基础上追加目标
|
||||
hmapdev build --outdir out # 指定输出目录(默认 dist)
|
||||
hmapdev build --sdk-path <path> # 覆盖 go.mod 的 replace 指向的 SDK
|
||||
hmapdev build --replace <mod@path> # 追加 go.mod replace(可多次)
|
||||
```
|
||||
|
||||
执行流程:
|
||||
|
||||
1. 读 `plg.json` 的 `targets` / `bundle` 决定构建目标
|
||||
2. 生成子进程运行时代码(`z_proc_gen.go`、`z_proc_shm_*.go`)
|
||||
3. **Go 插件**:`go build`(普通可执行文件,`CGO_ENABLED=0`)
|
||||
**Lua 插件**:直接打包源码,不编译
|
||||
4. 生成 `plugin.json` 输出清单
|
||||
5. 打成 `.hmap`
|
||||
|
||||
## 两个 JSON 的区别
|
||||
|
||||
这一点经常混淆:
|
||||
|
||||
| 文件 | 谁维护 | 作用 | 关键字段 |
|
||||
|---|---|---|---|
|
||||
| `plg.json` | **你** | 项目元信息,构建输入 | `targets`、`bundle` |
|
||||
| `plugin.json` | `hmapdev` 自动生成 | 构建产物清单 | `entry`、`platforms` |
|
||||
|
||||
`plg.json` 里的 `sdk` 字段声明**本插件针对的 SDK 版本**;未命中本地 SDK 存储
|
||||
会明确报错(见[环境与工具链](getting-started.md))。
|
||||
|
||||
## 多平台(bundle)
|
||||
|
||||
`build` 默认就是 bundle 模式:一次编译 linux/amd64、darwin/amd64、windows/amd64,
|
||||
产出一个含全部平台二进制的 `.hmap`;安装时内核挑当前平台那份。
|
||||
|
||||
```bash
|
||||
hmapdev build # → dist/myplugin_bundle.hmap
|
||||
hmapdev build --no-bundle # → dist/myplugin_linux_amd64.hmap 等
|
||||
```
|
||||
|
||||
!!! note "bundle 模式会忽略 `plg.json` 的 `targets`"
|
||||
固定构建上述三个平台。交叉编译需要对应工具链(如 Linux 上构建 darwin 需要
|
||||
clang / macOS SDK),缺工具链时会失败 —— 此时用 `--no-bundle` 只构建当前平台。
|
||||
|
||||
bundle 包内按 `plugin.bin.<goos>.<goarch>` 区分,安装时重命名为 `plugin.bin`。
|
||||
|
||||
## 产物形态
|
||||
|
||||
子进程插件是**普通可执行文件**,不分平台后缀:
|
||||
|
||||
| 平台 | 二进制 |
|
||||
|---|---|
|
||||
| Linux / macOS / Windows | `plugin.bin` |
|
||||
|
||||
!!! warning "v1.0.0 破坏性变更:不再加载 `.so` / `.dll`"
|
||||
外部插件从 C ABI 动态库改为**子进程 + 共享内存**。
|
||||
|
||||
- `plugin.so` / `plugin.dylib` / `plugin.dll` **不再被加载**。
|
||||
新内核遇到旧产物会跳过并报可操作错误,不崩溃。
|
||||
- **业务代码不用改一行** —— 公开 SDK 接口零改动,用新版 `hmapdev`
|
||||
(原 `plugindev`)重编即可。
|
||||
- `plg.json` 的 `entry` 字段对 Go 插件**已无意义**(写 `plugin.so` 也无妨),
|
||||
现在只用于区分 Lua 插件。
|
||||
- 产物不再需要 cgo,交叉编译无需目标平台 C 工具链。
|
||||
|
||||
## 安装
|
||||
|
||||
三种方式(`9876` 是 pluginmgr 的本地端口,默认只监听 `127.0.0.1`、无鉴权):
|
||||
|
||||
```bash
|
||||
# 从 URL 安装(仅 http/https,流式下载不落盘)
|
||||
curl -X POST http://127.0.0.1:9876/plugins \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"url": "https://example.com/myplugin.hmap"}'
|
||||
|
||||
# 从本地路径安装(读取文件,不移动原文件)
|
||||
curl -X POST http://127.0.0.1:9876/plugins \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"path": "/path/to/myplugin.hmap"}'
|
||||
|
||||
# 直接上传二进制
|
||||
curl -X POST http://127.0.0.1:9876/plugins \
|
||||
--data-binary @dist/myplugin_bundle.hmap
|
||||
```
|
||||
|
||||
安装后调用 `/api/v1/plugins/reload` 或重启内核生效。
|
||||
|
||||
走 WebUI 的 HTTP API(默认 `8080`,需 `api_key` 鉴权,内部代理到 pluginmgr):
|
||||
|
||||
```bash
|
||||
curl -X POST http://127.0.0.1:8080/api/v1/plugins \
|
||||
-H "Authorization: Bearer <api_key>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"path": "/path/to/myplugin.hmap"}'
|
||||
```
|
||||
|
||||
也可以在 WebUI 的插件管理页面上传。
|
||||
|
||||
## 发布前自查
|
||||
|
||||
- [ ] `plg.json` 的 `sdk` 版本与目标内核匹配
|
||||
- [ ] `version` 已递增(内核按版本判断是否需要重装)
|
||||
- [ ] 若插件有外部状态,`SetAutoRestart(false)` 或在 `Start` 里重建连接
|
||||
(崩溃重启是**线性退避** 1s→2s→3s,5 分钟内第 4 次崩溃即停止,
|
||||
见[生命周期](../api/lifecycle.md#pluginsdksetautorestart))
|
||||
- [ ] `RegisterOnRemoveHandler` 里清理自己写下的数据文件
|
||||
- [ ] 在 `-race` 下跑一遍:插件的 `Start` 与工具的并发访问是最常见的竞态来源
|
||||
81
docs/guide/security.md
Normal file
81
docs/guide/security.md
Normal file
@ -0,0 +1,81 @@
|
||||
# 受限 SDK 与安全
|
||||
|
||||
外部插件与内置插件的区别不只是「能不能调某个函数」,还包含一层**安全边界**:
|
||||
外部插件的进程不共享内核地址空间,能力通过显式注入点交过去。
|
||||
|
||||
## 三层隔离
|
||||
|
||||
| 层 | 机制 | 防住了什么 |
|
||||
|---|---|---|
|
||||
| **进程** | 插件跑在独立子进程 | 插件 panic / 内存越界**不会带崩内核** |
|
||||
| **能力** | 只注入显式声明的接口 | 插件拿不到未授权的内核内部结构 |
|
||||
| **权限** | 公开接口是内部接口的**只读子集** | 插件无法改写他人数据 |
|
||||
|
||||
第一种是 v1.0.0 从 C ABI 动态库改为子进程 + 共享内存的直接收益:
|
||||
在此之前,插件 panic 会带崩 `homed`。
|
||||
|
||||
## 受限接口是怎么实现的
|
||||
|
||||
**按接口裁剪,而不是按方法裁剪。** 同一个概念在公开包与内部包里是**两个不同的
|
||||
接口声明**,公开的那个只保留安全子集:
|
||||
|
||||
```go
|
||||
// 公开 SDK:6 个只读方法
|
||||
type SocialAPI interface {
|
||||
GetPerson(name string) (*PersonProfile, error)
|
||||
GetTrait(name, trait string) (string, bool)
|
||||
GetRelations(name string) ([]SocialRelation, error)
|
||||
GetNetwork(name string, depth int) ([]*PersonProfile, error)
|
||||
ListPersons() ([]string, error)
|
||||
}
|
||||
```
|
||||
|
||||
写操作只在内核内部接口里。这样外部插件**在类型层面就调不到**,
|
||||
不是靠运行时检查拦截。
|
||||
|
||||
同理,`EventSubscriber` 公开版**刻意只有 `Subscribe`,没有 `Publish`**:
|
||||
|
||||
```go
|
||||
// 插件可以订阅,但由内核决定投递哪些事件
|
||||
type EventSubscriber interface {
|
||||
Subscribe(eventType EventType, handler EventHandler) func()
|
||||
}
|
||||
```
|
||||
|
||||
## 进程边界带来的约束
|
||||
|
||||
事件订阅是理解这层边界的典型例子。公开包里有一个 `Events() EventSubscriber`,
|
||||
但**外部插件拿到的恒为 nil** —— 桥接运行时不注入它(`SetEventSubscriber`
|
||||
在全仓没有调用点)。外部插件的事件订阅由生成的运行时通过 `events.subscribe`
|
||||
RPC 完成,Lua 插件走内部 SDK 的 `Subscribe`。
|
||||
|
||||
这不是缺陷,而是进程边界的结果:跨进程无法共享内核的事件发布通道。
|
||||
详见[能力边界](capability-boundary.md)。
|
||||
|
||||
## 共享内存中的数据面
|
||||
|
||||
工具调用帧、Cleaner、输入输出通道、媒体块、文档与知识正文**都走共享内存**,
|
||||
RPC 只传偏移描述符。因此:
|
||||
|
||||
- 大对象不经 JSON 序列化,避免了大 payload 的性能与内存放大;
|
||||
- StageContext 在同一份状态上读改写,消除了副本模型的 lost update
|
||||
(实测由 35.8~36.8% 降到 0)。
|
||||
|
||||
`SharedRef`(共享内存描述符)是**内部实现细节**,插件开发者看不到它 ——
|
||||
公开 SDK 只暴露普通字符串与 map。
|
||||
|
||||
## 插件作者的实践建议
|
||||
|
||||
- **不要在 `Start` 里长时间阻塞** —— 内核在等待它返回。
|
||||
- **工具处理器要可并发**:模型可能并发发起多个调用;共享状态用锁保护
|
||||
(`example/memo` 用 `sync.RWMutex`)。
|
||||
- **写文件用原子替换**(临时文件 + rename),避免进程被强杀时截断数据。
|
||||
- **声明 `NoMemory`**:定时提醒、连接状态这类不是对话内容的东西,
|
||||
别让它们污染记忆(`InjectOptions{NoMemory: true}`)。
|
||||
- **在 `-race` 下测**:插件重载瞬间的并发访问是历史高发缺陷。
|
||||
|
||||
## 许可与分发
|
||||
|
||||
SDK 是 **MIT**,插件可以**闭源分发**,可商用、可私有,无需回馈。
|
||||
这是刻意的:SDK 随插件静态链接(源码进入插件二进制),用传染性许可会
|
||||
强迫插件开源。内核本身是 AGPL-3.0-only,但那是内核的许可,与外部插件无关。
|
||||
49
docs/index.md
Normal file
49
docs/index.md
Normal file
@ -0,0 +1,49 @@
|
||||
# HomeAgent 插件 SDK
|
||||
|
||||
用 **Go** 或 **Lua** 为 HomeAgent 编写插件。插件跑在独立进程里,通过公开 SDK 与内核通信:
|
||||
注册工具供模型调用、挂阶段钩子干预流程、读写三层记忆、注册输入输出通道、订阅事件。
|
||||
|
||||
<div id="api-search"></div>
|
||||
|
||||
## 从这里开始
|
||||
|
||||
<div class="grid cards" markdown>
|
||||
|
||||
- :material-rocket-launch: **第一次写插件**
|
||||
|
||||
装工具链、生成工程、写一个工具、打包成 `.hmap` 装进内核跑起来。
|
||||
|
||||
[:octicons-arrow-right-24: 快速开始](guide/getting-started.md)
|
||||
|
||||
- :material-book-open-variant: **API 参考**
|
||||
|
||||
逐个符号的签名与说明,直接取自源码注释。附示例插件里的真实调用点。
|
||||
|
||||
[:octicons-arrow-right-24: 工具(Tools)](api/tools.md)
|
||||
|
||||
- :material-shield-lock: **能力边界**
|
||||
|
||||
哪些 API 外部插件能用、哪些仅内置插件可用,以及为什么。**先看这个能省很多时间。**
|
||||
|
||||
[:octicons-arrow-right-24: 能力边界](guide/capability-boundary.md)
|
||||
|
||||
- :material-code-braces: **示例插件**
|
||||
|
||||
`example/` 下有多个真实可编译的插件,覆盖常见形态。
|
||||
|
||||
[:octicons-arrow-right-24: 示例总览](examples/index.md)
|
||||
|
||||
</div>
|
||||
|
||||
## 许可
|
||||
|
||||
SDK 以 **MIT** 发布 —— 插件作者可**自由选择自己的许可**(闭源、商业、私有均可),
|
||||
不必同许可、也不必回馈。原因:SDK 会随插件一起静态链接(源码进入插件二进制),
|
||||
若用传染性许可,插件作者就被强制开源;MIT 让第三方插件生态不必承担这个代价。
|
||||
|
||||
内核本身是 **AGPL-3.0-only**,但那是内核的许可,与外部插件无关 ——
|
||||
SDK 完全自包含(`go.mod` 零外部依赖,只依赖 Go 标准库),不引用内核任何代码。
|
||||
|
||||
## 版本
|
||||
|
||||
本文档站的 API 参考从源码生成,对应 SDK 版本见 [版本与兼容](versions.md)。
|
||||
245
docs/javascripts/api-search.js
Normal file
245
docs/javascripts/api-search.js
Normal file
@ -0,0 +1,245 @@
|
||||
/*
|
||||
* API 即时检索。
|
||||
*
|
||||
* 为什么要自建:Material 内置搜索按「整页文本」建索引,搜 `InjectText`
|
||||
* 会把所有提到它的页面都列出来,但**分不清哪一条是它的定义**;而且内置
|
||||
* 索引要等 mkdocs build 才生成,改一行 API 也得重建。
|
||||
*
|
||||
* 这里读的是 `assets/api-index.json`——由 tools/apidoc/gensite 直接产出,
|
||||
* 每条记录带 名称/签名/描述/类别/所属页面/是否仅内置/源文件:行号。
|
||||
* 因此可以做到:
|
||||
* - 按名称搜(精确/前缀优先)
|
||||
* - 按描述搜(中文按字、英文按词,都对 API 的文档注释做匹配)
|
||||
* - 按签名搜(如 "(string) error")
|
||||
* - 过滤「仅内置」——外部插件作者最容易被这个绊住
|
||||
*
|
||||
* 设计取舍:纯前端、零依赖、不阻塞页面。索引 ~130 条、约 40KB,一次拉取足够。
|
||||
*/
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
var INDEX_URL = (function () {
|
||||
// 文档站可能部署在子路径下,按当前页面深度回推到站点根。
|
||||
var path = window.location.pathname;
|
||||
var marker = "/api/";
|
||||
var i = path.indexOf(marker);
|
||||
if (i >= 0) return path.slice(0, i) + "/assets/api-index.json";
|
||||
// guide/ 等目录同样回退一层。
|
||||
var lastSlash = path.lastIndexOf("/");
|
||||
return path.slice(0, lastSlash) + "/assets/api-index.json";
|
||||
})();
|
||||
|
||||
var state = { all: [], loaded: false, loading: false };
|
||||
|
||||
function load() {
|
||||
if (state.loaded || state.loading) return Promise.resolve(state.all);
|
||||
state.loading = true;
|
||||
return fetch(INDEX_URL)
|
||||
.then(function (r) {
|
||||
if (!r.ok) throw new Error("HTTP " + r.status);
|
||||
return r.json();
|
||||
})
|
||||
.then(function (data) {
|
||||
state.all = data || [];
|
||||
state.loaded = true;
|
||||
return state.all;
|
||||
})
|
||||
.catch(function () {
|
||||
state.all = [];
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------- 打分 ---------- */
|
||||
//
|
||||
// 三级优先级:名称命中 > 描述命中 > 签名命中。
|
||||
// 名称命中里再分「完全相等 / 前缀 / 子串」,因为用户敲 `InjectText` 时
|
||||
// 想要的是那个符号,不是所有名字里含它的。
|
||||
|
||||
function score(item, q) {
|
||||
var name = (item.n || "").toLowerCase();
|
||||
var ql = q.toLowerCase();
|
||||
var s = 0;
|
||||
|
||||
if (name === ql) s += 1000;
|
||||
else if (name.indexOf(ql) === 0) s += 600;
|
||||
else if (name.indexOf(ql) > 0) s += 350;
|
||||
|
||||
// 限定符:PluginSDK.RegisterTool / IOInjector.InjectText。
|
||||
// 额外支持「去掉 API/SDK 后缀」与「去掉点号」两种写法,
|
||||
// 因为读者习惯写 `memory.recall`(RPC 名),而 Go 名是 `MemoryAPI.Recall`。
|
||||
var qual = ((item.r || "") + "." + name).toLowerCase();
|
||||
if (item.r && qual.indexOf(ql) >= 0) s += 200;
|
||||
if (item.r) {
|
||||
var flat = qual.replace(/[._]/g, "").replace(/apis?dk|sdk|api/g, "");
|
||||
var qflat = ql.replace(/[._\s]/g, "");
|
||||
if (qflat && flat.indexOf(qflat) >= 0) s += 180;
|
||||
}
|
||||
|
||||
var desc = (item.d || "").toLowerCase();
|
||||
if (desc.indexOf(ql) >= 0) s += 120;
|
||||
|
||||
// 签名按 token 匹配:把查询拆词(去掉括号/逗号等标点),全部命中才算。
|
||||
// 这样 `(string) error`、`ContentBlock 媒体` 这类片段都能搜到。
|
||||
// 注意必须先去标点:否则 token `(string)` 永远匹配不到签名里的 `string`。
|
||||
var sig = (item.s || "").toLowerCase();
|
||||
if (sig.indexOf(ql) >= 0) s += 60;
|
||||
var toks = ql
|
||||
.replace(/[()\[\]{},;:]/g, " ")
|
||||
.split(/\s+/)
|
||||
.filter(function (t) { return t.length > 1; });
|
||||
if (toks.length && sig.length) {
|
||||
var allSig = toks.every(function (t) { return sig.indexOf(t) >= 0; });
|
||||
if (allSig) s += 55;
|
||||
}
|
||||
|
||||
// 中文按字匹配:中文没有词边界,逐字命中比整串更实用。
|
||||
if (/[\u4e00-\u9fa5]/.test(q)) {
|
||||
var hit = 0;
|
||||
for (var i = 0; i < q.length; i++) {
|
||||
if (desc.indexOf(q[i]) >= 0) hit++;
|
||||
}
|
||||
if (hit === q.length) s += 100; // 全部字都出现
|
||||
else s += hit * 8;
|
||||
}
|
||||
|
||||
// 公开 API 略优先于「仅内置」——后者通常是噪声。
|
||||
if (s > 0 && !item.b) s += 15;
|
||||
return s;
|
||||
}
|
||||
|
||||
function search(q) {
|
||||
var qq = (q || "").trim();
|
||||
if (!qq) return [];
|
||||
var out = [];
|
||||
for (var i = 0; i < state.all.length; i++) {
|
||||
var sc = score(state.all[i], qq);
|
||||
if (sc > 0) out.push({ item: state.all[i], score: sc });
|
||||
}
|
||||
out.sort(function (a, b) {
|
||||
if (b.score !== a.score) return b.score - a.score;
|
||||
return (a.item.n || "").length - (b.item.n || "").length;
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
/* ---------- 渲染 ---------- */
|
||||
//
|
||||
// 挂在 Material 首页/目录页的一个容器上:#api-search。
|
||||
// 没找到容器就不做任何事——这样同一份 JS 可以安全地全站引入。
|
||||
|
||||
function el(tag, cls, text) {
|
||||
var e = document.createElement(tag);
|
||||
if (cls) e.className = cls;
|
||||
if (text != null) e.textContent = text;
|
||||
return e;
|
||||
}
|
||||
|
||||
function render(mount, q) {
|
||||
mount.innerHTML = "";
|
||||
if (!q.trim()) {
|
||||
mount.appendChild(el("p", "api-hint",
|
||||
"输入 API 名称、描述或签名片段。例:InjectText、注册工具、崩溃、memory.recall、ContentBlock"));
|
||||
return;
|
||||
}
|
||||
var results = search(q);
|
||||
if (!results.length) {
|
||||
mount.appendChild(el("p", "api-hint", "没有匹配的 API。试试更短的词,或按功能描述搜(如「注入」「重载」)。"));
|
||||
return;
|
||||
}
|
||||
var head = el("p", "api-count", "命中 " + results.length + " 个 API");
|
||||
mount.appendChild(head);
|
||||
|
||||
var list = el("ul", "api-results");
|
||||
results.slice(0, 40).forEach(function (r) {
|
||||
var it = r.item;
|
||||
var li = el("li", "api-result");
|
||||
|
||||
var title = el("a", "api-name", (it.r ? it.r + "." : "") + it.n);
|
||||
// 锚点必须用**完整标题文本**(`PluginSDK.InjectText`,点号被 slug 丢掉),
|
||||
// 不是裸方法名 —— 否则跳到页面顶部而到不了那一条。
|
||||
title.href = pageURL(it.p) + "#" + anchorOf((it.r ? it.r + "." : "") + it.n);
|
||||
li.appendChild(title);
|
||||
|
||||
if (it.b) {
|
||||
var badge = el("span", "api-badge api-badge-builtin", "仅内置");
|
||||
badge.title = "外部(第三方)插件运行时拿不到这个 API";
|
||||
li.appendChild(badge);
|
||||
}
|
||||
|
||||
li.appendChild(el("code", "api-sig", it.s || ""));
|
||||
|
||||
if (it.d) {
|
||||
var d = el("span", "api-desc", it.d);
|
||||
li.appendChild(d);
|
||||
}
|
||||
if (it.f) {
|
||||
li.appendChild(el("span", "api-loc", it.f + (it.l ? ":" + it.l : "")));
|
||||
}
|
||||
list.appendChild(li);
|
||||
});
|
||||
mount.appendChild(list);
|
||||
}
|
||||
|
||||
function pageURL(page) {
|
||||
if (!page) return "#";
|
||||
// 所有 API 章节都在 /api/ 下(生成物),示例页在 /examples/。
|
||||
// 从当前 URL 回推到站点根,保证部署在子路径下也能用。
|
||||
var path = window.location.pathname;
|
||||
var i = path.indexOf("/api/");
|
||||
var root;
|
||||
if (i >= 0) {
|
||||
root = path.slice(0, i + 1);
|
||||
} else {
|
||||
var j = path.indexOf("/guide/");
|
||||
if (j >= 0) root = path.slice(0, j + 1);
|
||||
else if (path.indexOf("/examples/") >= 0) root = path.slice(0, path.indexOf("/examples/") + 1);
|
||||
else root = path.slice(0, path.lastIndexOf("/") + 1);
|
||||
}
|
||||
var dir = page === "examples" ? "examples" : "api";
|
||||
return root + dir + "/" + page + "/";
|
||||
}
|
||||
|
||||
// anchorOf 复现 MkDocs 的 slug:小写、去掉非 [a-z0-9_-] 的字符(点号被去掉)、
|
||||
// 下划线保留、空格转连字符。
|
||||
function anchorOf(name) {
|
||||
return String(name)
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9_ -]/g, "")
|
||||
.replace(/\s+/g, "-");
|
||||
}
|
||||
|
||||
function mount() {
|
||||
var box = document.getElementById("api-search");
|
||||
if (!box) return;
|
||||
|
||||
var input = el("input", "api-input");
|
||||
input.type = "search";
|
||||
input.placeholder = "搜索 API:名称、描述、签名…";
|
||||
input.setAttribute("autocomplete", "off");
|
||||
input.setAttribute("spellcheck", "false");
|
||||
|
||||
var out = el("div", "api-output");
|
||||
box.appendChild(input);
|
||||
box.appendChild(out);
|
||||
|
||||
load().then(function () {
|
||||
render(out, "");
|
||||
input.addEventListener("input", function () {
|
||||
render(out, input.value);
|
||||
});
|
||||
});
|
||||
|
||||
// 支持 ?q= 直达(可从别处链接到一次检索)。
|
||||
var m = /[?&]q=([^&]+)/.exec(window.location.search);
|
||||
if (m) {
|
||||
input.value = decodeURIComponent(m[1].replace(/\+/g, " "));
|
||||
}
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", mount);
|
||||
} else {
|
||||
mount();
|
||||
}
|
||||
})();
|
||||
115
docs/stylesheets/extra.css
Normal file
115
docs/stylesheets/extra.css
Normal file
@ -0,0 +1,115 @@
|
||||
/* API 即时检索与文档站的少量本地样式。
|
||||
只补 Material 没覆盖的部分,不覆盖主题变量(保持深浅色自动适配)。 */
|
||||
|
||||
#api-search {
|
||||
margin: 1.2rem 0 2rem;
|
||||
}
|
||||
|
||||
.api-input {
|
||||
width: 100%;
|
||||
padding: 0.7rem 0.9rem;
|
||||
font-size: 1rem;
|
||||
border: 1px solid var(--md-default-fg-color--lightest);
|
||||
border-radius: 0.3rem;
|
||||
background: var(--md-default-bg-color);
|
||||
color: var(--md-default-fg-color);
|
||||
}
|
||||
|
||||
.api-input:focus {
|
||||
outline: 2px solid var(--md-accent-fg-color);
|
||||
outline-offset: 1px;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.api-hint {
|
||||
color: var(--md-default-fg-color--light);
|
||||
font-size: 0.8rem;
|
||||
margin: 0.6rem 0 0;
|
||||
}
|
||||
|
||||
.api-count {
|
||||
color: var(--md-default-fg-color--light);
|
||||
font-size: 0.75rem;
|
||||
margin: 0.8rem 0 0.4rem;
|
||||
}
|
||||
|
||||
.api-results {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.api-result {
|
||||
padding: 0.55rem 0.6rem;
|
||||
border-left: 3px solid var(--md-primary-fg-color);
|
||||
margin-bottom: 0.4rem;
|
||||
background: var(--md-code-bg-color);
|
||||
border-radius: 0 0.2rem 0.2rem 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
gap: 0.45rem;
|
||||
}
|
||||
|
||||
.api-name {
|
||||
font-family: var(--md-code-font-family, monospace);
|
||||
font-weight: 700;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.api-sig {
|
||||
font-size: 0.72rem;
|
||||
color: var(--md-default-fg-color--light);
|
||||
background: none;
|
||||
padding: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.api-desc {
|
||||
flex-basis: 100%;
|
||||
font-size: 0.78rem;
|
||||
color: var(--md-default-fg-color--light);
|
||||
}
|
||||
|
||||
.api-loc {
|
||||
flex-basis: 100%;
|
||||
font-size: 0.68rem;
|
||||
color: var(--md-default-fg-color--lighter, var(--md-default-fg-color--light));
|
||||
font-family: var(--md-code-font-family, monospace);
|
||||
}
|
||||
|
||||
.api-badge {
|
||||
font-size: 0.62rem;
|
||||
padding: 0.08rem 0.34rem;
|
||||
border-radius: 0.6rem;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.api-badge-builtin {
|
||||
background: rgba(245, 158, 11, 0.18);
|
||||
color: #b45309;
|
||||
border: 1px solid rgba(245, 158, 11, 0.5);
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .api-badge-builtin {
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
/* 「仅内置」告警块里的依据说明通常很长,窄屏下允许更小字号。 */
|
||||
@media screen and (max-width: 44.98em) {
|
||||
.api-sig { font-size: 0.68rem; }
|
||||
}
|
||||
|
||||
/* 签名与长类型定义可能超出正文宽度(如 ContentBlock 的字段列表)。
|
||||
highlight 代码块默认不换行,靠横向滚动;把默认改为换行显示,
|
||||
因为文档读者更希望一眼看全签名而不是拖滚动条。 */
|
||||
.md-typeset pre > code {
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
/* 接口方法表里的签名可能很长,允许在任意位置折行。 */
|
||||
.md-typeset table code {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
77
docs/versions.md
Normal file
77
docs/versions.md
Normal file
@ -0,0 +1,77 @@
|
||||
# 版本与兼容
|
||||
|
||||
## SDK 版本语义
|
||||
|
||||
**SDK 版本跟随内核的中版本,patch 位恒为 `.0`。**
|
||||
|
||||
整条内核 `1.1.x` 线(1.1.0、1.1.1、1.1.7…)共用 **SDK 1.1.0**;
|
||||
只有内核进入 `1.2.0` 这种中版本跃迁时,SDK 才升到 1.2.0。
|
||||
|
||||
这样插件作者只需关心「我在为哪个中版本写插件」,不必跟着内核的每个 bugfix 换依赖。
|
||||
当前内核声明的兼容上限是 **SDK 1.3.0**。
|
||||
|
||||
## 版本历史
|
||||
|
||||
| SDK | 内核 | 变化 | 需要重编? |
|
||||
|---|---|---|---|
|
||||
| **1.3.0** | 1.4.0+ | 驻留子 agent、`RecallPolicy` 等 | 想用新 API 才需要 |
|
||||
| **1.2.0** | 1.2.0 / 1.3.x | `InjectOptions{NoMemory, ContextPolicy}`、六个 `*Opts` 变体、`ChannelDef.ContextPolicy` | 不需要 |
|
||||
| **1.1.0** | 1.1.x | 多模态贯通:`Triple.SentenceText`、`Doc.Attachments`、`MediaAttachment`、`InsertWithMedia`、媒体注入方法 | 不需要 |
|
||||
| **1.0.0** | 1.0.0+ | **运行模型变更**:C ABI 动态库 → 子进程 + 共享内存 | **需要** |
|
||||
|
||||
### 1.0.0 是唯一一次破坏性变更
|
||||
|
||||
- `.so` / `.dylib` / `.dll` **不再被加载**,遇到旧产物会跳过并报可操作错误(不崩溃)。
|
||||
- **业务代码不用改一行** —— 公开 SDK 接口零改动,用新版 `hmapdev` 重编即可。
|
||||
- 产物从 `plugin.so` 变为 `plugin.bin`;不再需要 cgo。
|
||||
|
||||
### 1.1.0 / 1.2.0 是纯追加
|
||||
|
||||
两次都是**新增方法由插件调用、内核实现**,不调就不受影响。
|
||||
零值 `InjectOptions` 与旧的三参数方法完全等价,因此存量插件**不需要改、也不需要重编**;
|
||||
想用新字段的重编即可。
|
||||
|
||||
!!! tip "什么时候必须重编"
|
||||
只有两种情况:① 内核跨了中版本(如 1.1 → 1.2)且你用了新 API;
|
||||
② 内核的 RPC 协议版本变了(`.hmap` 里的 `protocol` 字段与内核不匹配)。
|
||||
后者的错配**不会静默失效** —— 握手时会显式拦下。
|
||||
|
||||
## RPC 协议版本
|
||||
|
||||
插件包里带 `protocol` 字段,必须等于内核的 `ProtocolVersion`(当前 **2**)。
|
||||
|
||||
协议 v2 引入了调用帧(tool / cleaner / output)与 `blocks_ref` 媒体块。
|
||||
v1 插件遇上 v2 内核会拿到空参数,反过来 v2 插件发 `blocks_ref` 会被 v1 内核静默忽略 ——
|
||||
**两边错配都不报错、只是静默失效**,所以协议版本在握手上显式校验。
|
||||
|
||||
## 怎么确认自己在用什么
|
||||
|
||||
装的 SDK 版本:
|
||||
|
||||
```bash
|
||||
hmapdev sdk current
|
||||
hmapdev sdk list
|
||||
```
|
||||
|
||||
插件声明的目标版本在 `plg.json` 的 `sdk` 字段。若该版本不在本地存储里,
|
||||
`hmapdev` 会**明确报错**,不静默降级 —— 静默降级会产出与内核协议不匹配的包,
|
||||
那种失败要到运行时才暴露。
|
||||
|
||||
## 文档站对应的版本
|
||||
|
||||
本页与 [API 参考](api/index.md) 由 `tools/apidoc` 从源码生成,
|
||||
内容随源码一起演进。发现文档与代码不一致时,**改的是源码注释**,
|
||||
`go run ./tools/apidoc` 重新生成即可(见下方「维护」)。
|
||||
|
||||
## 维护(给 SDK 维护者)
|
||||
|
||||
```bash
|
||||
cd homeagent-sdk
|
||||
go run ./tools/apidoc -pkgdir ./sdk -out /tmp/api.json
|
||||
go run ./tools/apidoc/gensite -api /tmp/api.json -out ./docs -examples ./example
|
||||
mkdocs serve # 本地预览
|
||||
mkdocs build # 产出 site_build/
|
||||
```
|
||||
|
||||
API 面的**能力分层**(哪些 API 仅内置可用)记在 `tools/apidoc/tiers.json`,
|
||||
每条裁定都附源码依据 —— 改这里而不是改生成物。
|
||||
128
mkdocs.yml
Normal file
128
mkdocs.yml
Normal file
@ -0,0 +1,128 @@
|
||||
# HomeAgent 插件 SDK 文档站配置。
|
||||
#
|
||||
# 设计取舍:
|
||||
# - 每页顶部都放「版本 + 编辑链接」,因为 SDK 与内核的协议版本会错配,
|
||||
# 读者必须先能确认自己看的是哪一版。
|
||||
# - 中文检索依赖 jieba(已装);英文走内置分词。两者都不要额外服务。
|
||||
# - docs/api/*.md 是**生成物**(tools/apidoc/gensite),页首会写明,防止手改。
|
||||
|
||||
site_name: HomeAgent 插件 SDK
|
||||
site_description: 用 Go 或 Lua 为 HomeAgent 编写插件 —— API 参考与开发指南
|
||||
site_url: https://sdk.homeagent.jianfgit.xyz/
|
||||
copyright: MIT 许可 · JianFeeeee
|
||||
|
||||
docs_dir: docs
|
||||
site_dir: site_build
|
||||
|
||||
theme:
|
||||
name: material
|
||||
language: zh
|
||||
features:
|
||||
- navigation.instant
|
||||
- navigation.instant.progress
|
||||
- navigation.tracking
|
||||
- navigation.tabs
|
||||
- navigation.sections
|
||||
- navigation.indexes
|
||||
- navigation.top
|
||||
- toc.follow
|
||||
- search.suggest
|
||||
- search.highlight
|
||||
- search.share
|
||||
- content.code.copy
|
||||
- content.code.annotate
|
||||
- content.action.edit
|
||||
palette:
|
||||
- media: "(prefers-color-scheme: light)"
|
||||
scheme: default
|
||||
primary: indigo
|
||||
accent: indigo
|
||||
toggle:
|
||||
icon: material/weather-night
|
||||
name: 切换到深色
|
||||
- media: "(prefers-color-scheme: dark)"
|
||||
scheme: slate
|
||||
primary: indigo
|
||||
accent: indigo
|
||||
toggle:
|
||||
icon: material/weather-sunny
|
||||
name: 切换到浅色
|
||||
icon:
|
||||
repo: fontawesome/brands/git-alt
|
||||
|
||||
plugins:
|
||||
- search:
|
||||
lang:
|
||||
- zh
|
||||
- en
|
||||
separator: '[\s\u200b\-]'
|
||||
# 中文按词切(jieba),否则整句变一个 token,检索不到。
|
||||
jieba_dict: null
|
||||
|
||||
markdown_extensions:
|
||||
- admonition
|
||||
- attr_list
|
||||
- def_list
|
||||
- footnotes
|
||||
- md_in_html
|
||||
- tables
|
||||
- toc:
|
||||
permalink: true
|
||||
toc_depth: 3
|
||||
- pymdownx.details
|
||||
# Material 的图标语法 :material-xxx: / :octicons-xxx: 依赖这个扩展。
|
||||
# 没开时它们会**原样显示为文本**(实测首页四个卡片全花了)。
|
||||
- pymdownx.emoji:
|
||||
emoji_index: !!python/name:material.extensions.emoji.twemoji
|
||||
emoji_generator: !!python/name:material.extensions.emoji.to_svg
|
||||
- pymdownx.highlight:
|
||||
anchor_linenums: true
|
||||
- pymdownx.inlinehilite
|
||||
- pymdownx.snippets
|
||||
- pymdownx.superfences
|
||||
- pymdownx.tabbed:
|
||||
alternate_style: true
|
||||
|
||||
extra:
|
||||
generator: false
|
||||
social:
|
||||
- icon: fontawesome/solid/code
|
||||
link: https://gitcode.com/JianFeeeee/homeagent-sdk
|
||||
name: SDK 源码
|
||||
# 自定义检索端点在 docs/javascripts/api-search.js 里注册,
|
||||
# 索引文件由 gensite 产出:docs/assets/api-index.json
|
||||
|
||||
nav:
|
||||
- 首页: index.md
|
||||
- 快速开始:
|
||||
- 环境与工具链: guide/getting-started.md
|
||||
- 第一个 Go 插件: guide/first-plugin.md
|
||||
- 第一个 Lua 插件: guide/first-lua-plugin.md
|
||||
- API 参考:
|
||||
- api/index.md
|
||||
- 工具(Tools): api/tools.md
|
||||
- 阶段钩子(Stages): api/stages.md
|
||||
- 记忆(Memory): api/memory.md
|
||||
- 输入/输出通道: api/channels.md
|
||||
- 配置(Settings): api/settings.md
|
||||
- 生命周期(Lifecycle): api/lifecycle.md
|
||||
- 事件(Events): api/events.md
|
||||
- LLM 调用: api/llm.md
|
||||
- 常量与枚举: api/constants.md
|
||||
- 桥接装配点: api/bridge.md
|
||||
- 其他类型: api/misc.md
|
||||
- 仅内置插件可用: api/builtin-only.md
|
||||
- 指南:
|
||||
- 能力边界(哪些 API 外部可用): guide/capability-boundary.md
|
||||
- 打包与发布: guide/packaging.md
|
||||
- 多平台构建: guide/multi-platform.md
|
||||
- 受限 SDK 与安全: guide/security.md
|
||||
- 示例插件:
|
||||
- 总览: examples/index.md
|
||||
- 版本与兼容: versions.md
|
||||
|
||||
extra_css:
|
||||
- stylesheets/extra.css
|
||||
|
||||
extra_javascript:
|
||||
- javascripts/api-search.js
|
||||
86
tools/apidoc/README.md
Normal file
86
tools/apidoc/README.md
Normal file
@ -0,0 +1,86 @@
|
||||
# 插件 SDK 文档站
|
||||
|
||||
用 MkDocs Material 构建的 SDK 文档站。**API 参考不是手写的** ——
|
||||
它从 `sdk/*.go` 的源码注释生成,因为手抄必然与代码漂移。
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
mkdocs.yml 站点配置(导航、主题、中文检索)
|
||||
docs/
|
||||
├── index.md ┐
|
||||
├── versions.md │
|
||||
├── guide/*.md ├─ 手写:指南、边界说明、版本
|
||||
├── api/index.md │
|
||||
├── javascripts/ │
|
||||
│ └── api-search.js │ 自建 API 检索(按名称/描述/签名)
|
||||
├── stylesheets/extra.css ┘
|
||||
├── api/*.md ┐ 生成物 —— 勿手改
|
||||
├── examples/index.md │ (build 时覆盖)
|
||||
└── assets/api-index.json ┘
|
||||
tools/apidoc/ 生成器(本仓 Go 代码,零外部依赖)
|
||||
├── extract.go 从源码提取符号、注释、分层
|
||||
├── tiers.go 应用能力分层(public / builtin / bridge)
|
||||
├── tiers.json **能力边界的事实源**(每条附源码依据)
|
||||
├── gensite/main.go 渲染 Markdown + 检索索引
|
||||
├── gensite/usages.go 从 example/ 抽取真实调用点
|
||||
└── build.sh 一键生成 + 构建
|
||||
```
|
||||
|
||||
## 构建
|
||||
|
||||
```bash
|
||||
tools/apidoc/build.sh # 生成 + 构建到 site_build/
|
||||
tools/apidoc/build.sh serve # 本地预览(http://127.0.0.1:8000)
|
||||
```
|
||||
|
||||
依赖:Go 1.21+、`mkdocs-material`(`pip install mkdocs-material`)、
|
||||
`jieba`(中文检索分词,`pip install jieba`)。
|
||||
|
||||
## 两条设计原则
|
||||
|
||||
**① API 参考从源码生成。** 签名、说明、示例全部来自 `sdk/*.go` 的文档注释。
|
||||
发现文档不对时,**改的是源码注释**,然后重新生成。生成页首行有「勿手改」标记。
|
||||
|
||||
**② 能力边界是可核对的事实,不是印象。** 哪些 API 外部插件拿不到,
|
||||
逐条记在 `tools/apidoc/tiers.json`,每条都写清**可复核的依据**
|
||||
(文件:行号、或 `grep` 结论)。判断标准是:
|
||||
|
||||
| 依据 | 含义 |
|
||||
|---|---|
|
||||
| `tools/hmapdev/templates/proc_main.go.tmpl` 的 `base.Set*` 调用 | 外部插件运行时**实际注入**哪些能力 |
|
||||
| `internal/sdk` | 内置插件用的完整接口(对照出外部缺什么) |
|
||||
| `internal/plugin/proc/protocol.go` | 外部插件**能发哪些 RPC** |
|
||||
|
||||
文档站上每条「仅内置」告警都带这个依据,读者可自行核对。
|
||||
|
||||
### 为什么这个边界值得单独维护
|
||||
|
||||
写这个站时,实测发现文档与源码有**三处不符**(现已在站内更正):
|
||||
|
||||
1. `PluginMgr()` 曾被写成「仅内置可用」——实际桥接**显式注入**了它。
|
||||
真正的区别是方法数:公开面 3 个,内部面 9 个(两个包里同名不同接口)。
|
||||
2. `Events()` 曾被当作可用的事件订阅入口——实际桥接**不注入** subscriber,
|
||||
外部插件拿到的恒为 nil(`SetEventSubscriber` 全仓无调用点)。
|
||||
外部插件的事件订阅实际由生成的运行时走 `events.subscribe` RPC 完成。
|
||||
3. `UnregisterOutputChannel` 易被当成「可用但会报错」——实际返回 nil,
|
||||
**静默无效**(桥不注入 unregister),不报错也不注销。
|
||||
|
||||
## 检索
|
||||
|
||||
站内有两套检索,互补:
|
||||
|
||||
- **MkDocs 内置搜索**(右上角):全文检索,中文走 jieba 分词。
|
||||
- **自建 API 检索**(首页与 API 参考页的输入框):读 `assets/api-index.json`,
|
||||
专门解决「**按描述找 API**」——搜「注册工具」能找到 `RegisterTool`,
|
||||
搜「崩溃」能找到 `SetAutoRestart`,并可区分公开/仅内置。
|
||||
|
||||
自建检索支持四类查询:名称、描述(中英文)、`限定符.方法`
|
||||
(如 `memory.recall`)、签名片段(如 `(string) error`)。
|
||||
|
||||
## 维护提示
|
||||
|
||||
- **改了 `sdk/*.go` 的注释或签名** → 重跑 `build.sh`,改动自动进文档。
|
||||
- **改了能力边界** → 改 `tiers.json`,不要直接改生成的 `.md`。
|
||||
- **新增示例插件** → 自动出现在「示例插件」页的用法表里(扫 `example/`)。
|
||||
- `site_build/` 是构建产物,已 gitignore,不要提交。
|
||||
34
tools/apidoc/build.sh
Executable file
34
tools/apidoc/build.sh
Executable file
@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env bash
|
||||
# 生成并构建插件 SDK 文档站。
|
||||
#
|
||||
# 两步:
|
||||
# 1. apidoc —— 从 sdk/*.go 提取公开 API 面(签名/注释/分层)→ JSON
|
||||
# 2. gensite —— 把 JSON 渲染成 docs/api/*.md + 检索索引
|
||||
# 然后 mkdocs 构建静态站。
|
||||
#
|
||||
# 为什么要脚本而不是手敲:API 参考是**生成物**,必须与源码同步,
|
||||
# 否则文档会悄悄过时(这是文档站最常见的死法)。
|
||||
#
|
||||
# 用法:
|
||||
# tools/apidoc/build.sh # 生成 + 构建
|
||||
# tools/apidoc/build.sh serve # 生成 + 本地预览(热重载)
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
TMP_API="${TMPDIR:-/tmp}/homeagent-sdk-api.json"
|
||||
|
||||
echo "=== 1/3 提取 API 面 ==="
|
||||
go run ./tools/apidoc -pkgdir ./sdk -out "$TMP_API"
|
||||
|
||||
echo "=== 2/3 渲染文档页与检索索引 ==="
|
||||
go run ./tools/apidoc/gensite -api "$TMP_API" -out ./docs -examples ./example
|
||||
|
||||
echo "=== 3/3 构建静态站 ==="
|
||||
if [ "${1:-}" = "serve" ]; then
|
||||
exec mkdocs serve
|
||||
fi
|
||||
mkdocs build --strict
|
||||
echo
|
||||
echo "完成。产物在 site_build/,本地预览:tools/apidoc/build.sh serve"
|
||||
427
tools/apidoc/extract.go
Normal file
427
tools/apidoc/extract.go
Normal file
@ -0,0 +1,427 @@
|
||||
// Command apidoc 从 SDK 源码提取公开 API 面,输出 JSON 供文档站生成使用。
|
||||
//
|
||||
// 设计约束:
|
||||
// - **只用标准库**(go/ast、go/parser、go/token)——不需要网络、不依赖
|
||||
// golang.org/x/tools,clone 下来就能跑。
|
||||
// - **只读源码**,不做 import 解析:它按文件解析 `sdk/*.go`,因此不必处于
|
||||
// 任何 Go module 内,也不会把依赖带进 SDK 主 module。
|
||||
// - 输出是文档站生成的**唯一事实源**:文档里的签名、注释、示例代码块
|
||||
// 全部来自这里,不手抄,避免文档与源码漂移。
|
||||
//
|
||||
// 用法:
|
||||
//
|
||||
// go run ./tools/apidoc -pkgdir ./sdk -out /tmp/api.json
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/doc"
|
||||
"go/parser"
|
||||
"go/printer"
|
||||
"go/token"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Symbol 是一条 API 记录。
|
||||
type Symbol struct {
|
||||
Kind string `json:"kind"` // func / method / type / const / var
|
||||
Recv string `json:"recv"` // 方法接收者(仅 method)
|
||||
Name string `json:"name"` // 符号名
|
||||
Signature string `json:"signature"` // 一行签名
|
||||
Doc string `json:"doc"` // 文档注释(原文,含 markdown)
|
||||
DocBrief string `json:"doc_brief"` // 首行摘要
|
||||
File string `json:"file"`
|
||||
Line int `json:"line"`
|
||||
Group string `json:"group"` // 归属分组(由 groupFor 决定)
|
||||
Exported bool `json:"exported"`
|
||||
Examples []string `json:"examples"` // 注释里的 ```go 代码块
|
||||
// Deprecated/Since 由注释里的标记提取,供文档打标。
|
||||
Deprecated bool `json:"deprecated"`
|
||||
// BuiltinOnly 标记「仅内核内置插件可用」——由 tiers.json 注入。
|
||||
BuiltinOnly bool `json:"builtin_only"`
|
||||
// Tier 是可见级别(public / builtin / bridge)——由 tiers.json 注入。
|
||||
// 用显式字段而非从 TierReason 里找关键字判断:中文说明里「桥接」两字
|
||||
// 在公开条目的理由里也会出现(“桥接模板注入…外部插件可用”),
|
||||
// 靠 strings.Contains 判断会把公开 API 误标成装配点。
|
||||
Tier string `json:"tier"`
|
||||
TierReason string `json:"tier_reason"`
|
||||
}
|
||||
|
||||
// Interface 是一个接口类型及其方法。
|
||||
type Interface struct {
|
||||
Name string `json:"name"`
|
||||
Doc string `json:"doc"`
|
||||
Methods []Symbol `json:"methods"`
|
||||
}
|
||||
|
||||
// Package 是提取结果。
|
||||
type Package struct {
|
||||
ImportPath string `json:"import_path"`
|
||||
Doc string `json:"doc"`
|
||||
Symbols []Symbol `json:"symbols"`
|
||||
Interfaces []Interface `json:"interfaces"`
|
||||
// ConstGroups 保留源码里 const(...) 的分组结构。
|
||||
ConstGroups []ConstGroup `json:"const_groups"`
|
||||
SDKVersion string `json:"sdk_version"`
|
||||
}
|
||||
|
||||
type ConstGroup struct {
|
||||
Doc string `json:"doc"`
|
||||
Consts []Symbol `json:"consts"`
|
||||
}
|
||||
|
||||
var (
|
||||
codeBlockRe = regexp.MustCompile("(?s)```(?:go|bash|json|)\n(.*?)```")
|
||||
deprecatedRe = regexp.MustCompile(`(?i)\b(deprecated|已废弃|已弃用|即将移除)\b`)
|
||||
)
|
||||
|
||||
func main() {
|
||||
pkgdir := flag.String("pkgdir", "./sdk", "要解析的包目录")
|
||||
out := flag.String("out", "-", "输出 JSON 路径,- 表示 stdout")
|
||||
flag.Parse()
|
||||
|
||||
fset := token.NewFileSet()
|
||||
pkgs, err := parser.ParseDir(fset, *pkgdir, func(fi os.FileInfo) bool {
|
||||
return !strings.HasSuffix(fi.Name(), "_test.go")
|
||||
}, parser.ParseComments)
|
||||
if err != nil {
|
||||
log.Fatalf("解析 %s 失败: %v", *pkgdir, err)
|
||||
}
|
||||
|
||||
var result Package
|
||||
for name, pkg := range pkgs {
|
||||
result.ImportPath = name
|
||||
// doc.New 会归并同名符号、抽取示例,并给出包级文档。
|
||||
d := doc.New(pkg, name, doc.AllDecls)
|
||||
result.Doc = strings.TrimSpace(d.Doc)
|
||||
|
||||
for _, f := range d.Funcs {
|
||||
result.Symbols = append(result.Symbols, makeFunc(fset, f, *pkgdir))
|
||||
}
|
||||
for _, t := range d.Types {
|
||||
result.Symbols = append(result.Symbols, makeType(fset, t, *pkgdir))
|
||||
for _, m := range t.Methods {
|
||||
result.Symbols = append(result.Symbols, makeMethod(fset, m, t.Name, *pkgdir))
|
||||
}
|
||||
if iface, ok := t.Decl.Specs[0].(*ast.TypeSpec).Type.(*ast.InterfaceType); ok {
|
||||
result.Interfaces = append(result.Interfaces,
|
||||
makeInterface(t, iface, fset, *pkgdir))
|
||||
}
|
||||
}
|
||||
// const/var 用 Value 承载,按源码 const 块分组保留。
|
||||
for _, v := range d.Consts {
|
||||
result.Symbols = append(result.Symbols, makeValue(fset, v, "const", *pkgdir)...)
|
||||
}
|
||||
for _, v := range d.Vars {
|
||||
result.Symbols = append(result.Symbols, makeValue(fset, v, "var", *pkgdir)...)
|
||||
}
|
||||
result.ConstGroups = groupConsts(fset, pkg, *pkgdir)
|
||||
}
|
||||
|
||||
sort.Slice(result.Symbols, func(i, j int) bool {
|
||||
if result.Symbols[i].Group != result.Symbols[j].Group {
|
||||
return result.Symbols[i].Group < result.Symbols[j].Group
|
||||
}
|
||||
return result.Symbols[i].Name < result.Symbols[j].Name
|
||||
})
|
||||
for i := range result.Interfaces {
|
||||
sort.Slice(result.Interfaces[i].Methods, func(a, b int) bool {
|
||||
return result.Interfaces[i].Methods[a].Name < result.Interfaces[i].Methods[b].Name
|
||||
})
|
||||
}
|
||||
|
||||
if err := applyTiers(&result); err != nil {
|
||||
log.Fatalf("应用能力分层失败: %v", err)
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(result, "", " ")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if *out == "-" {
|
||||
os.Stdout.Write(data)
|
||||
return
|
||||
}
|
||||
if err := os.WriteFile(*out, append(data, '\n'), 0o644); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "提取 %d 个符号 → %s\n", len(result.Symbols), *out)
|
||||
}
|
||||
|
||||
func oneLine(s string) string {
|
||||
return strings.Join(strings.Fields(s), " ")
|
||||
}
|
||||
|
||||
func brief(doc string) string {
|
||||
for _, line := range strings.Split(doc, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line != "" && !strings.HasPrefix(line, "//") {
|
||||
return line
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func examplesOf(doc string) []string {
|
||||
var out []string
|
||||
for _, m := range codeBlockRe.FindAllStringSubmatch(doc, -1) {
|
||||
out = append(out, strings.TrimRight(m[1], "\n"))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func finish(s *Symbol) Symbol {
|
||||
s.Doc = strings.TrimSpace(s.Doc)
|
||||
s.DocBrief = brief(s.Doc)
|
||||
s.Examples = examplesOf(s.Doc)
|
||||
s.Deprecated = deprecatedRe.MatchString(s.DocBrief)
|
||||
s.Group = groupFor(s)
|
||||
return *s
|
||||
}
|
||||
|
||||
func makeFunc(fset *token.FileSet, f *doc.Func, pkgdir string) Symbol {
|
||||
pos := fset.Position(f.Decl.Pos())
|
||||
return finish(&Symbol{
|
||||
Kind: "func",
|
||||
Name: f.Name,
|
||||
Signature: sigOf(fset, f.Decl),
|
||||
Doc: f.Doc,
|
||||
File: rel(pkgdir, pos.Filename),
|
||||
Line: pos.Line,
|
||||
Exported: ast.IsExported(f.Name),
|
||||
})
|
||||
}
|
||||
|
||||
func makeMethod(fset *token.FileSet, f *doc.Func, recv, pkgdir string) Symbol {
|
||||
pos := fset.Position(f.Decl.Pos())
|
||||
return finish(&Symbol{
|
||||
Kind: "method",
|
||||
Recv: recv,
|
||||
Name: f.Name,
|
||||
Signature: sigOf(fset, f.Decl),
|
||||
Doc: f.Doc,
|
||||
File: rel(pkgdir, pos.Filename),
|
||||
Line: pos.Line,
|
||||
Exported: ast.IsExported(f.Name),
|
||||
})
|
||||
}
|
||||
|
||||
func makeType(fset *token.FileSet, t *doc.Type, pkgdir string) Symbol {
|
||||
pos := fset.Position(t.Decl.Pos())
|
||||
return finish(&Symbol{
|
||||
Kind: "type",
|
||||
Name: t.Name,
|
||||
Signature: "type " + t.Name + " " + typeShape(fset, t),
|
||||
Doc: t.Doc,
|
||||
File: rel(pkgdir, pos.Filename),
|
||||
Line: pos.Line,
|
||||
Exported: ast.IsExported(t.Name),
|
||||
})
|
||||
}
|
||||
|
||||
func makeValue(fset *token.FileSet, v *doc.Value, kind, pkgdir string) []Symbol {
|
||||
// 一个 const/var 声明里可能有多组名字(如 CapText/CapFile/... 同块),
|
||||
// 拆成多条——把名字用逗号拼成一条在文档里很难读,检索也搜不到。
|
||||
var out []Symbol
|
||||
for _, spec := range v.Decl.Specs {
|
||||
vs, ok := spec.(*ast.ValueSpec)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
docText := strings.TrimSpace(vs.Doc.Text())
|
||||
if docText == "" {
|
||||
docText = strings.TrimSpace(v.Doc)
|
||||
}
|
||||
for _, n := range vs.Names {
|
||||
if !ast.IsExported(n.Name) {
|
||||
continue
|
||||
}
|
||||
pos := fset.Position(n.Pos())
|
||||
out = append(out, finish(&Symbol{
|
||||
Kind: kind,
|
||||
Name: n.Name,
|
||||
Signature: kind + " " + n.Name,
|
||||
Doc: docText,
|
||||
File: rel(pkgdir, pos.Filename),
|
||||
Line: pos.Line,
|
||||
Exported: true,
|
||||
}))
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// makeInterface 从接口类型的 AST 直接读方法。
|
||||
//
|
||||
// 注意:不能用 doc.Type.Methods —— 那个字段只收集**具名类型的方法声明**
|
||||
// (即 `func (x T) M()`),不含接口内嵌的方法列表。接口成员只能在 AST 的
|
||||
// InterfaceType.Methods 里拿到。
|
||||
func makeInterface(t *doc.Type, iface *ast.InterfaceType, fset *token.FileSet, pkgdir string) Interface {
|
||||
it := Interface{Name: t.Name, Doc: strings.TrimSpace(t.Doc)}
|
||||
for _, field := range iface.Methods.List {
|
||||
if len(field.Names) == 0 {
|
||||
// 内嵌接口(如 interface { io.Closer }):记为一条说明性条目。
|
||||
pos := fset.Position(field.Pos())
|
||||
embedded := oneLine(exprString(field.Type))
|
||||
it.Methods = append(it.Methods, finish(&Symbol{
|
||||
Kind: "embedded",
|
||||
Recv: t.Name,
|
||||
Name: embedded,
|
||||
Signature: embedded,
|
||||
Doc: strings.TrimSpace(field.Doc.Text()),
|
||||
File: rel(pkgdir, pos.Filename),
|
||||
Line: pos.Line,
|
||||
Exported: true,
|
||||
}))
|
||||
continue
|
||||
}
|
||||
ft, ok := field.Type.(*ast.FuncType)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
pos := fset.Position(field.Pos())
|
||||
sig := oneLine(exprString(ft))
|
||||
for _, n := range field.Names {
|
||||
if !ast.IsExported(n.Name) {
|
||||
continue
|
||||
}
|
||||
full := name(n.Name) + strings.TrimPrefix(sig, "func")
|
||||
it.Methods = append(it.Methods, finish(&Symbol{
|
||||
Kind: "method",
|
||||
Recv: t.Name,
|
||||
Name: n.Name,
|
||||
Signature: full,
|
||||
Doc: strings.TrimSpace(field.Doc.Text()),
|
||||
File: rel(pkgdir, pos.Filename),
|
||||
Line: pos.Line,
|
||||
Exported: true,
|
||||
}))
|
||||
}
|
||||
}
|
||||
_ = iface
|
||||
return it
|
||||
}
|
||||
|
||||
// exprString 用 go/printer 把 AST 节点还原成源码文本。
|
||||
func exprString(n ast.Node) string {
|
||||
var buf strings.Builder
|
||||
if err := printer.Fprint(&buf, token.NewFileSet(), n); err != nil {
|
||||
return "?"
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func name(s string) string { return s }
|
||||
|
||||
func sigOf(fset *token.FileSet, fn *ast.FuncDecl) string {
|
||||
if fn.Type == nil {
|
||||
return fn.Name.Name
|
||||
}
|
||||
// 用源码原文截取签名,保证与源码逐字一致(不重新格式化)。
|
||||
start := fset.Position(fn.Pos()).Offset
|
||||
end := fset.Position(fn.Type.End()).Offset
|
||||
src, err := os.ReadFile(fset.Position(fn.Pos()).Filename)
|
||||
if err == nil && start < end && end <= len(src) {
|
||||
return oneLine(string(src[start:end]))
|
||||
}
|
||||
return fn.Name.Name
|
||||
}
|
||||
|
||||
func typeShape(fset *token.FileSet, t *doc.Type) string {
|
||||
spec, ok := t.Decl.Specs[0].(*ast.TypeSpec)
|
||||
if !ok {
|
||||
return "?"
|
||||
}
|
||||
start := fset.Position(spec.Type.Pos()).Offset
|
||||
end := fset.Position(spec.Type.End()).Offset
|
||||
src, err := os.ReadFile(fset.Position(spec.Type.Pos()).Filename)
|
||||
if err != nil || start >= end || end > len(src) {
|
||||
return "?"
|
||||
}
|
||||
raw := src[start:end]
|
||||
// 结构体只保留第一行 + 字段数提示,完整字段在 API 页单独展开。
|
||||
if len(raw) > 160 {
|
||||
return oneLine(string(raw[:160])) + " …"
|
||||
}
|
||||
return oneLine(string(raw))
|
||||
}
|
||||
|
||||
func rel(base, p string) string {
|
||||
if r, err := filepath.Rel(base, p); err == nil {
|
||||
return r
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// groupFor 把符号归到文档站的章节。规则集中在这里,避免散落。
|
||||
func groupFor(s *Symbol) string {
|
||||
switch {
|
||||
case s.Recv == "PluginSDK" || strings.HasPrefix(s.Name, "PluginSDK"):
|
||||
return "plugin-sdk"
|
||||
case s.Recv == "StageContext":
|
||||
return "stages"
|
||||
case s.Kind == "const" || s.Kind == "var":
|
||||
return "constants"
|
||||
case s.Recv == "" && s.Kind == "func":
|
||||
return "functions"
|
||||
case s.Kind == "type":
|
||||
return "types"
|
||||
case s.Recv != "":
|
||||
return "interfaces"
|
||||
}
|
||||
return "misc"
|
||||
}
|
||||
|
||||
func groupConsts(fset *token.FileSet, pkg *ast.Package, pkgdir string) []ConstGroup {
|
||||
var groups []ConstGroup
|
||||
for _, f := range pkg.Files {
|
||||
for _, decl := range f.Decls {
|
||||
gd, ok := decl.(*ast.GenDecl)
|
||||
if !ok || gd.Tok != token.CONST {
|
||||
continue
|
||||
}
|
||||
g := ConstGroup{}
|
||||
if gd.Doc != nil {
|
||||
g.Doc = strings.TrimSpace(gd.Doc.Text())
|
||||
}
|
||||
for _, spec := range gd.Specs {
|
||||
vs, ok := spec.(*ast.ValueSpec)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
var names []string
|
||||
for _, n := range vs.Names {
|
||||
if ast.IsExported(n.Name) {
|
||||
names = append(names, n.Name)
|
||||
}
|
||||
}
|
||||
if len(names) == 0 {
|
||||
continue
|
||||
}
|
||||
pos := fset.Position(vs.Pos())
|
||||
g.Consts = append(g.Consts, Symbol{
|
||||
Kind: "const",
|
||||
Name: strings.Join(names, ", "),
|
||||
Doc: strings.TrimSpace(vs.Doc.Text()),
|
||||
DocBrief: brief(strings.TrimSpace(vs.Doc.Text())),
|
||||
File: rel(pkgdir, pos.Filename),
|
||||
Line: pos.Line,
|
||||
Exported: true,
|
||||
Group: "constants",
|
||||
})
|
||||
}
|
||||
if len(g.Consts) > 0 {
|
||||
groups = append(groups, g)
|
||||
}
|
||||
}
|
||||
}
|
||||
return groups
|
||||
}
|
||||
596
tools/apidoc/gensite/main.go
Normal file
596
tools/apidoc/gensite/main.go
Normal file
@ -0,0 +1,596 @@
|
||||
// Command gensite 把 apidoc 提取出的 api.json 渲染成文档站的 Markdown 页面,
|
||||
// 并额外产出一份供浏览器即时检索的索引。
|
||||
//
|
||||
// 为什么要「生成」而不是手写:API 面有 100+ 个符号,手抄必然与源码漂移。
|
||||
// 这里的每个签名、每段说明都直接来自源码注释,因此文档只在「人写的指南」
|
||||
// 部分才需要人工维护。
|
||||
//
|
||||
// 用法:
|
||||
//
|
||||
// go run ./tools/apidoc -pkgdir ./sdk -out /tmp/api.json # 先提取
|
||||
// go run ./tools/apidoc/gensite -api /tmp/api.json -out ./docs # 再渲染
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 与 extract.go 的 Package 结构对应(两个 main 包不共享代码,故重复声明)。
|
||||
type Symbol struct {
|
||||
Kind string `json:"kind"`
|
||||
Recv string `json:"recv"`
|
||||
Name string `json:"name"`
|
||||
Signature string `json:"signature"`
|
||||
Doc string `json:"doc"`
|
||||
DocBrief string `json:"doc_brief"`
|
||||
File string `json:"file"`
|
||||
Line int `json:"line"`
|
||||
Group string `json:"group"`
|
||||
Exported bool `json:"exported"`
|
||||
Examples []string `json:"examples"`
|
||||
Deprecated bool `json:"deprecated"`
|
||||
BuiltinOnly bool `json:"builtin_only"`
|
||||
Tier string `json:"tier"`
|
||||
TierReason string `json:"tier_reason"`
|
||||
}
|
||||
|
||||
type Interface struct {
|
||||
Name string `json:"name"`
|
||||
Doc string `json:"doc"`
|
||||
Methods []Symbol `json:"methods"`
|
||||
}
|
||||
|
||||
type ConstGroup struct {
|
||||
Doc string `json:"doc"`
|
||||
Consts []Symbol `json:"consts"`
|
||||
}
|
||||
|
||||
type Package struct {
|
||||
ImportPath string `json:"import_path"`
|
||||
Doc string `json:"doc"`
|
||||
Symbols []Symbol `json:"symbols"`
|
||||
Interfaces []Interface `json:"interfaces"`
|
||||
ConstGroups []ConstGroup `json:"const_groups"`
|
||||
}
|
||||
|
||||
// siteSection 是 API 参考的一个页面。
|
||||
type siteSection struct {
|
||||
File string // 输出文件名(不含 .md)
|
||||
Title string // 页面标题
|
||||
Desc string // 页面导语
|
||||
Match func(Symbol) bool
|
||||
}
|
||||
|
||||
// 章节划分:按「插件作者想做什么」组织,而不是按 Go 的符号类别。
|
||||
// 这是文档好不好用的关键——作者是来找「怎么注册工具」的,不是来找 type 的。
|
||||
var sections = []siteSection{
|
||||
{
|
||||
File: "tools", Title: "工具(Tools)",
|
||||
Desc: "注册 LLM 可调用的工具。工具是插件最主要的能力形态:模型看到 `ToolDef` 的说明后决定是否调用,调用时执行你的 `ToolHandler`。",
|
||||
Match: func(s Symbol) bool {
|
||||
return s.Name == "RegisterTool" || s.Name == "ToolDef" || s.Name == "ToolHandler" || s.Name == "ToolCall" || s.Name == "ToolResult" || s.Name == "ContentBlock" || s.Name == "ToolCleaner"
|
||||
},
|
||||
},
|
||||
{
|
||||
File: "stages", Title: "阶段钩子(Stages)",
|
||||
Desc: "在消息处理管道的固定点位插入自己的逻辑。阶段比工具更底层:工具是模型主动调用的,阶段是流程经过时必然触发的。",
|
||||
Match: func(s Symbol) bool {
|
||||
return s.Group == "stages" || s.Name == "Stage" || s.Name == "StageHandler" || s.Name == "StageScope" || s.Name == "RegisterStage" || strings.HasPrefix(s.Name, "Stage")
|
||||
},
|
||||
},
|
||||
{
|
||||
File: "memory", Title: "记忆(Memory)",
|
||||
Desc: "三层记忆的读写接口:**图记忆**(三元组关系)、**文档记忆**(带元数据的文档)、**文本记忆**(事件流水)。以及知识库。",
|
||||
Match: func(s Symbol) bool {
|
||||
switch s.Name {
|
||||
case "MemoryAPI", "TextMemoryAPI", "DocMemoryAPI", "KnowledgeAPI", "Entity", "Relation", "Triple", "Doc", "TextEvent", "MediaAttachment", "Knowledge", "Memory", "TextMemory", "DocMemory", "DocQuery", "SocialAPI", "PersonProfile", "SocialRelation", "Social":
|
||||
return true
|
||||
}
|
||||
return strings.HasPrefix(s.Recv, "Memory") || strings.HasPrefix(s.Recv, "Doc") || strings.HasPrefix(s.Recv, "TextMemory") || strings.HasPrefix(s.Recv, "Knowledge")
|
||||
},
|
||||
},
|
||||
{
|
||||
File: "channels", Title: "输入 / 输出通道",
|
||||
Desc: "通道是插件与外界(设备、其他 Agent、外部系统)交换消息的入口。**输入通道**接收外部消息,**输出通道**把消息投递出去。",
|
||||
Match: func(s Symbol) bool {
|
||||
switch s.Name {
|
||||
case "RegisterInputChannel", "RegisterOutputChannel", "ChannelDef", "CapText", "CapFile", "CapImage", "CapAudio", "CapStructured", "IOInjector", "InjectText", "InjectTextNoMemory", "InjectTextOpts", "InjectInterruptText", "InjectInterruptTextOpts", "InjectInputSync", "InjectInputSyncOpts", "InjectInputMedia", "InjectInputMediaSync", "InjectInputMediaOpts", "InjectInputMediaSyncOpts", "InjectInterruptMedia", "InjectInterruptMediaOpts", "InjectOptions", "ContextPolicyNone", "ContextPolicyPrune", "RecallPolicyNone", "RecallPolicyAuto", "SetToolBlocks", "ValidContextPolicy", "ValidRecallPolicy", "PriorityL1", "PriorityL2", "PriorityL3", "PriorityL4":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
},
|
||||
{
|
||||
File: "settings", Title: "配置(Settings)",
|
||||
Desc: "声明插件自己的配置项,内核会把它渲染到 WebUI 的设置页,并为每个插件维护独立的配置表。",
|
||||
Match: func(s Symbol) bool { return s.Name == "SettingsAPI" || s.Name == "ConfigDef" || s.Name == "Settings" },
|
||||
},
|
||||
{
|
||||
File: "lifecycle", Title: "生命周期(Lifecycle)",
|
||||
Desc: "插件的启动、停止与卸载回调。停止与卸载是两件事:**停止**是进程/加载状态变化,**卸载**(onRemove)是插件被删除前的清理机会。",
|
||||
Match: func(s Symbol) bool {
|
||||
switch s.Name {
|
||||
case "Plugin", "RegisterStopHandler", "RunStopHandlers", "RegisterOnRemoveHandler", "RunOnRemoveHandlers", "SetAutoRestart", "AutoRestart", "PluginName", "RegisterPluginAPI", "PluginMgr", "PluginMgrAPI", "Settings":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
},
|
||||
{
|
||||
File: "events", Title: "事件(Events)",
|
||||
Desc: "订阅内核事件。**注意**:外部分布式插件的事件订阅不走 `Events()`(该接口在外部插件路径上未被注入,恒为 nil),而是由 `hmapdev` 生成的运行时通过 `events.subscribe` 完成。详见下方说明。",
|
||||
Match: func(s Symbol) bool {
|
||||
return s.Name == "EventSubscriber" || s.Name == "Event" || s.Name == "EventType" || s.Name == "EventHandler" || s.Name == "Events" || strings.HasPrefix(s.Name, "Event")
|
||||
},
|
||||
},
|
||||
{
|
||||
File: "llm", Title: "LLM 调用",
|
||||
Desc: "让插件自己调用模型(而不是只等模型来调你)。",
|
||||
Match: func(s Symbol) bool { return s.Name == "LLMAPI" || s.Name == "LLM" },
|
||||
},
|
||||
{
|
||||
File: "bridge", Title: "桥接装配点(Bridge)",
|
||||
Desc: "以下方法**不是给插件业务代码调的**——它们由 `hmapdev` 生成的运行时在启动时调用,用来把内核能力注入到 SDK 实例。列在这里是为了让「公开 API 面」完整,并说明每个注入点对应什么能力。",
|
||||
Match: func(s Symbol) bool {
|
||||
// 只匹配真正的「装配注入点」:Set*API 系列,以及三个 Register/Injector 注入点。
|
||||
// 注意不能用「Set 开头」一刀切 —— SetAutoRestart / SetToolBlocks 是
|
||||
// 插件业务代码会调的公开方法,不属于装配面,分别归入 lifecycle / channels。
|
||||
if s.Recv == "PluginSDK" && strings.HasPrefix(s.Name, "Set") && strings.HasSuffix(s.Name, "API") {
|
||||
return true
|
||||
}
|
||||
switch s.Name {
|
||||
case "APIRegistrar", "ToolRegistrar", "InputChannelRegistrar", "OutputChannelRegistrar", "OutputChannelUnregistrar",
|
||||
"SetIOInjector", "SetInputChannelRegistrar", "SetOutputChannelRegistrar", "SetOutputChannelUnregistrar", "SetEventSubscriber":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
},
|
||||
{
|
||||
File: "misc", Title: "其他类型",
|
||||
Desc: "剩余的类型与方法:`PluginSDK` 本体的访问器、`StageContext` 的并发控制,以及多模态辅助类型。没有归入上面任何一个主题,但可能仍会用到。",
|
||||
Match: func(s Symbol) bool { return true },
|
||||
},
|
||||
}
|
||||
|
||||
func main() {
|
||||
apiPath := flag.String("api", "/tmp/api.json", "apidoc 提取的 JSON")
|
||||
outDir := flag.String("out", "./docs", "文档站目录")
|
||||
exDir := flag.String("examples", "./example", "示例插件目录(用于抽取真实用法)")
|
||||
flag.Parse()
|
||||
|
||||
raw, err := os.ReadFile(*apiPath)
|
||||
if err != nil {
|
||||
log.Fatalf("读取 %s: %v", *apiPath, err)
|
||||
}
|
||||
var pkg Package
|
||||
if err := json.Unmarshal(raw, &pkg); err != nil {
|
||||
log.Fatalf("解析 %s: %v", *apiPath, err)
|
||||
}
|
||||
|
||||
apiDir := filepath.Join(*outDir, "api")
|
||||
if err := os.MkdirAll(apiDir, 0o755); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// 先收集所有待渲染的符号名,再扫 example/ 取真实用法。
|
||||
want := map[string]bool{}
|
||||
for _, s := range pkg.Symbols {
|
||||
if s.Exported {
|
||||
want[s.Name] = true
|
||||
}
|
||||
}
|
||||
for _, it := range pkg.Interfaces {
|
||||
want[it.Name] = true
|
||||
}
|
||||
usages := scanUsages(*exDir, want)
|
||||
|
||||
used := map[string]bool{}
|
||||
var index []indexEntry
|
||||
|
||||
for _, sec := range sections {
|
||||
var picked []Symbol
|
||||
for _, s := range pkg.Symbols {
|
||||
if !s.Exported || !sec.Match(s) || used[key(s)] {
|
||||
continue
|
||||
}
|
||||
picked = append(picked, s)
|
||||
used[key(s)] = true
|
||||
}
|
||||
// 该章节涉及的接口(其方法单独列在接口下,避免重复)。
|
||||
var ifaces []Interface
|
||||
for _, it := range pkg.Interfaces {
|
||||
if sec.Match(Symbol{Kind: "type", Name: it.Name}) {
|
||||
ifaces = append(ifaces, it)
|
||||
}
|
||||
}
|
||||
if len(picked) == 0 && len(ifaces) == 0 {
|
||||
continue
|
||||
}
|
||||
sort.Slice(picked, func(i, j int) bool { return picked[i].Name < picked[j].Name })
|
||||
md := renderSection(sec, picked, ifaces, usages)
|
||||
if err := writeFile(filepath.Join(apiDir, sec.File+".md"), md); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
for _, s := range picked {
|
||||
index = append(index, indexEntry{
|
||||
N: s.Name, S: s.Signature, D: s.DocBrief,
|
||||
K: s.Kind, R: s.Recv, P: sec.File,
|
||||
B: s.BuiltinOnly, F: s.File, L: s.Line,
|
||||
})
|
||||
}
|
||||
// 接口本身与其方法也要进索引。此前只加了顶层符号,导致
|
||||
// `MemoryAPI.Recall` 这类接口方法搜不到(只能靠页面浏览)。
|
||||
for _, it := range ifaces {
|
||||
index = append(index, indexEntry{
|
||||
N: it.Name, S: "type " + it.Name + " interface",
|
||||
D: briefOf(it.Doc), K: "interface", P: sec.File,
|
||||
})
|
||||
for _, m := range it.Methods {
|
||||
index = append(index, indexEntry{
|
||||
N: m.Name, S: m.Signature, D: m.DocBrief,
|
||||
K: "method", R: it.Name, P: sec.File,
|
||||
B: m.BuiltinOnly, F: m.File, L: m.Line,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 常量单独成页(它们是取值枚举,不是「怎么做」)。
|
||||
if md := renderConstants(pkg.ConstGroups); md != "" {
|
||||
if err := writeFile(filepath.Join(apiDir, "constants.md"), md); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
for _, g := range pkg.ConstGroups {
|
||||
for _, c := range g.Consts {
|
||||
index = append(index, indexEntry{
|
||||
N: c.Name, S: "const " + c.Name, D: c.DocBrief,
|
||||
K: "const", P: "constants", F: c.File, L: c.Line,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 仅内置 API 汇总页——这是「真实的私密边界」的显式落点。
|
||||
if md := renderBuiltinOnly(pkg); md != "" {
|
||||
if err := writeFile(filepath.Join(apiDir, "builtin-only.md"), md); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// 客户端即时检索索引。
|
||||
//
|
||||
// 除了「按名称」与「按签名」,还带 examples/ 里的真实调用点,
|
||||
// 因为用户的核心诉求是「按描述搜到 API」——描述文本与用法片段
|
||||
// 一起进索引,搜「发消息」能找到 InjectText,搜「注册工具」能找到 RegisterTool。
|
||||
//
|
||||
// 去重:同一符号可能同时作为「顶层方法」与「接口方法」被扫到
|
||||
// (如 PluginSDK 方法与接口方法共享名字)。按 接收者.名称 去掉重复,
|
||||
// 否则搜索结果里同一 API 会出现两次。
|
||||
index = dedupeIndex(index)
|
||||
sort.Slice(index, func(i, j int) bool { return index[i].N < index[j].N })
|
||||
if err := writeJSON(filepath.Join(*outDir, "assets", "api-index.json"), index); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// 示例插件总览页(真实调用点也在这里汇总)。
|
||||
if err := writeFile(filepath.Join(*outDir, "examples", "index.md"), renderExamples(usages)); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
sort.Slice(pkg.Symbols, func(i, j int) bool { return pkg.Symbols[i].Name < pkg.Symbols[j].Name })
|
||||
fmt.Fprintf(os.Stderr, "生成 %d 个章节 + 检索索引 %d 条 → %s\n",
|
||||
len(sections), len(index), apiDir)
|
||||
}
|
||||
|
||||
type indexEntry struct {
|
||||
N string `json:"n"` // 名称
|
||||
S string `json:"s"` // 签名
|
||||
D string `json:"d"` // 描述摘要
|
||||
K string `json:"k"` // 类别
|
||||
R string `json:"r"` // 接收者
|
||||
P string `json:"p"` // 所属页面
|
||||
B bool `json:"b"` // 仅内置
|
||||
F string `json:"f"` // 源文件
|
||||
L int `json:"l"` // 行号
|
||||
}
|
||||
|
||||
func key(s Symbol) string {
|
||||
if s.Recv != "" {
|
||||
return s.Recv + "." + s.Name
|
||||
}
|
||||
return s.Kind + "." + s.Name
|
||||
}
|
||||
|
||||
func briefOf(doc string) string {
|
||||
for _, line := range strings.Split(doc, "\n") {
|
||||
if t := strings.TrimSpace(line); t != "" {
|
||||
return t
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// genBanner 是生成页的首页标记。
|
||||
//
|
||||
// 必须有:docs/api/*.md 会被提交进仓,而它们下次构建就被覆盖。
|
||||
// 没有这行提示,别人手改一处再发现改动消失,会以为是自己弄错了。
|
||||
const genBanner = "<!-- 本页由 tools/apidoc/gensite 从源码生成,请勿手改;要改文档就改 sdk/*.go 的注释。 -->\n\n"
|
||||
|
||||
// renderSection 渲染一个 API 章节。
|
||||
func renderSection(sec siteSection, syms []Symbol, ifaces []Interface, usages map[string][]Usage) string {
|
||||
var b strings.Builder
|
||||
b.WriteString(genBanner)
|
||||
fmt.Fprintf(&b, "# %s\n\n%s\n\n", sec.Title, sec.Desc)
|
||||
|
||||
// 接口优先展示(它们是「能力清单」),再列独立符号。
|
||||
for _, it := range ifaces {
|
||||
fmt.Fprintf(&b, "## `%s`\n\n", it.Name)
|
||||
if it.Doc != "" {
|
||||
fmt.Fprintf(&b, "%s\n\n", it.Doc)
|
||||
}
|
||||
if len(it.Methods) > 0 {
|
||||
b.WriteString("| 方法 | 说明 |\n|---|---|\n")
|
||||
for _, m := range it.Methods {
|
||||
// 锚点必须与标题逐字对应:标题是 `接口.方法`,slug 会把点号丢掉。
|
||||
fmt.Fprintf(&b, "| [`%s`](#%s) | %s |\n",
|
||||
m.Name, anchor(it.Name+"."+m.Name), escapePipe(m.DocBrief))
|
||||
}
|
||||
b.WriteString("\n")
|
||||
for _, m := range it.Methods {
|
||||
b.WriteString(renderSymbol(m, usages[m.Name]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, s := range syms {
|
||||
b.WriteString(renderSymbol(s, usages[s.Name]))
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// renderSymbol 渲染单个符号。签名放进代码块便于复制,说明保留原文 markdown。
|
||||
func renderSymbol(s Symbol, uses []Usage) string {
|
||||
var b strings.Builder
|
||||
|
||||
name := s.Name
|
||||
if s.Recv != "" {
|
||||
name = s.Recv + "." + s.Name
|
||||
}
|
||||
fmt.Fprintf(&b, "### `%s`\n\n", name)
|
||||
|
||||
if s.BuiltinOnly {
|
||||
b.WriteString("!!! warning \"仅内核内置插件可用\"\n")
|
||||
if s.TierReason != "" {
|
||||
b.WriteString(indent(s.TierReason, " ") + "\n")
|
||||
}
|
||||
b.WriteString("\n")
|
||||
} else if s.Tier == "bridge" {
|
||||
b.WriteString("!!! info \"桥接装配点\"\n")
|
||||
b.WriteString(indent(s.TierReason, " ") + "\n\n")
|
||||
}
|
||||
|
||||
b.WriteString("```go\n")
|
||||
b.WriteString(strings.TrimSpace(s.Signature))
|
||||
b.WriteString("\n```\n\n")
|
||||
|
||||
if s.Doc != "" {
|
||||
fmt.Fprintf(&b, "%s\n\n", strings.TrimSpace(s.Doc))
|
||||
}
|
||||
if s.Deprecated {
|
||||
b.WriteString("!!! danger \"已废弃\"\n 不要在新代码里使用。\n\n")
|
||||
}
|
||||
for _, ex := range s.Examples {
|
||||
b.WriteString("**示例**\n\n```go\n" + ex + "\n```\n\n")
|
||||
}
|
||||
if len(uses) > 0 {
|
||||
b.WriteString("**示例插件里的真实用法**\n\n")
|
||||
b.WriteString("| 插件 | 位置 | 代码 |\n|---|---|---|\n")
|
||||
for _, u := range uses {
|
||||
fmt.Fprintf(&b, "| [`%s`](../examples/index.md#%s) | `%s:%d` | `%s` |\n",
|
||||
u.Plugin, u.Plugin, u.File, u.Line, escapePipe(u.Code))
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
if s.File != "" {
|
||||
fmt.Fprintf(&b, "<small>`%s:%d`</small>\n\n", s.File, s.Line)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func renderConstants(groups []ConstGroup) string {
|
||||
if len(groups) == 0 {
|
||||
return ""
|
||||
}
|
||||
var b strings.Builder
|
||||
b.WriteString(genBanner)
|
||||
b.WriteString("# 常量与枚举\n\n")
|
||||
b.WriteString("SDK 里的取值枚举。其中带「仅内置」标注的取值在内核侧会被夹到较低级别。\n\n")
|
||||
for _, g := range groups {
|
||||
title := "相关取值"
|
||||
if len(g.Consts) > 0 {
|
||||
title = g.Consts[0].Name
|
||||
if len(g.Consts) > 1 {
|
||||
title += " 等"
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(&b, "## %s\n\n", title)
|
||||
if g.Doc != "" {
|
||||
fmt.Fprintf(&b, "%s\n\n", strings.TrimSpace(g.Doc))
|
||||
}
|
||||
b.WriteString("| 名称 | 说明 |\n|---|---|\n")
|
||||
for _, c := range g.Consts {
|
||||
fmt.Fprintf(&b, "| `%s` | %s |\n", c.Name, escapePipe(c.DocBrief))
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// renderBuiltinOnly 汇总仅内置符号——把「真正的私密边界」集中在一页,
|
||||
// 外部插件作者一眼能看出哪些不属于自己。
|
||||
func renderBuiltinOnly(pkg Package) string {
|
||||
var hits []Symbol
|
||||
var ifaceHits []struct {
|
||||
iface string
|
||||
m Symbol
|
||||
}
|
||||
for _, s := range pkg.Symbols {
|
||||
if s.BuiltinOnly {
|
||||
hits = append(hits, s)
|
||||
}
|
||||
}
|
||||
for _, it := range pkg.Interfaces {
|
||||
for _, m := range it.Methods {
|
||||
if m.BuiltinOnly {
|
||||
ifaceHits = append(ifaceHits, struct {
|
||||
iface string
|
||||
m Symbol
|
||||
}{it.Name, m})
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(hits) == 0 && len(ifaceHits) == 0 {
|
||||
return ""
|
||||
}
|
||||
var b strings.Builder
|
||||
b.WriteString(genBanner)
|
||||
b.WriteString("# 仅内置插件可用的 API\n\n")
|
||||
b.WriteString("这些 API **存在于公开 SDK 包里**,但在外部(第三方)插件的运行路径上" +
|
||||
"不可用:要么桥接运行时根本不注入它(拿到 nil),要么内核会拒绝/降级。" +
|
||||
"列在这里是为了让边界显式——而不是让你在运行时才发现拿不到。\n\n")
|
||||
b.WriteString("判断依据全部来自源码与 `hmapdev` 桥接模板,逐条记在每条说明里。\n\n")
|
||||
for _, s := range hits {
|
||||
b.WriteString(renderSymbol(s, nil))
|
||||
}
|
||||
for _, h := range ifaceHits {
|
||||
fmt.Fprintf(&b, "### `%s.%s`\n\n!!! warning \"仅内核内置插件可用\"\n", h.iface, h.m.Name)
|
||||
if h.m.TierReason != "" {
|
||||
b.WriteString(indent(h.m.TierReason, " ") + "\n")
|
||||
}
|
||||
fmt.Fprintf(&b, "\n```go\n%s\n```\n\n", strings.TrimSpace(h.m.Signature))
|
||||
if h.m.Doc != "" {
|
||||
fmt.Fprintf(&b, "%s\n\n", strings.TrimSpace(h.m.Doc))
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// renderExamples 汇总示例插件,并把每个插件用到的 API 列出。
|
||||
// 这是「想找一个能跑的参考实现」的入口。
|
||||
func renderExamples(usages map[string][]Usage) string {
|
||||
byPlugin := map[string]map[string]bool{}
|
||||
for api, list := range usages {
|
||||
for _, u := range list {
|
||||
if byPlugin[u.Plugin] == nil {
|
||||
byPlugin[u.Plugin] = map[string]bool{}
|
||||
}
|
||||
byPlugin[u.Plugin][api] = true
|
||||
}
|
||||
}
|
||||
names := make([]string, 0, len(byPlugin))
|
||||
for n := range byPlugin {
|
||||
names = append(names, n)
|
||||
}
|
||||
sort.Strings(names)
|
||||
|
||||
var b strings.Builder
|
||||
b.WriteString(genBanner)
|
||||
b.WriteString("# 示例插件\n\n")
|
||||
b.WriteString("SDK 仓 `example/` 下有多个**真实可编译**的示例插件," +
|
||||
"覆盖工具注册、通道、记忆读写、LLM 调用、生命周期等常见形态。\n\n")
|
||||
b.WriteString("每个示例都能用 `hmapdev build` 打成 `.hmap` 装进内核直接跑。\n\n")
|
||||
for _, n := range names {
|
||||
apis := make([]string, 0, len(byPlugin[n]))
|
||||
for a := range byPlugin[n] {
|
||||
apis = append(apis, a)
|
||||
}
|
||||
sort.Strings(apis)
|
||||
fmt.Fprintf(&b, "## `%s`\n\n", n)
|
||||
fmt.Fprintf(&b, "用到的 API:%s\n\n", codeList(apis))
|
||||
}
|
||||
if len(names) == 0 {
|
||||
b.WriteString("(未找到示例插件调用点)\n")
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// dedupeIndex 按「接收者.名称」去重。无接收者的用「类别.名称」。
|
||||
// 同名但不同接收者(如 MemoryAPI.Recall 与 IOInjector.InjectText)都保留。
|
||||
func dedupeIndex(in []indexEntry) []indexEntry {
|
||||
seen := map[string]bool{}
|
||||
out := in[:0]
|
||||
for _, e := range in {
|
||||
k := e.R + "." + e.N
|
||||
if e.R == "" {
|
||||
k = e.K + "." + e.N
|
||||
}
|
||||
if seen[k] {
|
||||
continue
|
||||
}
|
||||
seen[k] = true
|
||||
out = append(out, e)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func codeList(items []string) string {
|
||||
parts := make([]string, 0, len(items))
|
||||
for _, s := range items {
|
||||
parts = append(parts, "`"+s+"`")
|
||||
}
|
||||
return strings.Join(parts, " · ")
|
||||
}
|
||||
|
||||
// anchor 复现 python-markdown 的 toc slugify:小写,去掉非字母数字与下划线
|
||||
// **以外**的字符(点号、反引号、括号都在此列),空格与下划线**保留为原形**。
|
||||
//
|
||||
// 实测确认:`### \`SettingsAPI.DataDir\“ → id="settingsapidatadir";
|
||||
// `## \`ai_image\“ → id="ai_image"(下划线保留,不转连字符)。
|
||||
// 所以调用方必须传**完整标题文本**(如 "SettingsAPI.DataDir"),不是裸方法名。
|
||||
func anchor(s string) string {
|
||||
var b strings.Builder
|
||||
for _, r := range strings.ToLower(s) {
|
||||
switch {
|
||||
case r >= 'a' && r <= 'z', r >= '0' && r <= '9', r == '_', r == '-':
|
||||
b.WriteRune(r)
|
||||
case r == ' ':
|
||||
b.WriteByte('-')
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func escapePipe(s string) string { return strings.ReplaceAll(s, "|", "\\|") }
|
||||
|
||||
func indent(s, pad string) string {
|
||||
lines := strings.Split(s, "\n")
|
||||
for i := range lines {
|
||||
if strings.TrimSpace(lines[i]) != "" {
|
||||
lines[i] = pad + lines[i]
|
||||
}
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
func writeFile(path, content string) error {
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(path, []byte(content), 0o644)
|
||||
}
|
||||
|
||||
func writeJSON(path string, v any) error {
|
||||
data, err := json.MarshalIndent(v, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return writeFile(path, string(append(data, '\n')))
|
||||
}
|
||||
118
tools/apidoc/gensite/usages.go
Normal file
118
tools/apidoc/gensite/usages.go
Normal file
@ -0,0 +1,118 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Usage 是一条真实用法:某个示例插件在某行用了这个 API。
|
||||
//
|
||||
// 为什么要有它:API 参考最容易变成「签名罗列」——读者看得到参数,
|
||||
// 却不知道该怎么用。SDK 的源码注释里几乎没有可运行示例(实测只有 7 行缩进
|
||||
// 代码块),但 example/ 下有 21 个**真实可编译**的示例插件。把它们里的调用点
|
||||
// 反查到每个 API 上,读者就能直接跳到能跑的代码。
|
||||
type Usage struct {
|
||||
Plugin string `json:"plugin"` // 示例名(example 下的目录名)
|
||||
File string `json:"file"` // 相对 SDK 仓根的路径
|
||||
Line int `json:"line"`
|
||||
Code string `json:"code"` // 该行原文(裁剪首尾空白)
|
||||
}
|
||||
|
||||
var callRe = regexp.MustCompile(`\.([A-Z][A-Za-z0-9_]*)\s*\(`)
|
||||
|
||||
// scanUsages 遍历 example/ 下所有 .go 文件,找出每个 API 的真实调用点。
|
||||
//
|
||||
// names 是要找的符号名集合(越小越快)。只扫 example/,不扫 tools/——
|
||||
// 工具链自己也会调 SDK,那属于内部实现,不是「用法示例」。
|
||||
func scanUsages(exDir string, names map[string]bool) map[string][]Usage {
|
||||
out := map[string][]Usage{}
|
||||
_ = filepath.Walk(exDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil || info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
if !strings.HasSuffix(path, ".go") {
|
||||
return nil
|
||||
}
|
||||
rel, _ := filepath.Rel(filepath.Dir(exDir), path)
|
||||
plugin := pluginName(path, exDir)
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer f.Close()
|
||||
sc := bufio.NewScanner(f)
|
||||
sc.Buffer(make([]byte, 0, 64*1024), 1024*1024)
|
||||
line := 0
|
||||
for sc.Scan() {
|
||||
line++
|
||||
text := sc.Text()
|
||||
trimmed := strings.TrimSpace(text)
|
||||
// 跳过注释行:注释里提到 API 名不算「用法」。
|
||||
if strings.HasPrefix(trimmed, "//") || strings.HasPrefix(trimmed, "*") {
|
||||
continue
|
||||
}
|
||||
for _, m := range callRe.FindAllStringSubmatch(text, -1) {
|
||||
name := m[1]
|
||||
if !names[name] {
|
||||
continue
|
||||
}
|
||||
out[name] = append(out[name], Usage{
|
||||
Plugin: plugin, File: rel, Line: line, Code: truncate(trimmed, 110),
|
||||
})
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
for k := range out {
|
||||
sort.Slice(out[k], func(i, j int) bool {
|
||||
if out[k][i].Plugin != out[k][j].Plugin {
|
||||
return out[k][i].Plugin < out[k][j].Plugin
|
||||
}
|
||||
return out[k][i].Line < out[k][j].Line
|
||||
})
|
||||
// 每个 API 最多留 4 条,避免页面被用法淹没;优先保留不同插件。
|
||||
out[k] = dedupeByPlugin(out[k], 4)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// pluginName 从路径里取示例名:example/<plugin>/....go → <plugin>。
|
||||
func pluginName(path, exDir string) string {
|
||||
rel, err := filepath.Rel(exDir, path)
|
||||
if err != nil {
|
||||
return "?"
|
||||
}
|
||||
parts := strings.Split(rel, string(filepath.Separator))
|
||||
if len(parts) > 0 {
|
||||
return parts[0]
|
||||
}
|
||||
return "?"
|
||||
}
|
||||
|
||||
func dedupeByPlugin(in []Usage, limit int) []Usage {
|
||||
seen := map[string]int{}
|
||||
var out []Usage
|
||||
for _, u := range in {
|
||||
if seen[u.Plugin] >= 1 {
|
||||
continue
|
||||
}
|
||||
seen[u.Plugin]++
|
||||
out = append(out, u)
|
||||
if len(out) >= limit {
|
||||
break
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func truncate(s string, n int) string {
|
||||
r := []rune(s)
|
||||
if len(r) <= n {
|
||||
return s
|
||||
}
|
||||
return string(r[:n]) + "…"
|
||||
}
|
||||
149
tools/apidoc/tiers.go
Normal file
149
tools/apidoc/tiers.go
Normal file
@ -0,0 +1,149 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// Tier 是符号的可见级别。
|
||||
const (
|
||||
TierPublic = "public" // 外部(第三方)插件可用
|
||||
TierBuiltin = "builtin" // 仅内核内置插件可用
|
||||
TierBridge = "bridge" // 桥接运行时注入的装配点(插件业务代码不调)
|
||||
)
|
||||
|
||||
// tierTable 对应 tools/apidoc/tiers.json 的结构。
|
||||
type tierTable struct {
|
||||
Public struct {
|
||||
Source string `json:"_source"`
|
||||
Methods []string `json:"methods"`
|
||||
} `json:"public"`
|
||||
Bridge struct {
|
||||
Doc string `json:"_doc"`
|
||||
Source string `json:"_source"`
|
||||
Methods []string `json:"methods"`
|
||||
} `json:"bridge"`
|
||||
TierOverrides map[string]json.RawMessage `json:"tier_overrides"`
|
||||
Interfaces map[string]json.RawMessage `json:"interfaces"`
|
||||
}
|
||||
|
||||
// applyTiers 把能力分层写回符号。分层表与源码一样是事实源:
|
||||
// 找不到分层表就报错,不静默降级——否则文档会悄悄丢掉「仅内置」标记。
|
||||
func applyTiers(p *Package) error {
|
||||
path := tierPath()
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取 %s: %w", path, err)
|
||||
}
|
||||
var t tierTable
|
||||
if err := json.Unmarshal(data, &t); err != nil {
|
||||
return fmt.Errorf("解析 %s: %w", path, err)
|
||||
}
|
||||
|
||||
pub := map[string]bool{}
|
||||
for _, m := range t.Public.Methods {
|
||||
pub[m] = true
|
||||
}
|
||||
brg := map[string]bool{}
|
||||
for _, m := range t.Bridge.Methods {
|
||||
brg[m] = true
|
||||
}
|
||||
|
||||
// tier_overrides 里既有 "_doc" 这类说明键(值是数组),也有真正的裁定
|
||||
// (值是对象)。逐一解码,跳过 _ 开头的说明键。
|
||||
overrides := map[string]struct {
|
||||
Tier string `json:"tier"`
|
||||
Reason string `json:"reason"`
|
||||
}{}
|
||||
for name, raw := range t.TierOverrides {
|
||||
if len(name) > 0 && name[0] == '_' {
|
||||
continue
|
||||
}
|
||||
var v struct {
|
||||
Tier string `json:"tier"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &v); err != nil {
|
||||
return fmt.Errorf("tier_overrides[%s]: %w", name, err)
|
||||
}
|
||||
overrides[name] = v
|
||||
}
|
||||
|
||||
// 接口级裁定先落到接口本身;其方法继承接口的 tier。
|
||||
ifaceTier := map[string]string{}
|
||||
for name, raw := range t.Interfaces {
|
||||
if len(name) > 0 && name[0] == '_' {
|
||||
continue
|
||||
}
|
||||
var v struct {
|
||||
Tier string `json:"tier"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &v); err != nil {
|
||||
return fmt.Errorf("interfaces[%s]: %w", name, err)
|
||||
}
|
||||
ifaceTier[name] = v.Tier
|
||||
}
|
||||
|
||||
tierOf := func(s Symbol) (string, string) {
|
||||
// 1) 逐符号覆盖优先(它带 reason,最有信息量)。
|
||||
if o, ok := overrides[s.Name]; ok {
|
||||
return o.Tier, o.Reason
|
||||
}
|
||||
// 2) 接口方法:跟随接口的 tier。
|
||||
if s.Recv != "" {
|
||||
if tier, ok := ifaceTier[s.Recv]; ok {
|
||||
return tier, fmt.Sprintf("接口 %s 的层级裁定", s.Recv)
|
||||
}
|
||||
}
|
||||
// 3) 桥接注入点。
|
||||
if brg[s.Name] {
|
||||
return TierBridge, "桥接运行时注入点(由 hmapdev 生成的 proc_main 调用,插件业务代码不调用)"
|
||||
}
|
||||
// 4) 显式公开名单。
|
||||
if pub[s.Name] {
|
||||
return TierPublic, "桥接模板注入或 proc* 实现,外部插件可用"
|
||||
}
|
||||
// 5) 公开包里的方法默认公开;内部专有符号不会出现在本包里。
|
||||
if s.Recv == "PluginSDK" || s.Recv == "StageContext" {
|
||||
return TierPublic, "公开 SDK 的方法,未列入 bridge/内置清单"
|
||||
}
|
||||
return TierPublic, ""
|
||||
}
|
||||
|
||||
for i := range p.Symbols {
|
||||
tier, reason := tierOf(p.Symbols[i])
|
||||
p.Symbols[i].Tier = tier
|
||||
p.Symbols[i].BuiltinOnly = tier == TierBuiltin
|
||||
p.Symbols[i].TierReason = reason
|
||||
}
|
||||
for i := range p.Interfaces {
|
||||
if tier, ok := ifaceTier[p.Interfaces[i].Name]; ok {
|
||||
for j := range p.Interfaces[i].Methods {
|
||||
p.Interfaces[i].Methods[j].Tier = tier
|
||||
p.Interfaces[i].Methods[j].BuiltinOnly = tier == TierBuiltin
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// tierPath 找 tiers.json:先看可执行文件旁,再看源码目录,最后看工作目录。
|
||||
// 这样 `go run ./tools/apidoc` 与编译后的二进制都能找到它。
|
||||
func tierPath() string {
|
||||
candidates := []string{}
|
||||
if exe, err := os.Executable(); err == nil {
|
||||
candidates = append(candidates, filepath.Join(filepath.Dir(exe), "tiers.json"))
|
||||
}
|
||||
candidates = append(candidates,
|
||||
filepath.Join("tools", "apidoc", "tiers.json"),
|
||||
"tiers.json",
|
||||
)
|
||||
for _, c := range candidates {
|
||||
if _, err := os.Stat(c); err == nil {
|
||||
return c
|
||||
}
|
||||
}
|
||||
return candidates[0]
|
||||
}
|
||||
157
tools/apidoc/tiers.json
Normal file
157
tools/apidoc/tiers.json
Normal file
@ -0,0 +1,157 @@
|
||||
{
|
||||
"_doc": [
|
||||
"能力分层表:决定文档站上每个 API 的可见级别。",
|
||||
"",
|
||||
"tier 取值:",
|
||||
" public —— 外部(第三方)插件可用。这是文档站的主体。",
|
||||
" builtin —— 仅内核内置插件可用。外部插件调用会失败或拿到 nil。",
|
||||
" bridge —— 由桥接运行时注入的装配点(SetXxxAPI),插件业务代码不该调;",
|
||||
" 但它是公开 SDK 的一部分,故列出并标注用途。",
|
||||
"",
|
||||
"**每条判断都必须能追溯到源码**,依据记在 reason 里。不要凭文档注释推测——",
|
||||
"实测发现文档与源码有三处不符(见下 tier_overrides 的注释)。"
|
||||
],
|
||||
|
||||
"public": {
|
||||
"_source": "hmapdev 桥接模板 tools/hmapdev/templates/proc_main.go.tmpl 的 buildPluginSDK():凡被 base.Set* 注入或在 procIO/procMemory/... 上实现的,外部插件都能真调到。",
|
||||
"methods": [
|
||||
"PluginName",
|
||||
"Settings",
|
||||
"Memory", "TextMemory", "DocMemory", "Knowledge", "LLM", "Social",
|
||||
"PluginMgr",
|
||||
"RegisterTool", "RegisterStage", "RegisterPluginAPI",
|
||||
"RegisterOutputChannel", "RegisterInputChannel",
|
||||
"InjectText", "InjectTextNoMemory", "InjectTextOpts",
|
||||
"InjectInterruptText", "InjectInterruptTextOpts",
|
||||
"InjectInputSync", "InjectInputSyncOpts",
|
||||
"InjectInputMedia", "InjectInputMediaSync", "InjectInputMediaOpts",
|
||||
"InjectInputMediaSyncOpts", "InjectInterruptMedia", "InjectInterruptMediaOpts",
|
||||
"SetToolBlocks",
|
||||
"SetAutoRestart", "AutoRestart",
|
||||
"RegisterStopHandler", "RunStopHandlers",
|
||||
"RegisterOnRemoveHandler", "RunOnRemoveHandlers"
|
||||
]
|
||||
},
|
||||
|
||||
"bridge": {
|
||||
"_doc": "桥接注入点:公开 SDK 的装配接口,外部插件的**业务代码不调用**它们,由 hmapdev 生成的运行时调用。文档里单列一节说明,不与业务 API 混排。",
|
||||
"_source": "proc_main.go.tmpl:685-705 逐个 base.Set* 调用。",
|
||||
"methods": [
|
||||
"SetIOInjector", "SetMemoryAPI", "SetTextMemoryAPI", "SetDocMemoryAPI",
|
||||
"SetKnowledgeAPI", "SetLLMAPI", "SetSocialAPI", "SetPluginMgrAPI",
|
||||
"SetInputChannelRegistrar",
|
||||
"SetOutputChannelRegistrar", "SetOutputChannelUnregistrar",
|
||||
"SetEventSubscriber"
|
||||
]
|
||||
},
|
||||
|
||||
"tier_overrides": {
|
||||
"_doc": [
|
||||
"逐符号的边界裁定。键是符号名,值是 {tier, reason}。",
|
||||
"reason 必须写出**可复核的依据**(文件:行 或 grep 结论),不接受「大概」。"
|
||||
],
|
||||
|
||||
"Events": {
|
||||
"tier": "builtin",
|
||||
"reason": "实测全仓 SetEventSubscriber 只有定义、无任何调用点(grep -rn SetEventSubscriber --include=*.go 仅命中 sdk/plugin.go 的定义与 lua_plugin.go 的一句注释)。外部插件拿到的 Events() 恒为 nil。外部插件订阅事件走的是桥接运行时的 events.subscribe RPC(proc_main.go.tmpl:1421 在插件侧分发、内核 protocol.go MethodEventsSubscribe),Lua 插件则走内部 SDK 的 Subscribe。这正是 PLUGIN_DEV.md 里没有说清的一处。"
|
||||
},
|
||||
|
||||
"SetEventSubscriber": {
|
||||
"tier": "builtin",
|
||||
"reason": "同上:无调用点。标 builtin 而非 bridge,因为连桥接运行时都不注入它——外部插件无法用它获得事件订阅能力。"
|
||||
},
|
||||
|
||||
"UnregisterOutputChannel": {
|
||||
"tier": "builtin",
|
||||
"reason": "外部插件的桥接模板只注入 SetOutputChannelRegistrar(proc_main.go.tmpl:693-705),**未**注入 SetOutputChannelUnregistrar(grep Unregistrar 在 templates/ 下无命中)。因此外部插件的 regOutputUnreg 为 nil,UnregisterOutputChannel 会命中 `if reg != nil` 的 else 分支**直接返回 nil**(sdk/plugin.go:539-549)——即**静默无效**:不报错、通道也没注销。内置插件由 internal/sdk/plugin.go:330 注入 cfg.RegOutputUnreg,真正生效。外部插件要让通道下线,只能重载插件。"
|
||||
},
|
||||
|
||||
"SetOutputChannelUnregistrar": {
|
||||
"tier": "builtin",
|
||||
"reason": "同上,桥接模板不注入。"
|
||||
},
|
||||
|
||||
"PluginMgr": {
|
||||
"tier": "public",
|
||||
"reason": "proc_main.go.tmpl:692 显式注入 base.SetPluginMgrAPI(procPluginMgr{}),且 sdk/plugin.go:282 注释写明「外部插件可调用」。注意返回的 PluginMgrAPI 只有 3 个方法(ReloadOne / ListLoadedPlugins / IsPluginDisabled),与 internal/sdk 的完整 PluginManager(含 ReloadPlugins / DisablePlugin / RemovePlugin / ListDisabledPlugins / IsBuiltinPlugin 等)**不是同一个接口**——同名不同包,文档必须分清。PLUGIN_DEV.md:786 写「PluginMgr() 仅内置插件可用」是**错的**,已在本站更正。"
|
||||
},
|
||||
|
||||
"SetToolBlocks": {
|
||||
"tier": "public",
|
||||
"reason": "procIO 实现了它(proc_main.go.tmpl:749),模板在 base.SetIOInjector(procIO{}) 中注入。"
|
||||
},
|
||||
|
||||
"RunStopHandlers": {
|
||||
"tier": "public",
|
||||
"reason": "由生成的运行时在收到 plugin.stop 时调用(proc_main.go.tmpl:1274、1659),插件注册的 stop handler 由此触发;插件业务代码也可直接调。"
|
||||
},
|
||||
|
||||
"RunOnRemoveHandlers": {
|
||||
"tier": "public",
|
||||
"reason": "与 RunStopHandlers 同源;onRemove 语义见 sdk/plugin.go:824。"
|
||||
},
|
||||
|
||||
"PriorityL4": {
|
||||
"tier": "builtin",
|
||||
"reason": "sdk/plugin.go:119-126 明确:L1..L3 任何插件可声明,L4 只有内核级插件可用,外部插件声明会被内核夹到 L3(内核侧有 priority_clamp_test.go 守着)。外部插件文档应说明「声明 L4 无效」。"
|
||||
},
|
||||
|
||||
"Subscribe": {
|
||||
"tier": "builtin",
|
||||
"reason": "Subscribe 只存在于内部 SDK(internal/sdk)与 Lua 桥;公开 sdk 的 EventSubscriber 接口虽声明了 Subscribe,但该接口实例在外部插件路径上恒为 nil(见 Events 条目)。"
|
||||
},
|
||||
|
||||
"Publish": {
|
||||
"tier": "builtin",
|
||||
"reason": "Publish 只在 internal/sdk/plugin.go:465(内置插件面)。公开 SDK 的 EventSubscriber 接口刻意只有 Subscribe 没有 Publish——sdk/plugin.go:276 注释「restricted interface: plugins can subscribe but the kernel controls which events are delivered」。"
|
||||
},
|
||||
|
||||
"OutputChan": {
|
||||
"tier": "builtin",
|
||||
"reason": "只存在于 internal/sdk/plugin.go:438。外部插件的输出能力是 RegisterOutputChannel + 内核回调(output.invoke),不是自己持有 chan。"
|
||||
},
|
||||
|
||||
"RegisterChannel": {
|
||||
"tier": "builtin",
|
||||
"reason": "internal/sdk/plugin.go:445 的内部面(agentIO.Device 直接注册)。外部插件用公开的 RegisterInputChannel / RegisterOutputChannel。"
|
||||
},
|
||||
|
||||
"ListChannels": {
|
||||
"tier": "builtin",
|
||||
"reason": "internal/sdk/plugin.go:458。PLUGIN_DEV.md:530 已正确说明这些方法(RegisterChannel/UnregisterChannel/ListChannels/InjectInput 等)仅内置插件可用。"
|
||||
},
|
||||
|
||||
"InjectInput": {
|
||||
"tier": "builtin",
|
||||
"reason": "internal/sdk/plugin.go:413。外部插件用公开的 InjectText / InjectInputSync / InjectTextOpts 等。"
|
||||
},
|
||||
|
||||
"InjectInterrupt": {
|
||||
"tier": "builtin",
|
||||
"reason": "internal/sdk/plugin.go:420。外部插件用 InjectInterruptText / InjectInterruptMedia。"
|
||||
}
|
||||
},
|
||||
|
||||
"interfaces": {
|
||||
"_doc": "接口级裁定:外部插件拿到的是「受限接口」,方法是子集。",
|
||||
"SocialAPI": {
|
||||
"tier": "public",
|
||||
"reason": "外部插件由 proc_main.go.tmpl:690 注入 procSocial。注意公开 SocialAPI 只有 6 个**只读**方法(GetPerson/GetTrait/GetRelations/GetNetwork/ListPersons + 见 sdk/memory.go:100)。写操作(SetTrait/AddRelation 等)不在公开接口里——这是「受限 SDK」的实现方式:**按接口裁剪,而非按方法裁剪**。"
|
||||
},
|
||||
"EventSubscriber": {
|
||||
"tier": "builtin",
|
||||
"reason": "接口在公开包里,但实例恒 nil(见 Events)。"
|
||||
},
|
||||
"MemoryAPI": { "tier": "public", "reason": "proc_main.go.tmpl:686 注入 procMemory。" },
|
||||
"TextMemoryAPI": { "tier": "public", "reason": "proc_main.go.tmpl:691。" },
|
||||
"DocMemoryAPI": { "tier": "public", "reason": "proc_main.go.tmpl:687。" },
|
||||
"KnowledgeAPI": { "tier": "public", "reason": "proc_main.go.tmpl:688。" },
|
||||
"LLMAPI": { "tier": "public", "reason": "proc_main.go.tmpl:689。" },
|
||||
"SettingsAPI": { "tier": "public", "reason": "sdk 构造函数第三参数注入 procSettings(proc_main.go.tmpl:640)。" },
|
||||
"IOInjector": { "tier": "public", "reason": "proc_main.go.tmpl:685 注入 procIO。" },
|
||||
"PluginMgrAPI": {
|
||||
"tier": "public",
|
||||
"reason": "proc_main.go.tmpl:692。只有 3 个方法——与内部 PluginManager 不同。"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user