feat(sdk): 通用反代声明项(DeclareProxy)+ ToolDef.Serial 串行标记 + 场面策略文档

本次一并提交工作区此前累积的改动(均已验证),并接入工具并发调度所需的
声明项。

把「谁来反代谁」从内核硬编码变成插件可声明。设备网关(remotedevice)
这类**编译进内核、没有独立插件目录与 plugin.json** 的服务,静态扫描扫不到,
此前只能靠约定。新增 DeclareProxy 让它们能自己声明反代路由。

ParallelSafe 的**反向**声明项。判据优先级:Serial 胜出,显式声明不允许被
ParallelSafe 或任何默认值覆盖。

为什么需要它:ParallelSafe 零值 false 已表达「安全/串行」,插件无法区分
「我没想过」和「我确认过必须串行」。没有这个区分,工具作者只能靠命名约定
传递意图,那不是契约。

ParallelSafe 本身也补齐了注释,明确其零值语义(默认串行、保守)与理由
(新语义下并发会改变工具的行为前提,让存量插件意外并发比慢一点危险得多)。

配套 ScenePolicy 声明项的使用说明。

remotedevice/ 整目录(C 实现的设备网关,已由 Go 侧 DeclareProxy 路径取代)。

- sdk/knowledge.go:随场面策略配套调整
- docs/api/*、docs/assets/api-index.json、docs/llms.txt、mkdocs.yml:
  由 tools/apidoc/build.sh 从源码重新生成(行号随 plugin.go 变动漂移)
This commit is contained in:
JianFeeeee
2026-09-27 14:52:26 +08:00
parent e417c69fc8
commit bd73a9b241
26 changed files with 719 additions and 3656 deletions

View File

@ -12,7 +12,7 @@ type APIRegistrar func(name string) error
APIRegistrar registers a plugin API for external access.
<small>`plugin.go:311`</small>
<small>`plugin.go:410`</small>
### `InputChannelRegistrar`
@ -22,7 +22,7 @@ type InputChannelRegistrar func(name string, def ChannelDef) error
InputChannelRegistrar registers an input channel with its memory behavior.
<small>`plugin.go:314`</small>
<small>`plugin.go:413`</small>
### `OutputChannelRegistrar`
@ -32,7 +32,7 @@ type OutputChannelRegistrar func(name string, caps int, desc string, def Channel
OutputChannelRegistrar registers an output channel that the output_send tool can use.
<small>`plugin.go:317`</small>
<small>`plugin.go:416`</small>
### `OutputChannelUnregistrar`
@ -46,7 +46,7 @@ OutputChannelUnregistrar 注销一个输出通道。
动态通道 —— 典型是远程设备:`device/<id>` 只在设备在线期间存在,设备掉线后
必须注销,否则 output_list_channels 会一直列着它、模型会往一个死通道发消息。
<small>`plugin.go:324`</small>
<small>`plugin.go:423`</small>
### `PluginSDK.SetDocMemoryAPI`
@ -57,7 +57,7 @@ OutputChannelUnregistrar 注销一个输出通道。
func (s *PluginSDK) SetDocMemoryAPI(dm DocMemoryAPI)
```
<small>`plugin.go:619`</small>
<small>`plugin.go:718`</small>
### `PluginSDK.SetEventSubscriber`
@ -68,7 +68,7 @@ func (s *PluginSDK) SetDocMemoryAPI(dm DocMemoryAPI)
func (s *PluginSDK) SetEventSubscriber(es EventSubscriber)
```
<small>`plugin.go:643`</small>
<small>`plugin.go:742`</small>
### `PluginSDK.SetIOInjector`
@ -81,7 +81,7 @@ func (s *PluginSDK) SetIOInjector(io IOInjector)
SetIOInjector sets the IO injector (called by the core at startup).
<small>`plugin.go:600`</small>
<small>`plugin.go:699`</small>
### `PluginSDK.SetInputChannelRegistrar`
@ -94,7 +94,7 @@ func (s *PluginSDK) SetInputChannelRegistrar(r InputChannelRegistrar)
SetInputChannelRegistrar sets the input channel registrar (called by the core at startup).
<small>`plugin.go:593`</small>
<small>`plugin.go:692`</small>
### `PluginSDK.SetKnowledgeAPI`
@ -105,7 +105,7 @@ SetInputChannelRegistrar sets the input channel registrar (called by the core at
func (s *PluginSDK) SetKnowledgeAPI(kn KnowledgeAPI)
```
<small>`plugin.go:625`</small>
<small>`plugin.go:724`</small>
### `PluginSDK.SetLLMAPI`
@ -116,7 +116,7 @@ func (s *PluginSDK) SetKnowledgeAPI(kn KnowledgeAPI)
func (s *PluginSDK) SetLLMAPI(llm LLMAPI)
```
<small>`plugin.go:631`</small>
<small>`plugin.go:730`</small>
### `PluginSDK.SetMemoryAPI`
@ -129,7 +129,7 @@ func (s *PluginSDK) SetMemoryAPI(mem MemoryAPI)
SetMemoryAPI sets the memory API (called by the core at startup).
<small>`plugin.go:607`</small>
<small>`plugin.go:706`</small>
### `PluginSDK.SetOutputChannelRegistrar`
@ -142,7 +142,7 @@ func (s *PluginSDK) SetOutputChannelRegistrar(r OutputChannelRegistrar)
SetOutputChannelRegistrar sets the output channel registrar (called by the core at startup).
<small>`plugin.go:579`</small>
<small>`plugin.go:678`</small>
### `PluginSDK.SetOutputChannelUnregistrar`
@ -155,7 +155,7 @@ func (s *PluginSDK) SetOutputChannelUnregistrar(r OutputChannelUnregistrar)
SetOutputChannelUnregistrar sets the output channel unregistrar (called by the core at startup).
<small>`plugin.go:586`</small>
<small>`plugin.go:685`</small>
### `PluginSDK.SetPluginMgrAPI`
@ -168,7 +168,7 @@ func (s *PluginSDK) SetPluginMgrAPI(pm PluginMgrAPI)
SetPluginMgrAPI sets the plugin manager API (called by the bridge at startup).
<small>`plugin.go:650`</small>
<small>`plugin.go:749`</small>
### `PluginSDK.SetSocialAPI`
@ -179,7 +179,7 @@ SetPluginMgrAPI sets the plugin manager API (called by the bridge at startup).
func (s *PluginSDK) SetSocialAPI(social SocialAPI)
```
<small>`plugin.go:637`</small>
<small>`plugin.go:736`</small>
### `PluginSDK.SetTextMemoryAPI`
@ -190,7 +190,7 @@ func (s *PluginSDK) SetSocialAPI(social SocialAPI)
func (s *PluginSDK) SetTextMemoryAPI(tm TextMemoryAPI)
```
<small>`plugin.go:613`</small>
<small>`plugin.go:712`</small>
### `ToolRegistrar`
@ -200,5 +200,5 @@ type ToolRegistrar func(name string, def ToolDef, handler ToolHandler) error
ToolRegistrar registers a tool dynamically.
<small>`plugin.go:305`</small>
<small>`plugin.go:404`</small>

View File

@ -17,7 +17,7 @@ const PriorityL4
PriorityL4 仅内核级(内置)插件可用;外部插件声明会被夹到 L3。
<small>`plugin.go:127`</small>
<small>`plugin.go:165`</small>
### `PluginSDK.Events`
@ -30,7 +30,7 @@ func (s *PluginSDK) Events() EventSubscriber
Events returns the event subscriber for listening to kernel events (may be nil if not available).
<small>`plugin.go:451`</small>
<small>`plugin.go:550`</small>
### `PluginSDK.SetEventSubscriber`
@ -41,7 +41,7 @@ Events returns the event subscriber for listening to kernel events (may be nil i
func (s *PluginSDK) SetEventSubscriber(es EventSubscriber)
```
<small>`plugin.go:643`</small>
<small>`plugin.go:742`</small>
### `PluginSDK.SetOutputChannelUnregistrar`
@ -54,7 +54,7 @@ func (s *PluginSDK) SetOutputChannelUnregistrar(r OutputChannelUnregistrar)
SetOutputChannelUnregistrar sets the output channel unregistrar (called by the core at startup).
<small>`plugin.go:586`</small>
<small>`plugin.go:685`</small>
### `PluginSDK.UnregisterOutputChannel`
@ -67,7 +67,7 @@ func (s *PluginSDK) UnregisterOutputChannel(name string) error
UnregisterOutputChannel 注销一个输出通道(动态通道随资源生灭时必须调用)。
<small>`plugin.go:544`</small>
<small>`plugin.go:643`</small>
### `EventSubscriber.Subscribe`

View File

@ -33,7 +33,7 @@ for routing the agent's response.
InjectInputMedia(source, channel, text string, blocks []ContentBlock)
```
<small>`plugin.go:230`</small>
<small>`plugin.go:329`</small>
### `IOInjector.InjectInputMediaOpts`
@ -41,7 +41,7 @@ InjectInputMedia(source, channel, text string, blocks []ContentBlock)
InjectInputMediaOpts(source, channel, text string, blocks []ContentBlock, opts InjectOptions)
```
<small>`plugin.go:241`</small>
<small>`plugin.go:340`</small>
### `IOInjector.InjectInputMediaSync`
@ -49,7 +49,7 @@ InjectInputMediaOpts(source, channel, text string, blocks []ContentBlock, opts I
InjectInputMediaSync(source, channel, text string, blocks []ContentBlock) string
```
<small>`plugin.go:231`</small>
<small>`plugin.go:330`</small>
### `IOInjector.InjectInputMediaSyncOpts`
@ -57,7 +57,7 @@ InjectInputMediaSync(source, channel, text string, blocks []ContentBlock) string
InjectInputMediaSyncOpts(source, channel, text string, blocks []ContentBlock, opts InjectOptions) string
```
<small>`plugin.go:242`</small>
<small>`plugin.go:341`</small>
### `IOInjector.InjectInputSync`
@ -75,7 +75,7 @@ InjectInputSync 注入输入事件并同步等待 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>
<small>`plugin.go:325`</small>
### `IOInjector.InjectInputSyncOpts`
@ -83,7 +83,7 @@ InjectInputSync 注入输入事件并同步等待 agent 回复,返回回复文
InjectInputSyncOpts(source, channel, text string, opts InjectOptions) string
```
<small>`plugin.go:240`</small>
<small>`plugin.go:339`</small>
### `IOInjector.InjectInterruptMedia`
@ -91,7 +91,7 @@ InjectInputSyncOpts(source, channel, text string, opts InjectOptions) string
InjectInterruptMedia(source, channel, text string, blocks []ContentBlock)
```
<small>`plugin.go:232`</small>
<small>`plugin.go:331`</small>
### `IOInjector.InjectInterruptMediaOpts`
@ -99,7 +99,7 @@ InjectInterruptMedia(source, channel, text string, blocks []ContentBlock)
InjectInterruptMediaOpts(source, channel, text string, blocks []ContentBlock, opts InjectOptions)
```
<small>`plugin.go:243`</small>
<small>`plugin.go:342`</small>
### `IOInjector.InjectInterruptText`
@ -107,7 +107,7 @@ InjectInterruptMediaOpts(source, channel, text string, blocks []ContentBlock, op
InjectInterruptText(source, channel, text string)
```
<small>`plugin.go:221`</small>
<small>`plugin.go:320`</small>
### `IOInjector.InjectInterruptTextOpts`
@ -124,7 +124,7 @@ InjectInterruptTextOpts(source, channel, text string, opts InjectOptions)
| [`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>
<small>`plugin.go:338`</small>
### `IOInjector.InjectText`
@ -132,7 +132,7 @@ InjectInterruptTextOpts(source, channel, text string, opts InjectOptions)
InjectText(source, channel, text string)
```
<small>`plugin.go:222`</small>
<small>`plugin.go:321`</small>
### `IOInjector.InjectTextNoMemory`
@ -146,7 +146,7 @@ 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>
<small>`plugin.go:322`</small>
### `IOInjector.InjectTextOpts`
@ -159,7 +159,7 @@ InjectTextOpts(source, channel, text string, opts InjectOptions)
上面那些不带 opts 的方法等价于传零值 InjectOptions(记入记忆 + 不裁剪),
保留它们是为了不破坏已有插件;新代码应当用 Opts 变体把意图写清楚。
<small>`plugin.go:238`</small>
<small>`plugin.go:337`</small>
### `IOInjector.SetToolBlocks`
@ -170,7 +170,7 @@ SetToolBlocks(blocks []ContentBlock)
SetToolBlocks 插件工具注入多模态内容块(image_url/audio_url),内核在下一条
tool message 的 content 数组里带上这些块,让模型在后续轮次看到图/听到音频。
<small>`plugin.go:229`</small>
<small>`plugin.go:328`</small>
### `CapAudio`
@ -180,7 +180,7 @@ const CapAudio
Output capability flags
<small>`plugin.go:331`</small>
<small>`plugin.go:430`</small>
### `CapFile`
@ -190,7 +190,7 @@ const CapFile
Output capability flags
<small>`plugin.go:329`</small>
<small>`plugin.go:428`</small>
### `CapImage`
@ -200,7 +200,7 @@ const CapImage
Output capability flags
<small>`plugin.go:330`</small>
<small>`plugin.go:429`</small>
### `CapStructured`
@ -210,7 +210,7 @@ const CapStructured
Output capability flags
<small>`plugin.go:332`</small>
<small>`plugin.go:431`</small>
### `CapText`
@ -220,7 +220,7 @@ const CapText
Output capability flags
<small>`plugin.go:328`</small>
<small>`plugin.go:427`</small>
### `ChannelDef`
@ -233,12 +233,13 @@ NoMemory: 此通道输入/输出不参与记忆计算(向量化/关键词提
Cleaner: 计算层过滤函数,不改原文;仅在向量化/jieba/蒸馏/存档提取关键词时调用
ContextPolicy: 此通道的输入到达后是否据此裁剪上下文,默认 none(不裁剪)
RecallPolicy: 此通道的输入到达后是否据此召回相关记忆,默认 auto(召回)
ScenePolicy: 此通道的输入到达后是否参与场面识别(场景式记忆),默认 auto(参与)
JSON tag 是必需的:通道定义要跨进程传给内核,而 Cleaner 是函数(必须忽略)。
没有 tag 时既无法整体 marshal(func 不支持),又会诱使调用方手写字段白名单——
那样新增字段会被静默丢掉。
<small>`plugin.go:139`</small>
<small>`plugin.go:178`</small>
### `ContextPolicyNone`
@ -280,7 +281,7 @@ blocks 会落进媒体存储被记忆引用捕获,同时作为当前轮 conten
的「下一轮 tool message」语义。
等价于 InjectInputMediaOpts(..., InjectOptions{})。
<small>`plugin.go:706`</small>
<small>`plugin.go:805`</small>
### `PluginSDK.InjectInputMediaOpts`
@ -290,7 +291,7 @@ func (s *PluginSDK) InjectInputMediaOpts(source, channel, text string, blocks []
InjectInputMediaOpts 注入带媒体块的输入,并声明记忆/裁剪行为。
<small>`plugin.go:745`</small>
<small>`plugin.go:844`</small>
### `PluginSDK.InjectInputMediaSync`
@ -301,7 +302,7 @@ func (s *PluginSDK) InjectInputMediaSync(source, channel, text string, blocks []
InjectInputMediaSync 注入带媒体内容块的输入并同步等待 agent 回复。
等价于 InjectInputMediaSyncOpts(..., InjectOptions{})。
<small>`plugin.go:712`</small>
<small>`plugin.go:811`</small>
### `PluginSDK.InjectInputMediaSyncOpts`
@ -311,7 +312,7 @@ func (s *PluginSDK) InjectInputMediaSyncOpts(source, channel, text string, block
InjectInputMediaSyncOpts 注入带媒体块的输入并同步等待回复,同时声明记忆/裁剪行为。
<small>`plugin.go:752`</small>
<small>`plugin.go:851`</small>
### `PluginSDK.InjectInputSync`
@ -330,7 +331,7 @@ 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:697`</small>
<small>`plugin.go:796`</small>
### `PluginSDK.InjectInputSyncOpts`
@ -340,7 +341,7 @@ func (s *PluginSDK) InjectInputSyncOpts(source, channel, text string, opts Injec
InjectInputSyncOpts 注入输入并同步等待回复,同时在这次注入上声明记忆/裁剪行为。
<small>`plugin.go:736`</small>
<small>`plugin.go:835`</small>
### `PluginSDK.InjectInterruptMedia`
@ -351,7 +352,7 @@ func (s *PluginSDK) InjectInterruptMedia(source, channel, text string, blocks []
InjectInterruptMedia 注入带媒体内容块的中断,可抢占当前 LLM 处理。
blocks 随中断消息一起发给模型。
<small>`plugin.go:769`</small>
<small>`plugin.go:868`</small>
### `PluginSDK.InjectInterruptMediaOpts`
@ -361,7 +362,7 @@ func (s *PluginSDK) InjectInterruptMediaOpts(source, channel, text string, block
InjectInterruptMediaOpts 注入带媒体块的中断,并声明记忆/裁剪行为。
<small>`plugin.go:761`</small>
<small>`plugin.go:860`</small>
### `PluginSDK.InjectInterruptText`
@ -372,7 +373,7 @@ func (s *PluginSDK) InjectInterruptText(source, channel, text string)
InjectInterruptText injects a text interrupt that can preempt current LLM processing.
等价于 InjectInterruptTextOpts(..., InjectOptions{}):记入记忆、不裁剪。
<small>`plugin.go:678`</small>
<small>`plugin.go:777`</small>
### `PluginSDK.InjectInterruptTextOpts`
@ -394,7 +395,7 @@ InjectInterruptTextOpts 注入可抢占当前处理的中断文本。
| [`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:729`</small>
<small>`plugin.go:828`</small>
### `InjectOptions`
@ -424,7 +425,7 @@ CleanerName: 此次注入的内容用哪个**已注册的通道 cleaner** 清
而注入内容往往带 ANSI/JSON 包装,需要清洗后才是有效内容;
不指定就只能退到「按 source 查不到就不清洗」。
<small>`plugin.go:98`</small>
<small>`plugin.go:132`</small>
### `PluginSDK.InjectText`
@ -435,7 +436,7 @@ func (s *PluginSDK) InjectText(source, channel, text string)
InjectText injects a text message into the agent pipeline.
等价于 InjectTextOpts(..., InjectOptions{}):记入记忆、不裁剪。
<small>`plugin.go:684`</small>
<small>`plugin.go:783`</small>
### `PluginSDK.InjectTextNoMemory`
@ -452,7 +453,7 @@ InjectTextNoMemory injects a text message without generating memory.
|---|---|---|
| [`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:690`</small>
<small>`plugin.go:789`</small>
### `PluginSDK.InjectTextOpts`
@ -462,7 +463,7 @@ func (s *PluginSDK) InjectTextOpts(source, channel, text string, opts InjectOpti
InjectTextOpts 注入文本到 agent,并在这一次注入上声明记忆与裁剪行为。
<small>`plugin.go:719`</small>
<small>`plugin.go:818`</small>
### `PriorityL1`
@ -476,7 +477,7 @@ L1..L3 任何插件都可声明;**L4 只有内核级插件**(编译期内置
如 cli/webui/timer)才能声明——它用于实现真正的“立即打断”能力,
例如 WebUI 的终止按钮。外部插件(走 proc 桥)声明 L4 会被内核夹到 L3。
<small>`plugin.go:123`</small>
<small>`plugin.go:161`</small>
### `PriorityL2`
@ -490,7 +491,7 @@ L1..L3 任何插件都可声明;**L4 只有内核级插件**(编译期内置
如 cli/webui/timer)才能声明——它用于实现真正的“立即打断”能力,
例如 WebUI 的终止按钮。外部插件(走 proc 桥)声明 L4 会被内核夹到 L3。
<small>`plugin.go:124`</small>
<small>`plugin.go:162`</small>
### `PriorityL3`
@ -504,7 +505,7 @@ L1..L3 任何插件都可声明;**L4 只有内核级插件**(编译期内置
如 cli/webui/timer)才能声明——它用于实现真正的“立即打断”能力,
例如 WebUI 的终止按钮。外部插件(走 proc 桥)声明 L4 会被内核夹到 L3。
<small>`plugin.go:125`</small>
<small>`plugin.go:163`</small>
### `PriorityL4`
@ -517,7 +518,7 @@ const PriorityL4
PriorityL4 仅内核级(内置)插件可用;外部插件声明会被夹到 L3。
<small>`plugin.go:127`</small>
<small>`plugin.go:165`</small>
### `RecallPolicyAuto`
@ -577,7 +578,7 @@ def.Cleaner: 计算层对输入文本清洗后(不改原文)再向量化/
| [`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:566`</small>
<small>`plugin.go:665`</small>
### `PluginSDK.RegisterOutputChannel`
@ -614,7 +615,7 @@ handler: receives args map with keys: payload (string), type (string), meta (str
| [`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:533`</small>
<small>`plugin.go:632`</small>
### `PluginSDK.SetToolBlocks`
@ -625,7 +626,7 @@ func (s *PluginSDK) SetToolBlocks(blocks []ContentBlock)
SetToolBlocks 在工具处理函数内注入多模态内容块,内核在下一条 tool message
的 content 数组里带上它们。需要「本轮就让模型看到」时用 InjectInputMedia。
<small>`plugin.go:777`</small>
<small>`plugin.go:876`</small>
### `ValidContextPolicy`

View File

@ -22,7 +22,7 @@ controls which events are delivered.
Subscribe(eventType EventType, handler EventHandler) func()
```
<small>`plugin.go:279`</small>
<small>`plugin.go:378`</small>
### `Event`
@ -32,7 +32,7 @@ type Event struct { Type EventType `json:"type"` Source string `json:"source"` P
Event represents a system event published by the kernel.
<small>`plugin.go:265`</small>
<small>`plugin.go:364`</small>
### `EventHandler`
@ -42,7 +42,7 @@ type EventHandler func(evt *Event)
EventHandler processes a system event.
<small>`plugin.go:273`</small>
<small>`plugin.go:372`</small>
### `EventType`
@ -52,7 +52,7 @@ type EventType string
EventType identifies the kind of system event.
<small>`plugin.go:247`</small>
<small>`plugin.go:346`</small>
### `PluginSDK.Events`
@ -65,5 +65,5 @@ func (s *PluginSDK) Events() EventSubscriber
Events returns the event subscriber for listening to kernel events (may be nil if not available).
<small>`plugin.go:451`</small>
<small>`plugin.go:550`</small>

View File

@ -57,7 +57,7 @@ IsPluginDisabled(name string) bool
IsPluginDisabled 查询插件是否被禁用。
<small>`plugin.go:290`</small>
<small>`plugin.go:389`</small>
### `PluginMgrAPI.ListLoadedPlugins`
@ -67,7 +67,7 @@ ListLoadedPlugins() []string
ListLoadedPlugins 列出已加载插件。
<small>`plugin.go:288`</small>
<small>`plugin.go:387`</small>
### `PluginMgrAPI.ReloadOne`
@ -77,7 +77,7 @@ ReloadOne(name string) error
ReloadOne 重载单个插件(停止后重新加载)。
<small>`plugin.go:286`</small>
<small>`plugin.go:385`</small>
### `PluginSDK.AutoRestart`
@ -87,7 +87,7 @@ func (s *PluginSDK) AutoRestart() bool
AutoRestart 返回插件是否允许自动重启。
<small>`plugin.go:796`</small>
<small>`plugin.go:895`</small>
### `PluginSDK.PluginMgr`
@ -98,7 +98,7 @@ 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:658`</small>
<small>`plugin.go:757`</small>
### `PluginSDK.PluginName`
@ -108,7 +108,7 @@ func (s *PluginSDK) PluginName() string
PluginName returns the name of the plugin.
<small>`plugin.go:402`</small>
<small>`plugin.go:501`</small>
### `PluginSDK.RegisterOnRemoveHandler`
@ -129,7 +129,7 @@ RegisterOnRemoveHandler 注册插件被删除(卸载)时的清理回调。
| [`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:831`</small>
<small>`plugin.go:930`</small>
### `PluginSDK.RegisterPluginAPI`
@ -139,7 +139,7 @@ func (s *PluginSDK) RegisterPluginAPI(name string) error
RegisterPluginAPI registers this plugin's API for access by other plugins.
<small>`plugin.go:507`</small>
<small>`plugin.go:606`</small>
### `PluginSDK.RegisterStopHandler`
@ -159,7 +159,7 @@ RegisterStopHandler 注册插件停止阶段的清理回调。
| [`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:806`</small>
<small>`plugin.go:905`</small>
### `PluginSDK.RunOnRemoveHandlers`
@ -170,7 +170,7 @@ func (s *PluginSDK) RunOnRemoveHandlers()
RunOnRemoveHandlers 执行全部已注册的 onRemove handler(后注册先执行,执行后清空,幂等)。
由内核在卸载插件(registry.RemovePlugin)时、插件 Stop() 之后执行。
<small>`plugin.go:842`</small>
<small>`plugin.go:941`</small>
### `PluginSDK.RunStopHandlers`
@ -181,7 +181,7 @@ func (s *PluginSDK) RunStopHandlers()
RunStopHandlers 执行全部已注册的 stop handler(后注册先执行,执行后清空,幂等)。
由内核(内置插件)或插件桥接层(外部插件 z_bridge 的 StopPlugin)在调用插件 Stop() 前执行。
<small>`plugin.go:817`</small>
<small>`plugin.go:916`</small>
### `PluginSDK.SetAutoRestart`
@ -205,5 +205,5 @@ SetAutoRestart 设置插件崩溃后内核是否自动重启它。
| [`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:789`</small>
<small>`plugin.go:888`</small>

View File

@ -46,5 +46,5 @@ func (s *PluginSDK) LLM() LLMAPI
LLM returns the LLM provider API (may be nil if not available).
<small>`plugin.go:437`</small>
<small>`plugin.go:536`</small>

View File

@ -236,7 +236,7 @@ func (s *PluginSDK) DocMemory() DocMemoryAPI
DocMemory returns the document memory API (may be nil if not available).
<small>`plugin.go:423`</small>
<small>`plugin.go:522`</small>
### `Entity`
@ -251,7 +251,7 @@ Entity represents a named entity in the knowledge graph.
### `Knowledge`
```go
type Knowledge struct { Name string `json:"name"` Content string `json:"content"` }
type Knowledge struct { Name string `json:"name"` // Category 是该条目的父分类路径(如 "tech/go"),根下条目为空。 // // 为何加这个字段:对<EFBC9A><E5AFB9> …
```
Knowledge represents a knowledge entry.
@ -278,7 +278,7 @@ 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:430`</small>
<small>`plugin.go:529`</small>
### `MediaAttachment`
@ -308,7 +308,7 @@ func (s *PluginSDK) Memory() MemoryAPI
Memory returns the graph memory API (may be nil if not available).
<small>`plugin.go:409`</small>
<small>`plugin.go:508`</small>
### `PersonProfile`
@ -338,7 +338,7 @@ func (s *PluginSDK) Social() SocialAPI
Social returns the social graph API (may be nil if not available).
<small>`plugin.go:444`</small>
<small>`plugin.go:543`</small>
### `SocialRelation`
@ -366,7 +366,7 @@ func (s *PluginSDK) TextMemory() TextMemoryAPI
TextMemory returns the text memory API (may be nil if not available).
<small>`plugin.go:416`</small>
<small>`plugin.go:515`</small>
### `Triple`

View File

@ -79,7 +79,7 @@ controls which events are delivered.
Subscribe(eventType EventType, handler EventHandler) func()
```
<small>`plugin.go:279`</small>
<small>`plugin.go:378`</small>
## `IOInjector`
@ -110,7 +110,7 @@ for routing the agent's response.
InjectInputMedia(source, channel, text string, blocks []ContentBlock)
```
<small>`plugin.go:230`</small>
<small>`plugin.go:329`</small>
### `IOInjector.InjectInputMediaOpts`
@ -118,7 +118,7 @@ InjectInputMedia(source, channel, text string, blocks []ContentBlock)
InjectInputMediaOpts(source, channel, text string, blocks []ContentBlock, opts InjectOptions)
```
<small>`plugin.go:241`</small>
<small>`plugin.go:340`</small>
### `IOInjector.InjectInputMediaSync`
@ -126,7 +126,7 @@ InjectInputMediaOpts(source, channel, text string, blocks []ContentBlock, opts I
InjectInputMediaSync(source, channel, text string, blocks []ContentBlock) string
```
<small>`plugin.go:231`</small>
<small>`plugin.go:330`</small>
### `IOInjector.InjectInputMediaSyncOpts`
@ -134,7 +134,7 @@ InjectInputMediaSync(source, channel, text string, blocks []ContentBlock) string
InjectInputMediaSyncOpts(source, channel, text string, blocks []ContentBlock, opts InjectOptions) string
```
<small>`plugin.go:242`</small>
<small>`plugin.go:341`</small>
### `IOInjector.InjectInputSync`
@ -152,7 +152,7 @@ InjectInputSync 注入输入事件并同步等待 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>
<small>`plugin.go:325`</small>
### `IOInjector.InjectInputSyncOpts`
@ -160,7 +160,7 @@ InjectInputSync 注入输入事件并同步等待 agent 回复,返回回复文
InjectInputSyncOpts(source, channel, text string, opts InjectOptions) string
```
<small>`plugin.go:240`</small>
<small>`plugin.go:339`</small>
### `IOInjector.InjectInterruptMedia`
@ -168,7 +168,7 @@ InjectInputSyncOpts(source, channel, text string, opts InjectOptions) string
InjectInterruptMedia(source, channel, text string, blocks []ContentBlock)
```
<small>`plugin.go:232`</small>
<small>`plugin.go:331`</small>
### `IOInjector.InjectInterruptMediaOpts`
@ -176,7 +176,7 @@ InjectInterruptMedia(source, channel, text string, blocks []ContentBlock)
InjectInterruptMediaOpts(source, channel, text string, blocks []ContentBlock, opts InjectOptions)
```
<small>`plugin.go:243`</small>
<small>`plugin.go:342`</small>
### `IOInjector.InjectInterruptText`
@ -184,7 +184,7 @@ InjectInterruptMediaOpts(source, channel, text string, blocks []ContentBlock, op
InjectInterruptText(source, channel, text string)
```
<small>`plugin.go:221`</small>
<small>`plugin.go:320`</small>
### `IOInjector.InjectInterruptTextOpts`
@ -201,7 +201,7 @@ InjectInterruptTextOpts(source, channel, text string, opts InjectOptions)
| [`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>
<small>`plugin.go:338`</small>
### `IOInjector.InjectText`
@ -209,7 +209,7 @@ InjectInterruptTextOpts(source, channel, text string, opts InjectOptions)
InjectText(source, channel, text string)
```
<small>`plugin.go:222`</small>
<small>`plugin.go:321`</small>
### `IOInjector.InjectTextNoMemory`
@ -223,7 +223,7 @@ 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>
<small>`plugin.go:322`</small>
### `IOInjector.InjectTextOpts`
@ -236,7 +236,7 @@ InjectTextOpts(source, channel, text string, opts InjectOptions)
上面那些不带 opts 的方法等价于传零值 InjectOptions(记入记忆 + 不裁剪),
保留它们是为了不破坏已有插件;新代码应当用 Opts 变体把意图写清楚。
<small>`plugin.go:238`</small>
<small>`plugin.go:337`</small>
### `IOInjector.SetToolBlocks`
@ -247,7 +247,7 @@ SetToolBlocks(blocks []ContentBlock)
SetToolBlocks 插件工具注入多模态内容块(image_url/audio_url),内核在下一条
tool message 的 content 数组里带上这些块,让模型在后续轮次看到图/听到音频。
<small>`plugin.go:229`</small>
<small>`plugin.go:328`</small>
## `KnowledgeAPI`
@ -422,7 +422,7 @@ IsPluginDisabled(name string) bool
IsPluginDisabled 查询插件是否被禁用。
<small>`plugin.go:290`</small>
<small>`plugin.go:389`</small>
### `PluginMgrAPI.ListLoadedPlugins`
@ -432,7 +432,7 @@ ListLoadedPlugins() []string
ListLoadedPlugins 列出已加载插件。
<small>`plugin.go:288`</small>
<small>`plugin.go:387`</small>
### `PluginMgrAPI.ReloadOne`
@ -442,7 +442,7 @@ ReloadOne(name string) error
ReloadOne 重载单个插件(停止后重新加载)。
<small>`plugin.go:286`</small>
<small>`plugin.go:385`</small>
## `SettingsAPI`
@ -680,7 +680,7 @@ Append(evt TextEvent) error
type AudioURL struct { URL string `json:"url"` }
```
<small>`plugin.go:867`</small>
<small>`plugin.go:966`</small>
### `EffectiveProxyAuth`
@ -692,13 +692,32 @@ EffectiveProxyAuth 返回生效的鉴权模式(空串归一化为 ProxyAuthHom
<small>`proxy.go:169`</small>
### `ToolError.Error`
```go
func (e *ToolError) Error() string
```
Error 实现 error,便于工具同时走 (ToolError, error) 通道。
**示例插件里的真实用法**
| 插件 | 位置 | 代码 |
|---|---|---|
| [`a2a`](../examples/index.md#a2a) | `example/a2a/plugin.go:317` | `http.Error(w, "query/message.text required", http.StatusBadRequest)` |
| [`acp`](../examples/index.md#acp) | `example/acp/plugin.go:175` | `http.Error(w, "", http.StatusMethodNotAllowed)` |
| [`ai_image`](../examples/index.md#ai_image) | `example/ai_image/plugin.go:259` | `return map[string]interface{}{"isError": true, "content": "Request failed: " + err.Error()}, nil` |
| [`browser`](../examples/index.md#browser) | `example/browser/plugin_test.go:15` | `if err == nil \|\| !strings.Contains(err.Error(), "timeout is required") {` |
<small>`plugin.go:264`</small>
### `ImageURL`
```go
type ImageURL struct { URL string `json:"url"` Detail string `json:"detail,omitempty"` }
```
<small>`plugin.go:862`</small>
<small>`plugin.go:961`</small>
### `MemItem`
@ -708,7 +727,7 @@ type MemItem struct { Role string `json:"role"` Content string `json:"content"`
MemItem represents a memory item in stage context.
<small>`plugin.go:179`</small>
<small>`plugin.go:220`</small>
### `NormalizeProxyHost`
@ -732,7 +751,7 @@ type PluginSDK struct { name string regTool ToolRegistrar regStage StageRegistra
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>
<small>`plugin.go:436`</small>
### `ProxyAuthHomeAgent`
@ -877,6 +896,64 @@ SDKVersion 是对外暴露的 SDK 版本号。
<small>`plugin.go:10`</small>
### `ScenePolicyAuto`
```go
const ScenePolicyAuto
```
场面策略:决定一次输入是否参与**场面识别**(场景式记忆)。
与前两项再正交一轴:NoMemory 管「进不进记忆计算」、ContextPolicy 管
「裁不裁上下文」、RecallPolicy 管「召不召回记忆」,本项管的是
「这条输入算不算一场戏的一部分」——它决定输入会不会产出现场指纹
(通道/对话对象/工具/话题/时段),进而决定会不会长出、命中、写入场景。
默认(空串或 ScenePolicyAuto)**参与**,保持既有行为:场景式记忆自
v1.3 落地起就对所有通道无条件生效,没有开关。不默认关有两个原因:
1. 场景只**附加**现有记忆的检索路,不改记忆本体,默认关会让存量
通道突然失去场景召回;
2. 「关」是少数意图(内部信噪通道),少数意图不该是默认——
与 ContextPolicy 刻意相反(同为破坏性操作,那里是默认关)。
该关的典型是纯内部通道:system(内核自循环)、kernel、timer、healthcheck。
但**现网不标任何一个**(2026-09-26 裁定):实测这些 0-refs 通道合计 70
strength、0 条记忆,场景召回返回空;而 declared 场景不进相似度空间
(loadEmergentScenesLocked 只取 origin='emergent'),多写对聚类零影响。
「多写无影响、少写会缺场景」——默认 auto 保持开,声明项只作为插件
将来确实需要时的闸门。
<small>`plugin.go:98`</small>
### `ScenePolicyNone`
```go
const ScenePolicyNone
```
场面策略:决定一次输入是否参与**场面识别**(场景式记忆)。
与前两项再正交一轴:NoMemory 管「进不进记忆计算」、ContextPolicy 管
「裁不裁上下文」、RecallPolicy 管「召不召回记忆」,本项管的是
「这条输入算不算一场戏的一部分」——它决定输入会不会产出现场指纹
(通道/对话对象/工具/话题/时段),进而决定会不会长出、命中、写入场景。
默认(空串或 ScenePolicyAuto)**参与**,保持既有行为:场景式记忆自
v1.3 落地起就对所有通道无条件生效,没有开关。不默认关有两个原因:
1. 场景只**附加**现有记忆的检索路,不改记忆本体,默认关会让存量
通道突然失去场景召回;
2. 「关」是少数意图(内部信噪通道),少数意图不该是默认——
与 ContextPolicy 刻意相反(同为破坏性操作,那里是默认关)。
该关的典型是纯内部通道:system(内核自循环)、kernel、timer、healthcheck。
但**现网不标任何一个**(2026-09-26 裁定):实测这些 0-refs 通道合计 70
strength、0 条记忆,场景召回返回空;而 declared 场景不进相似度空间
(loadEmergentScenesLocked 只取 origin='emergent'),多写对聚类零影响。
「多写无影响、少写会缺场景」——默认 auto 保持开,声明项只作为插件
将来确实需要时的闸门。
<small>`plugin.go:99`</small>
### `PluginSDK.SetProxyRegistrar`
```go
@ -887,6 +964,24 @@ SetProxyRegistrar 由内核注入。插件不直接调它(与 SetInputChannelR
<small>`proxy.go:309`</small>
### `ToolError`
```go
type ToolError struct { // Field 是出错的参数字段名(参数校验失败时填)。 Field string `json:"field,omitempty"` // Reason 是机器可读的原因码: …
```
ToolError 描述一次工具调用的失败原因。
存在的理由:失败若只表达为文本,模型无法定位到字段,只能原样重试
(实测 cmd_run 失败率 34%~48%,全部源于同一个成因:参数被截断或
JSON 写坏,工具却只回报 "command is required" 这类与真因无关的错)。
⚠️ 零值语义:插件**不必**改用本类型。内核的失败识别同时兼容既有三种约定
({"error":…}、{"isError":true,…}、显式 error 返回),见 core.isToolError。
本类型是给**新写**的工具用的可选项,不是迁移要求。
<small>`plugin.go:252`</small>
### `PluginSDK.UnregisterOutputChannel`
!!! warning "仅内核内置插件可用"
@ -898,7 +993,7 @@ func (s *PluginSDK) UnregisterOutputChannel(name string) error
UnregisterOutputChannel 注销一个输出通道(动态通道随资源生灭时必须调用)。
<small>`plugin.go:544`</small>
<small>`plugin.go:643`</small>
### `ValidProxyAuth`
@ -923,6 +1018,16 @@ ValidProxyHostLabel 校验子域名标签是否合法(DNS label 规则)。
<small>`proxy.go:180`</small>
### `ValidScenePolicy`
```go
func ValidScenePolicy(policy string) bool
```
ValidScenePolicy 校验场面策略取值;空串等价于 ScenePolicyAuto。
<small>`plugin.go:103`</small>
### `ValidateProxyDef`
```go

View File

@ -193,5 +193,5 @@ sett 在 New 时一次性写入且无 setter,故不需要加锁。
| [`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:406`</small>
<small>`plugin.go:505`</small>

View File

@ -10,7 +10,7 @@
func (c *StageContext) IsResponded() bool
```
<small>`plugin.go:172`</small>
<small>`plugin.go:213`</small>
### `StageContext.Lock`
@ -27,7 +27,7 @@ func (c *StageContext) 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>
<small>`plugin.go:211`</small>
### `StageContext.RLock`
@ -44,7 +44,7 @@ func (c *StageContext) 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>
<small>`plugin.go:209`</small>
### `StageContext.RUnlock`
@ -61,7 +61,7 @@ func (c *StageContext) 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>
<small>`plugin.go:210`</small>
### `PluginSDK.RegisterStage`
@ -83,7 +83,7 @@ RegisterStage registers a handler for a pipeline stage.
| [`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:472`</small>
<small>`plugin.go:571`</small>
### `Stage`
@ -103,7 +103,7 @@ type StageContext struct { mu sync.RWMutex RawMessage string UserID string Group
StageContext provides context for stage handlers.
<small>`plugin.go:148`</small>
<small>`plugin.go:189`</small>
### `StageHandler`
@ -123,7 +123,7 @@ type StageRegistrar func(stage Stage, handler StageHandler)
StageRegistrar registers a stage handler.
<small>`plugin.go:308`</small>
<small>`plugin.go:407`</small>
### `StageScope`
@ -133,7 +133,7 @@ type StageScope int
StageScope controls which events a stage handler receives.
<small>`plugin.go:294`</small>
<small>`plugin.go:393`</small>
### `StageContext.Unlock`
@ -150,5 +150,5 @@ func (c *StageContext) 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>
<small>`plugin.go:212`</small>

View File

@ -14,7 +14,7 @@ ContentBlock 是多模态内容块(OpenAI 格式:text/image_url/audio_url)
插件工具返回结果时可用 PluginSDK.SetToolBlocks 注入,让下一轮 LLM
请求在 tool message 的 content 数组里带上图片/音频,实现"模型看图/听音频"。
<small>`plugin.go:855`</small>
<small>`plugin.go:954`</small>
### `PluginSDK.RegisterTool`
@ -33,7 +33,7 @@ RegisterTool registers a tool that the LLM can call.
| [`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:458`</small>
<small>`plugin.go:557`</small>
### `ToolCall`
@ -43,7 +43,7 @@ type ToolCall struct { ID string `json:"id"` Name string `json:"name"` Plugin st
ToolCall represents a model's request to call a tool.
<small>`plugin.go:186`</small>
<small>`plugin.go:227`</small>
### `ToolDef`
@ -53,7 +53,7 @@ type ToolDef struct { Name string `json:"name"` Plugin string `json:"plugin,omit
ToolDef describes a tool that the plugin exposes.
<small>`plugin.go:203`</small>
<small>`plugin.go:279`</small>
### `ToolHandler`
@ -73,5 +73,5 @@ type ToolResult struct { CallID string `json:"call_id"` Name string `json:"name"
ToolResult represents the result of a tool call.
<small>`plugin.go:194`</small>
<small>`plugin.go:235`</small>

File diff suppressed because it is too large Load Diff

View File

@ -8,15 +8,15 @@ SDK 仓 `example/` 下有多个**真实可编译**的示例插件,覆盖工具
## `a2a`
用到的 API:`InjectInputSync` · `Lock` · `RegisterInputChannel` · `RegisterOutputChannel` · `RegisterTool` · `SetAutoRestart` · `Settings` · `Unlock`
用到的 API:`Error` · `InjectInputSync` · `Lock` · `RegisterInputChannel` · `RegisterOutputChannel` · `RegisterTool` · `SetAutoRestart` · `Settings` · `Unlock`
## `acp`
用到的 API:`InjectInputSync` · `Lock` · `RLock` · `RUnlock` · `RegisterInputChannel` · `RegisterOutputChannel` · `RegisterTool` · `SetAutoRestart` · `Settings` · `Unlock`
用到的 API:`Error` · `InjectInputSync` · `Lock` · `RLock` · `RUnlock` · `RegisterInputChannel` · `RegisterOutputChannel` · `RegisterTool` · `SetAutoRestart` · `Settings` · `Unlock`
## `ai_image`
用到的 API:`RegisterTool` · `SetAutoRestart` · `Settings`
用到的 API:`Error` · `RegisterTool` · `SetAutoRestart` · `Settings`
## `bili`
@ -24,7 +24,7 @@ SDK 仓 `example/` 下有多个**真实可编译**的示例插件,覆盖工具
## `browser`
用到的 API:`InjectInterruptTextOpts` · `InjectTextNoMemory` · `Lock` · `RegisterInputChannel` · `Unlock`
用到的 API:`Error` · `InjectInterruptTextOpts` · `InjectTextNoMemory` · `Lock` · `RegisterInputChannel` · `Unlock`
## `calendar`

113
docs/guide/scene-memory.md Normal file
View File

@ -0,0 +1,113 @@
# 场景记忆(Scene Memory)
> 场景式记忆是内核 v1.3 起的能力。它不新增 API 面,只影响**你的输入被怎样记住与取回**。
> 与 `NoMemory` / `ContextPolicy` / `RecallPolicy` 并列为第四项声明:`ScenePolicy`。
## 它解决什么问题
三层记忆按**字面相关性**召回:你得说出相近的词,记忆才会被取回来。
场景记忆补上另一半:按**场合**召回。
同一场合再次出现时,当时挂在这个场合上的约定、偏好、人物关系会自动回来——
与这次说了什么措辞无关。
```
你:以后在群里回消息简短点
└─ 这条记忆挂到场面「chan:qq + peer:group_xxx」上
一周后,同一个群里有人问「上次说的格式是什么」
└─ 场面重现(还没等你提到「格式」),那条约定已经被取回
```
## 场面是自己长出来的
场景**不需要声明**。每轮交互,内核采集一组可观察信号当这轮<E8BF99><E8BDAE><EFBFBD>「场面指纹」:
| 特征 | 来源 | 权重 | 说明 |
|---|---|---|---|
| `chan` | 输入通道名 | 1.0 | 最强的同一性信号 |
| `peer` / `peer_group` | 注入点给的 `payload` 里的 `group_id`/`user_id`/`chat_id` 等 | 1.0 | 群与私聊分开,避免互相命中 |
| `tool` | 触发这一步的工具名 | 0.8 | 行为信号 |
| `topic` | 清洗后输入的内容词 | 0.4 | 软信号,同场面的不同话题不该被拆开 |
| `part` | 时段(夜间/上午/下午/晚间) | 0.2 | 最弱,只做辅助 |
指纹反复重合时,一场场面就成形了。相似度按**加权 Jaccard** 算
(共享特征的权重和 ÷ 并集的权重和)——不加权的话,一次偶然的话题重合
会把两个不同场面并成一个。
**同类场面出现第二次才被认定。** 一次性的交互不建场面:
那不是「场面」,建了只会让图库被一次性事件撑满。
## 声明你的参与姿态
```go
sdk.ChannelDef{
ScenePolicy: sdk.ScenePolicyNone, // 这条通道不参与场面识别
}
```
或单次注入覆盖:
```go
sdk.InjectOptions{
ScenePolicy: sdk.ScenePolicyNone,
}
```
| 取值 | 含义 |
|---|---|
| `""`(空)/ `ScenePolicyAuto` | **参与**(默认,保持既有行为) |
| `ScenePolicyNone` | **不参与**:不产任何场面指纹,也不派生场景键 |
**默认是参与而不是不参与**,与 `ContextPolicy` 刻意相反。原因是场景只
**附加**检索路径、不改记忆本体,默认关会让存量通道突然失去场景召回;
而「关」是少数意图(纯内部信号)。
声明 `none` 之后连时段特征都不产——一个不参与的门面不该在场面索引里
留下任何足迹。
### 谁该考虑关掉
内核自循环(`system`)、心跳(`timer`)、内部状态汇报(`kernel`)这类
纯内部信号。它们每次触发都在撑一个场面,会把不相干的交互聚到一起。
反过来说,**多标一个通道通常没有代价**:一个没人往上面写记忆的场面,
召回时返回空。关不关都不影响正确性——所以拿不准时,默认参与就好。
## 怎么给场面命名
场景键有两种来源:
**通道派生(默认)**——`evt.Source` 派生出 `chan:qq` 这类键。你不用管。
**显式声明(进阶)**——在注入时给出更有语义的键:
```go
p.sdk.InjectInterruptTextOpts("qq", "qq", text, sdk.InjectOptions{
ScenePolicy: sdk.ScenePolicyAuto,
})
```
也可以通过 `payload["scene"]` 传层级键(支持 `string` / `[]string` /
`[]interface{}` 三种形态):
```go
"chan:qq/peer:group_1027"
```
召回走**前缀匹配**(`chan:qq` 能覆盖 `chan:qq/peer:xxx`),用 `/` 兜底
以免 `chan:qq` 误吞 `chan:qq2` 这种同前缀但不同层的场景。
## 场面记忆不改变什么
- **不改记忆本体**:场景是记忆的**附加索引**,删掉场景不删记忆。
- **不让模型负责**:`memory_commit` 的 `scene` 留空即可,内核会挂到本轮
解析出的场面上。留空是安全的一侧——猜错的场面会把无关记忆钉死。
- **不影响同步通道**:`webui` / `cli` / 终端走 `ResponseCh`,不经
`output_send__*`,与场面无关。
## 相关 API
- `ChannelDef.ScenePolicy` —— 通道级声明(见 [输入/输出通道](../api/channels.md))
- `InjectOptions.ScenePolicy` —— 单次注入覆盖(见 [其他类型](../api/misc.md))
- `ScenePolicyAuto` / `ScenePolicyNone` / `ValidScenePolicy` —— 常量与校验

View File

@ -29,5 +29,6 @@
- [环境与工具链](https://sdk.homeagent.jianfgit.xyz/guide/getting-started.md): hmapdev 是 SDK 仓提供的统一插件开发工具链,Go 与 Lua 两种插件都用它,
- [多平台构建](https://sdk.homeagent.jianfgit.xyz/guide/multi-platform.md): hmapdev build 默认 bundle 模式,一次产出含三个平台的单个
- [打包与发布](https://sdk.homeagent.jianfgit.xyz/guide/packaging.md): hmapdev build 一次完成编译与打包,产出
- [场景记忆(Scene Memory)](https://sdk.homeagent.jianfgit.xyz/guide/scene-memory.md): > 场景式记忆是内核 v1
- [受限 SDK 与安全](https://sdk.homeagent.jianfgit.xyz/guide/security.md): 外部插件与内置插件的区别不只是「能不能调某个函数」,还包含一层安全边界:
- [版本与兼容](https://sdk.homeagent.jianfgit.xyz/versions.md): SDK 版本跟随内核的中版本,patch 位恒为