mirror of
https://gitcode.com/JianFeeeee/homeagent-sdk.git
synced 2026-09-29 22:05:25 +00:00
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:
@ -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>
|
||||
|
||||
|
||||
@ -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`
|
||||
|
||||
|
||||
@ -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`
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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`
|
||||
|
||||
|
||||
151
docs/api/misc.md
151
docs/api/misc.md
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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
@ -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
113
docs/guide/scene-memory.md
Normal 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` —— 常量与校验
|
||||
@ -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 位恒为
|
||||
|
||||
@ -120,6 +120,7 @@ nav:
|
||||
- 仅内置插件可用: api/builtin-only.md
|
||||
- 指南:
|
||||
- 能力边界(哪些 API 外部可用): guide/capability-boundary.md
|
||||
- 场景记忆(按场合召回): guide/scene-memory.md
|
||||
- 打包与发布: guide/packaging.md
|
||||
- 多平台构建: guide/multi-platform.md
|
||||
- 受限 SDK 与安全: guide/security.md
|
||||
|
||||
@ -1,116 +0,0 @@
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
project(ha_remotedevice VERSION 0.1.0 LANGUAGES C)
|
||||
|
||||
# ============================================================
|
||||
# ha_remotedevice — HomeAgent 远程设备接入 C SDK
|
||||
# 零外部依赖,纯 C 实现,兼容嵌入式平台。
|
||||
#
|
||||
# 使用方式:
|
||||
# add_subdirectory(path/to/ha_remotedevice)
|
||||
# target_link_libraries(my_app ha_remotedevice)
|
||||
# target_include_directories(my_app PRIVATE
|
||||
# ${HA_REMOTEDEVICE_INCLUDE_DIR})
|
||||
# ============================================================
|
||||
|
||||
# 选项: 构建为静态库或动态库
|
||||
option(BUILD_SHARED_LIBS "Build ha_remotedevice as shared library" OFF)
|
||||
|
||||
# 选项: 禁用 malloc/free(用于裸机环境,用户需提供 alloc 回调)
|
||||
option(HA_NO_ALLOC "Disable dynamic memory allocation" OFF)
|
||||
|
||||
# 选项: 日志级别
|
||||
set(HA_LOG_LEVEL 2 CACHE STRING "Log level: 0=none, 1=error, 2=info, 3=debug")
|
||||
|
||||
# 源文件
|
||||
set(HA_REMOTEDEVICE_SRC
|
||||
src/ha_remotedevice.c
|
||||
src/ha_json.c
|
||||
src/ha_ws.c
|
||||
)
|
||||
|
||||
# 头文件
|
||||
set(HA_REMOTEDEVICE_INCLUDE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||
)
|
||||
|
||||
# 编译选项
|
||||
if(HA_NO_ALLOC)
|
||||
add_definitions(-DHA_NO_ALLOC)
|
||||
endif()
|
||||
add_definitions(-DHA_LOG_LEVEL=${HA_LOG_LEVEL})
|
||||
|
||||
# 创建库
|
||||
if(BUILD_SHARED_LIBS)
|
||||
add_library(ha_remotedevice SHARED ${HA_REMOTEDEVICE_SRC})
|
||||
if(WIN32)
|
||||
# Windows 需要导出符号
|
||||
set_target_properties(ha_remotedevice PROPERTIES
|
||||
WINDOWS_EXPORT_ALL_SYMBOLS ON)
|
||||
endif()
|
||||
else()
|
||||
add_library(ha_remotedevice STATIC ${HA_REMOTEDEVICE_SRC})
|
||||
endif()
|
||||
|
||||
# 包含目录
|
||||
target_include_directories(ha_remotedevice
|
||||
PUBLIC ${HA_REMOTEDEVICE_INCLUDE}
|
||||
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src
|
||||
)
|
||||
|
||||
# 不链接任何外部库
|
||||
target_link_libraries(ha_remotedevice PRIVATE)
|
||||
|
||||
# 导出包含目录供外部项目使用
|
||||
set(HA_REMOTEDEVICE_INCLUDE_DIR
|
||||
${HA_REMOTEDEVICE_INCLUDE}
|
||||
CACHE INTERNAL "ha_remotedevice include directories")
|
||||
|
||||
# 安装规则
|
||||
install(TARGETS ha_remotedevice
|
||||
EXPORT ha_remotedevice-targets
|
||||
LIBRARY DESTINATION lib
|
||||
ARCHIVE DESTINATION lib
|
||||
RUNTIME DESTINATION bin
|
||||
INCLUDES DESTINATION include
|
||||
)
|
||||
|
||||
install(DIRECTORY include/
|
||||
DESTINATION include
|
||||
)
|
||||
|
||||
install(EXPORT ha_remotedevice-targets
|
||||
DESTINATION lib/cmake/ha_remotedevice
|
||||
NAMESPACE ha_remotedevice::
|
||||
)
|
||||
|
||||
# ============================================================
|
||||
# 测试(可选)
|
||||
# ============================================================
|
||||
option(BUILD_TESTS "Build ha_remotedevice tests" OFF)
|
||||
|
||||
if(BUILD_TESTS)
|
||||
find_package(Threads REQUIRED)
|
||||
|
||||
add_executable(ha_remotedevice_test
|
||||
test/test_ha_remotedevice.c
|
||||
)
|
||||
target_link_libraries(ha_remotedevice_test
|
||||
PRIVATE ha_remotedevice Threads::Threads
|
||||
)
|
||||
target_include_directories(ha_remotedevice_test
|
||||
PRIVATE ${HA_REMOTEDEVICE_INCLUDE_DIR}
|
||||
)
|
||||
|
||||
# 添加测试
|
||||
add_test(NAME ha_remotedevice_test
|
||||
COMMAND ha_remotedevice_test
|
||||
)
|
||||
endif()
|
||||
|
||||
# ============================================================
|
||||
# 编译信息
|
||||
# ============================================================
|
||||
message(STATUS "ha_remotedevice ${PROJECT_VERSION}")
|
||||
message(STATUS " Build type: $<CONFIG>")
|
||||
message(STATUS " Shared lib: ${BUILD_SHARED_LIBS}")
|
||||
message(STATUS " No alloc: ${HA_NO_ALLOC}")
|
||||
@ -1,216 +0,0 @@
|
||||
#ifndef HA_REMOTEDEVICE_H
|
||||
#define HA_REMOTEDEVICE_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ==================================================================
|
||||
* ha_remotedevice — 远程设备接入 C SDK
|
||||
*
|
||||
* 零外部依赖,纯 C 实现,兼容嵌入式平台。
|
||||
* 传输层由用户实现(4 个函数指针),SDK 处理所有协议细节。
|
||||
*
|
||||
* 声明式设计:
|
||||
* 设备在代码中声明自己是什么(kind)和能做什么(caps),
|
||||
* 声明支持哪些命令(shell/camerasue/screensee/...)并注册对应处理函数,
|
||||
* SDK 自动处理协议握手、心跳、消息路由、结果回执。
|
||||
*
|
||||
* 协议流程:
|
||||
* TCP 连接 → WS 升级 → hello(设备声明) → bind(令牌) → 就绪
|
||||
* 就绪后循环:读帧 → 按 handlers 表分发命令 → 自动回执结果
|
||||
* ================================================================== */
|
||||
|
||||
/* ======================== 状态码 ======================== */
|
||||
typedef enum {
|
||||
HA_OK = 0,
|
||||
HA_ERR_GENERIC = -1,
|
||||
HA_ERR_NOMEM = -2,
|
||||
HA_ERR_INVALID = -3,
|
||||
HA_ERR_TIMEOUT = -4,
|
||||
HA_ERR_DISCONNECTED = -5,
|
||||
HA_ERR_PROTOCOL = -6,
|
||||
HA_ERR_TRANSPORT = -7,
|
||||
HA_ERR_NOT_FOUND = -8,
|
||||
} ha_status_t;
|
||||
|
||||
/* ======================== 传输层抽象 ========================
|
||||
*
|
||||
* 用户必须实现这 4 个函数,适配不同平台(FreeRTOS+lwIP、Zephyr、裸机等)。
|
||||
*
|
||||
* connect(ctx, host, port) → 建立 TCP 连接,返回 0 成功
|
||||
* send(ctx, data, len) → 发送 len 字节,返回实际发送字节数,-1 失败
|
||||
* recv(ctx, buf, len) → 接收最多 len 字节,返回实际接收字节数,0 断开,-1 失败
|
||||
* close(ctx) → 关闭连接
|
||||
*/
|
||||
typedef struct {
|
||||
int (*connect)(void *ctx, const char *host, uint16_t port);
|
||||
int (*send)(void *ctx, const uint8_t *data, int len);
|
||||
int (*recv)(void *ctx, uint8_t *buf, int len);
|
||||
void (*close)(void *ctx);
|
||||
void *ctx;
|
||||
} ha_transport_t;
|
||||
|
||||
/* ======================== 设备声明 ========================
|
||||
*
|
||||
* 声明式配置:设备在代码中声明自己的类型和能力。
|
||||
* 这些信息通过 hello 消息发送给网关。
|
||||
*
|
||||
* device_id — 唯一标识,如 "esp32-cam-1"
|
||||
* name — 设备显示名,如 "门口摄像头"
|
||||
* kind — 设备种类,如 "camera"、"computer"、"speaker"、"light"
|
||||
* caps — 能力数组,以 NULL 结尾,如 {"camera","status",NULL}
|
||||
* info_json — 额外信息(JSON 字符串),可选,如 '{"chip":"ESP32-S3","psram":8}'
|
||||
*/
|
||||
typedef struct {
|
||||
const char *device_id;
|
||||
const char *name;
|
||||
const char *kind;
|
||||
const char **caps; /* NULL 结尾 */
|
||||
const char *info_json; /* 可选,NULL 或 JSON 字符串 */
|
||||
} ha_device_info_t;
|
||||
|
||||
/* ======================== 命令结果 ========================
|
||||
*
|
||||
* 命令处理函数通过填写此结构体返回数据。
|
||||
* SDK 收到结果后自动发送回执(文本或二进制分块)。
|
||||
*
|
||||
* 使用方式:
|
||||
* 1. 简单文本:设置 status=0, output="结果文本"
|
||||
* 2. 二进制数据:设置 has_binary=1, binary_data/binary_len/mime
|
||||
* 3. 错误:设置 status=1, error="错误信息"
|
||||
*
|
||||
* 注意:output 字符串由 SDK 内部 strdup 后发送,handler 返回后即可释放。
|
||||
* 我们约定 handler 不负责分配,由 SDK 在内部做好拷贝。
|
||||
* 所以 handler 可以返回栈上或静态字符串。
|
||||
*/
|
||||
typedef struct {
|
||||
int status; /* 0=ok, 非0=error */
|
||||
const char *output; /* 输出文本(如 base64 图像数据),SDK 内部拷贝 */
|
||||
const char *error; /* 错误信息 */
|
||||
int has_binary; /* 1=通过二进制分块回传 */
|
||||
const char *binary_mime; /* 二进制 MIME 类型 */
|
||||
const uint8_t *binary_data; /* 二进制数据指针 */
|
||||
int binary_len; /* 二进制数据长度 */
|
||||
} ha_cmd_result_t;
|
||||
|
||||
/* ======================== 命令处理声明 ========================
|
||||
*
|
||||
* 声明式命令注册:设备在配置中声明支持哪些命令,并绑定处理函数。
|
||||
*
|
||||
* command 值说明:
|
||||
* - "shell" → 处理 shell 类型命令,args 为完整命令字符串
|
||||
* - "camerasue" → 处理 homeagent-camerasue 命令,args 为参数
|
||||
* - "screensee" → 处理 homeagent-screensee 命令
|
||||
* - "speakeruse" → 处理 homeagent-speakeruse 命令
|
||||
* - "computeruse" → 处理 homeagent-computeruse 命令
|
||||
* - "clipboardsee" → 处理 homeagent-clipboardsee 命令
|
||||
* - "clipboardsue" → 处理 homeagent-clipboardsue 命令
|
||||
* - "screensue" → 处理 homeagent-screensue 命令
|
||||
* - "deviceinfo" → 处理设备信息查询
|
||||
* - 其他自定义命令名 → 按字符串匹配分发
|
||||
*
|
||||
* handler 处理完毕后只需填写 result 结构体,SDK 自动回执。
|
||||
*/
|
||||
typedef ha_status_t (*ha_cmd_handler_t)(const char *req_id, const char *args,
|
||||
ha_cmd_result_t *result, void *userdata);
|
||||
|
||||
typedef struct {
|
||||
const char *command; /* 命令名,如 "camerasue"、"shell" */
|
||||
ha_cmd_handler_t handler; /* 处理函数 */
|
||||
} ha_cmd_handler_def_t;
|
||||
|
||||
/* 二进制数据接收回调:收到服务端推送的二进制数据(如 TTS 音频)时调用。
|
||||
* data 指针在回调返回后失效,如需保存请拷贝。 */
|
||||
typedef void (*ha_binary_handler_t)(const char *req_id, const char *kind,
|
||||
const char *mime, const uint8_t *data,
|
||||
int len, void *userdata);
|
||||
|
||||
/* 连接状态变化回调 */
|
||||
typedef void (*ha_state_callback_t)(int connected, void *userdata);
|
||||
|
||||
/* ======================== 客户端配置 ========================
|
||||
*
|
||||
* 所有配置在 ha_client_new() 时一次性声明。
|
||||
* 声明式核心:handlers 表声明了设备支持的所有命令及其处理函数。
|
||||
*/
|
||||
typedef struct {
|
||||
ha_transport_t transport; /* 传输层实现(必须) */
|
||||
ha_device_info_t device; /* 设备声明(必须) */
|
||||
const char *server; /* 服务端地址,如 "192.168.1.100:9890"(必须) */
|
||||
const char *token; /* 接入令牌(必须) */
|
||||
|
||||
ha_cmd_handler_def_t *handlers; /* 声明式命令处理表,.command=NULL 标记结束 */
|
||||
ha_binary_handler_t on_binary; /* 二进制数据接收回调(可选) */
|
||||
ha_state_callback_t on_state; /* 状态变化回调(可选) */
|
||||
void *userdata; /* 用户自定义数据,传给所有回调 */
|
||||
|
||||
int ping_interval; /* 心跳间隔秒数,0 则默认 30 */
|
||||
int max_reconnect; /* 最大重连次数,-1 无限重连(默认),0 不重连 */
|
||||
} ha_config_t;
|
||||
|
||||
/* ======================== 客户端 API ======================== */
|
||||
|
||||
typedef struct ha_client ha_client_t;
|
||||
|
||||
/* 创建客户端实例。config 数据会在内部拷贝,外部可释放。 */
|
||||
ha_client_t *ha_client_new(const ha_config_t *config);
|
||||
|
||||
/* 启动连接:TCP 连接 → WS 升级 → hello → bind → 就绪。阻塞直到完成或失败。 */
|
||||
ha_status_t ha_client_start(ha_client_t *client);
|
||||
|
||||
/* 主循环处理:必须在用户的主循环中周期性调用。
|
||||
* - 读取 WS 帧并分发
|
||||
* - 按 handlers 表查找命令处理函数,自动回执结果
|
||||
* - 处理心跳 ping/pong
|
||||
* - 处理断线重连
|
||||
* 返回 HA_OK 表示正常,HA_ERR_DISCONNECTED 表示正在重连。 */
|
||||
ha_status_t ha_client_process(ha_client_t *client);
|
||||
|
||||
/* ===== 主动上报(设备主动推送,非命令响应) ===== */
|
||||
|
||||
/* 发送设备主动上报事件。type 如 "motion_detected",detail 为 JSON 字符串。 */
|
||||
void ha_client_send_event(ha_client_t *client, const char *type,
|
||||
const char *detail);
|
||||
|
||||
/* 发送设备状态更新。status: "online"、"offline"、"busy" 等。 */
|
||||
void ha_client_send_status(ha_client_t *client, const char *status);
|
||||
|
||||
/* ===== 生命周期 ===== */
|
||||
|
||||
/* 停止客户端,断开连接。 */
|
||||
void ha_client_stop(ha_client_t *client);
|
||||
|
||||
/* 销毁客户端,释放所有资源。 */
|
||||
void ha_client_destroy(ha_client_t *client);
|
||||
|
||||
/* ======================== 工具函数 ======================== */
|
||||
|
||||
/* 解析 homeagent-* 命令,返回能力名和参数。
|
||||
* command = "camerasue 5" → cap="camerasue", args="5"
|
||||
* command = "screensee" → cap="screensee", args=""
|
||||
* command = "computeruse {...}" → cap="computeruse", args="..." */
|
||||
void ha_cmd_parse_homeagent(const char *command, const char **cap,
|
||||
const char **args);
|
||||
|
||||
/* 解析 JSON 格式的命令参数,提取 action 和 JSON 字符串。
|
||||
* command = "computeruse {\"action\":\"click\",\"x\":100}"
|
||||
* → action="computeruse", json_str="{\"action\":\"click\",...}" */
|
||||
void ha_cmd_parse_json(const char *command, const char **action,
|
||||
const char **json_str);
|
||||
|
||||
/* Base64 编码(用于将二进制数据编码为文本回传)。
|
||||
* 返回写入 out 的字节数(不含 \0),out 不足时返回所需长度。 */
|
||||
int ha_base64_encode(const uint8_t *data, int len, char *out, int out_len);
|
||||
|
||||
/* 获取版本号 */
|
||||
const char *ha_version(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* HA_REMOTEDEVICE_H */
|
||||
@ -1,369 +0,0 @@
|
||||
#include "ha_json.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
#include <stdio.h>
|
||||
|
||||
/* ======================== 解析器 ======================== */
|
||||
|
||||
/* 前向声明 */
|
||||
static ha_json_node_t *parse_value(const char **pp);
|
||||
|
||||
/* 跳过空白 */
|
||||
static const char *skip_ws(const char *p) {
|
||||
while (*p && (unsigned char)*p <= ' ') p++;
|
||||
return p;
|
||||
}
|
||||
|
||||
/* 解析字符串("..."),返回新分配的字符串,p 更新到结束引号后 */
|
||||
static char *parse_string(const char **pp) {
|
||||
const char *p = skip_ws(*pp);
|
||||
if (*p != '"') return NULL;
|
||||
p++;
|
||||
int len = 0;
|
||||
const char *q = p;
|
||||
while (*q && *q != '"') {
|
||||
if (*q == '\\') { q++; if (*q) q++; }
|
||||
else q++;
|
||||
len++;
|
||||
}
|
||||
if (*q != '"') return NULL;
|
||||
char *s = (char *)malloc(len + 1);
|
||||
if (!s) return NULL;
|
||||
q = p;
|
||||
int i = 0;
|
||||
while (*q && *q != '"') {
|
||||
if (*q == '\\') {
|
||||
q++;
|
||||
switch (*q) {
|
||||
case '"': s[i++] = '"'; break;
|
||||
case '\\': s[i++] = '\\'; break;
|
||||
case '/': s[i++] = '/'; break;
|
||||
case 'b': s[i++] = '\b'; break;
|
||||
case 'f': s[i++] = '\f'; break;
|
||||
case 'n': s[i++] = '\n'; break;
|
||||
case 'r': s[i++] = '\r'; break;
|
||||
case 't': s[i++] = '\t'; break;
|
||||
case 'u': q += 4; s[i++] = '?'; continue;
|
||||
default: s[i++] = *q; break;
|
||||
}
|
||||
q++;
|
||||
} else {
|
||||
s[i++] = *q++;
|
||||
}
|
||||
}
|
||||
s[i] = '\0';
|
||||
*pp = q + 1;
|
||||
return s;
|
||||
}
|
||||
|
||||
static ha_json_node_t *new_node(ha_json_type_t type) {
|
||||
ha_json_node_t *n = (ha_json_node_t *)calloc(1, sizeof(ha_json_node_t));
|
||||
if (n) n->type = type;
|
||||
return n;
|
||||
}
|
||||
|
||||
/* 解析数字 */
|
||||
static ha_json_node_t *parse_number(const char **pp) {
|
||||
const char *p = *pp;
|
||||
int neg = 0;
|
||||
if (*p == '-') { neg = 1; p++; }
|
||||
if (!isdigit((unsigned char)*p)) return NULL;
|
||||
int val = 0;
|
||||
while (isdigit((unsigned char)*p)) {
|
||||
val = val * 10 + (*p - '0');
|
||||
p++;
|
||||
}
|
||||
if (*p == '.') { p++; while (isdigit((unsigned char)*p)) p++; }
|
||||
if (*p == 'e' || *p == 'E') {
|
||||
p++;
|
||||
if (*p == '+' || *p == '-') p++;
|
||||
while (isdigit((unsigned char)*p)) p++;
|
||||
}
|
||||
*pp = p;
|
||||
ha_json_node_t *n = new_node(HA_JSON_INT);
|
||||
if (n) n->int_val = neg ? -val : val;
|
||||
return n;
|
||||
}
|
||||
|
||||
/* 解析 true/false/null */
|
||||
static ha_json_node_t *parse_keyword(const char **pp) {
|
||||
const char *p = *pp;
|
||||
ha_json_node_t *n = NULL;
|
||||
if (strncmp(p, "true", 4) == 0 && !isalnum((unsigned char)p[4])) {
|
||||
n = new_node(HA_JSON_BOOL); if (n) n->bool_val = 1;
|
||||
*pp = p + 4;
|
||||
} else if (strncmp(p, "false", 5) == 0 && !isalnum((unsigned char)p[5])) {
|
||||
n = new_node(HA_JSON_BOOL); if (n) n->bool_val = 0;
|
||||
*pp = p + 5;
|
||||
} else if (strncmp(p, "null", 4) == 0 && !isalnum((unsigned char)p[4])) {
|
||||
n = new_node(HA_JSON_NULL);
|
||||
*pp = p + 4;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
/* 解析对象 */
|
||||
static ha_json_node_t *parse_object(const char **pp) {
|
||||
const char *p = skip_ws(*pp);
|
||||
if (*p != '{') return NULL;
|
||||
p++;
|
||||
ha_json_node_t *obj = new_node(HA_JSON_OBJECT);
|
||||
if (!obj) return NULL;
|
||||
ha_json_node_t **tail = &obj->child;
|
||||
p = skip_ws(p);
|
||||
if (*p == '}') { *pp = p + 1; return obj; }
|
||||
while (*p) {
|
||||
p = skip_ws(p);
|
||||
char *key = parse_string(&p);
|
||||
if (!key) break;
|
||||
p = skip_ws(p);
|
||||
if (*p != ':') { free(key); break; }
|
||||
p++;
|
||||
ha_json_node_t *val = parse_value(&p);
|
||||
if (!val) { free(key); break; }
|
||||
val->key = key;
|
||||
*tail = val;
|
||||
tail = &val->next;
|
||||
p = skip_ws(p);
|
||||
if (*p == ',') { p++; continue; }
|
||||
if (*p == '}') break;
|
||||
}
|
||||
p = skip_ws(p);
|
||||
if (*p == '}') { *pp = p + 1; return obj; }
|
||||
ha_json_free(obj);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* 解析数组 */
|
||||
static ha_json_node_t *parse_array(const char **pp) {
|
||||
const char *p = skip_ws(*pp);
|
||||
if (*p != '[') return NULL;
|
||||
p++;
|
||||
ha_json_node_t *arr = new_node(HA_JSON_ARRAY);
|
||||
if (!arr) return NULL;
|
||||
ha_json_node_t **tail = &arr->child;
|
||||
p = skip_ws(p);
|
||||
if (*p == ']') { *pp = p + 1; return arr; }
|
||||
while (*p) {
|
||||
ha_json_node_t *val = parse_value(&p);
|
||||
if (!val) break;
|
||||
*tail = val;
|
||||
tail = &val->next;
|
||||
p = skip_ws(p);
|
||||
if (*p == ',') { p++; continue; }
|
||||
if (*p == ']') break;
|
||||
}
|
||||
p = skip_ws(p);
|
||||
if (*p == ']') { *pp = p + 1; return arr; }
|
||||
ha_json_free(arr);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* 解析值(主入口) */
|
||||
static ha_json_node_t *parse_value(const char **pp) {
|
||||
const char *p = skip_ws(*pp);
|
||||
if (*p == '{') return parse_object(pp);
|
||||
if (*p == '[') return parse_array(pp);
|
||||
if (*p == '"') {
|
||||
char *s = parse_string(pp);
|
||||
if (!s) return NULL;
|
||||
ha_json_node_t *n = new_node(HA_JSON_STRING);
|
||||
if (!n) { free(s); return NULL; }
|
||||
n->str_val = s;
|
||||
return n;
|
||||
}
|
||||
if (*p == '-' || isdigit((unsigned char)*p)) return parse_number(pp);
|
||||
return parse_keyword(pp);
|
||||
}
|
||||
|
||||
/* ======================== 公共 API ======================== */
|
||||
|
||||
ha_json_node_t *ha_json_parse(const char *str) {
|
||||
if (!str) return NULL;
|
||||
const char *p = str;
|
||||
return parse_value(&p);
|
||||
}
|
||||
|
||||
const char *ha_json_get_string(const ha_json_node_t *obj, const char *key) {
|
||||
ha_json_node_t *n = ha_json_get(obj, key);
|
||||
if (!n || n->type != HA_JSON_STRING) return NULL;
|
||||
return n->str_val;
|
||||
}
|
||||
|
||||
int ha_json_get_int(const ha_json_node_t *obj, const char *key, int def) {
|
||||
ha_json_node_t *n = ha_json_get(obj, key);
|
||||
if (!n || n->type != HA_JSON_INT) return def;
|
||||
return n->int_val;
|
||||
}
|
||||
|
||||
ha_json_node_t *ha_json_get(const ha_json_node_t *obj, const char *key) {
|
||||
if (!obj || obj->type != HA_JSON_OBJECT) return NULL;
|
||||
ha_json_node_t *c = obj->child;
|
||||
while (c) {
|
||||
if (c->key && strcmp(c->key, key) == 0) return c;
|
||||
c = c->next;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int ha_json_array_len(const ha_json_node_t *arr) {
|
||||
if (!arr || arr->type != HA_JSON_ARRAY) return 0;
|
||||
int n = 0;
|
||||
ha_json_node_t *c = arr->child;
|
||||
while (c) { n++; c = c->next; }
|
||||
return n;
|
||||
}
|
||||
|
||||
ha_json_node_t *ha_json_array_get(const ha_json_node_t *arr, int index) {
|
||||
if (!arr || arr->type != HA_JSON_ARRAY) return NULL;
|
||||
ha_json_node_t *c = arr->child;
|
||||
int i = 0;
|
||||
while (c) {
|
||||
if (i == index) return c;
|
||||
i++; c = c->next;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void ha_json_free(ha_json_node_t *root) {
|
||||
if (!root) return;
|
||||
ha_json_node_t *c = root->child;
|
||||
while (c) {
|
||||
ha_json_node_t *next = c->next;
|
||||
free(c->key);
|
||||
if (c->type == HA_JSON_STRING) free(c->str_val);
|
||||
ha_json_free(c);
|
||||
c = next;
|
||||
}
|
||||
free(root);
|
||||
}
|
||||
|
||||
/* ======================== 构建器 ======================== */
|
||||
|
||||
static void json_escape(ha_json_builder_t *jb, const char *s) {
|
||||
if (!s) { ha_json_builder_raw(jb, "null"); return; }
|
||||
ha_json_builder_raw(jb, "\"");
|
||||
for (const char *p = s; *p; p++) {
|
||||
unsigned char c = (unsigned char)*p;
|
||||
switch (c) {
|
||||
case '"': ha_json_builder_raw(jb, "\\\""); break;
|
||||
case '\\': ha_json_builder_raw(jb, "\\\\"); break;
|
||||
case '\b': ha_json_builder_raw(jb, "\\b"); break;
|
||||
case '\f': ha_json_builder_raw(jb, "\\f"); break;
|
||||
case '\n': ha_json_builder_raw(jb, "\\n"); break;
|
||||
case '\r': ha_json_builder_raw(jb, "\\r"); break;
|
||||
case '\t': ha_json_builder_raw(jb, "\\t"); break;
|
||||
default:
|
||||
if (c < 0x20) {
|
||||
char buf[8];
|
||||
snprintf(buf, sizeof(buf), "\\u%04x", c);
|
||||
ha_json_builder_raw(jb, buf);
|
||||
} else {
|
||||
char buf[2] = { (char)c, 0 };
|
||||
ha_json_builder_raw(jb, buf);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
ha_json_builder_raw(jb, "\"");
|
||||
}
|
||||
|
||||
void ha_json_builder_init(ha_json_builder_t *jb, char *buf, int cap) {
|
||||
jb->buf = buf;
|
||||
jb->len = 0;
|
||||
jb->cap = cap;
|
||||
jb->depth = 0;
|
||||
if (cap > 0) buf[0] = '\0';
|
||||
}
|
||||
|
||||
void ha_json_builder_reset(ha_json_builder_t *jb) {
|
||||
jb->len = 0;
|
||||
jb->depth = 0;
|
||||
if (jb->cap > 0) jb->buf[0] = '\0';
|
||||
}
|
||||
|
||||
void ha_json_builder_raw(ha_json_builder_t *jb, const char *s) {
|
||||
while (*s && jb->len < jb->cap - 1) {
|
||||
jb->buf[jb->len++] = *s++;
|
||||
}
|
||||
jb->buf[jb->len] = '\0';
|
||||
}
|
||||
|
||||
void ha_json_builder_comma(ha_json_builder_t *jb) {
|
||||
if (jb->depth > 0 && jb->item_count[jb->depth - 1] > 0) {
|
||||
ha_json_builder_raw(jb, ",");
|
||||
}
|
||||
if (jb->depth > 0) jb->item_count[jb->depth - 1]++;
|
||||
}
|
||||
|
||||
void ha_json_builder_begin_object(ha_json_builder_t *jb) {
|
||||
ha_json_builder_comma(jb);
|
||||
ha_json_builder_raw(jb, "{");
|
||||
if (jb->depth < 16) jb->item_count[jb->depth] = 0;
|
||||
jb->depth++;
|
||||
}
|
||||
|
||||
void ha_json_builder_end_object(ha_json_builder_t *jb) {
|
||||
jb->depth--;
|
||||
ha_json_builder_raw(jb, "}");
|
||||
}
|
||||
|
||||
void ha_json_builder_begin_array(ha_json_builder_t *jb) {
|
||||
ha_json_builder_comma(jb);
|
||||
ha_json_builder_raw(jb, "[");
|
||||
if (jb->depth < 16) jb->item_count[jb->depth] = 0;
|
||||
jb->depth++;
|
||||
}
|
||||
|
||||
void ha_json_builder_end_array(ha_json_builder_t *jb) {
|
||||
jb->depth--;
|
||||
ha_json_builder_raw(jb, "]");
|
||||
}
|
||||
|
||||
void ha_json_builder_key(ha_json_builder_t *jb, const char *key) {
|
||||
ha_json_builder_comma(jb);
|
||||
json_escape(jb, key);
|
||||
ha_json_builder_raw(jb, ":");
|
||||
}
|
||||
|
||||
void ha_json_builder_add_string(ha_json_builder_t *jb, const char *val) {
|
||||
json_escape(jb, val);
|
||||
}
|
||||
|
||||
void ha_json_builder_add_int(ha_json_builder_t *jb, int val) {
|
||||
char buf[16];
|
||||
snprintf(buf, sizeof(buf), "%d", val);
|
||||
ha_json_builder_raw(jb, buf);
|
||||
}
|
||||
|
||||
void ha_json_builder_add_bool(ha_json_builder_t *jb, int val) {
|
||||
ha_json_builder_raw(jb, val ? "true" : "false");
|
||||
}
|
||||
|
||||
void ha_json_builder_add_null(ha_json_builder_t *jb) {
|
||||
ha_json_builder_raw(jb, "null");
|
||||
}
|
||||
|
||||
void ha_json_builder_string(ha_json_builder_t *jb, const char *key, const char *val) {
|
||||
ha_json_builder_key(jb, key);
|
||||
json_escape(jb, val);
|
||||
}
|
||||
|
||||
void ha_json_builder_int(ha_json_builder_t *jb, const char *key, int val) {
|
||||
ha_json_builder_key(jb, key);
|
||||
ha_json_builder_add_int(jb, val);
|
||||
}
|
||||
|
||||
void ha_json_builder_bool(ha_json_builder_t *jb, const char *key, int val) {
|
||||
ha_json_builder_key(jb, key);
|
||||
ha_json_builder_add_bool(jb, val);
|
||||
}
|
||||
|
||||
const char *ha_json_builder_str(ha_json_builder_t *jb) {
|
||||
return jb->buf;
|
||||
}
|
||||
|
||||
int ha_json_builder_len(ha_json_builder_t *jb) {
|
||||
return jb->len;
|
||||
}
|
||||
@ -1,107 +0,0 @@
|
||||
#ifndef HA_JSON_H
|
||||
#define HA_JSON_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ======================== JSON 解析器(DOM 风格) ======================== */
|
||||
typedef enum {
|
||||
HA_JSON_NULL,
|
||||
HA_JSON_BOOL,
|
||||
HA_JSON_INT,
|
||||
HA_JSON_STRING,
|
||||
HA_JSON_ARRAY,
|
||||
HA_JSON_OBJECT,
|
||||
} ha_json_type_t;
|
||||
|
||||
typedef struct ha_json_node {
|
||||
ha_json_type_t type;
|
||||
union {
|
||||
int bool_val;
|
||||
int int_val;
|
||||
char *str_val;
|
||||
};
|
||||
struct ha_json_node *next; /* linked list for array/object items */
|
||||
struct ha_json_node *child; /* first child for array/object */
|
||||
char *key; /* key for object members */
|
||||
} ha_json_node_t;
|
||||
|
||||
/* 解析 JSON 字符串,返回根节点。失败返回 NULL。 */
|
||||
ha_json_node_t *ha_json_parse(const char *str);
|
||||
|
||||
/* 从对象中按 key 获取字符串值,不存在返回 NULL */
|
||||
const char *ha_json_get_string(const ha_json_node_t *obj, const char *key);
|
||||
|
||||
/* 从对象中按 key 获取 int 值,不存在返回 def */
|
||||
int ha_json_get_int(const ha_json_node_t *obj, const char *key, int def);
|
||||
|
||||
/* 从对象中按 key 获取子节点,不存在返回 NULL */
|
||||
ha_json_node_t *ha_json_get(const ha_json_node_t *obj, const char *key);
|
||||
|
||||
/* 获取数组长度 */
|
||||
int ha_json_array_len(const ha_json_node_t *arr);
|
||||
|
||||
/* 获取数组第 index 个元素,越界返回 NULL */
|
||||
ha_json_node_t *ha_json_array_get(const ha_json_node_t *arr, int index);
|
||||
|
||||
/* 释放整个 JSON 树 */
|
||||
void ha_json_free(ha_json_node_t *root);
|
||||
|
||||
/* ======================== JSON 构建器(直接写缓冲区) ======================== */
|
||||
typedef struct {
|
||||
char *buf;
|
||||
int len;
|
||||
int cap;
|
||||
int depth;
|
||||
int item_count[16]; /* 每层已添加元素数,用于逗号判断 */
|
||||
} ha_json_builder_t;
|
||||
|
||||
/* 初始化构建器 */
|
||||
void ha_json_builder_init(ha_json_builder_t *jb, char *buf, int cap);
|
||||
|
||||
/* 清空构建器 */
|
||||
void ha_json_builder_reset(ha_json_builder_t *jb);
|
||||
|
||||
/* 基础写入 */
|
||||
void ha_json_builder_raw(ha_json_builder_t *jb, const char *s);
|
||||
|
||||
/* 逗号(自动判断是否需要加) */
|
||||
void ha_json_builder_comma(ha_json_builder_t *jb);
|
||||
|
||||
/* 对象 */
|
||||
void ha_json_builder_begin_object(ha_json_builder_t *jb);
|
||||
void ha_json_builder_end_object(ha_json_builder_t *jb);
|
||||
|
||||
/* 数组 */
|
||||
void ha_json_builder_begin_array(ha_json_builder_t *jb);
|
||||
void ha_json_builder_end_array(ha_json_builder_t *jb);
|
||||
|
||||
/* 键名 */
|
||||
void ha_json_builder_key(ha_json_builder_t *jb, const char *key);
|
||||
|
||||
/* 值 */
|
||||
void ha_json_builder_add_string(ha_json_builder_t *jb, const char *val);
|
||||
void ha_json_builder_add_int(ha_json_builder_t *jb, int val);
|
||||
void ha_json_builder_add_bool(ha_json_builder_t *jb, int val);
|
||||
void ha_json_builder_add_null(ha_json_builder_t *jb);
|
||||
|
||||
/* 快捷方法:直接写 "key":"val" */
|
||||
void ha_json_builder_string(ha_json_builder_t *jb, const char *key, const char *val);
|
||||
void ha_json_builder_int(ha_json_builder_t *jb, const char *key, int val);
|
||||
void ha_json_builder_bool(ha_json_builder_t *jb, const char *key, int val);
|
||||
|
||||
/* 获取当前构建的字符串指针 */
|
||||
const char *ha_json_builder_str(ha_json_builder_t *jb);
|
||||
|
||||
/* 获取当前长度 */
|
||||
int ha_json_builder_len(ha_json_builder_t *jb);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* HA_JSON_H */
|
||||
@ -1,628 +0,0 @@
|
||||
#include "ha_remotedevice.h"
|
||||
#include "ha_json.h"
|
||||
#include "ha_ws.h"
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#define HA_VERSION "0.1.0"
|
||||
|
||||
/* 前向声明(因 handle_cmd_msg 需要调用这些函数,而它们定义在后面) */
|
||||
void ha_client_send_result(ha_client_t *client, const char *req_id,
|
||||
const char *status, const char *output,
|
||||
const char *error);
|
||||
void ha_client_send_data_chunked(ha_client_t *client, const char *req_id,
|
||||
const char *kind, const char *mime,
|
||||
const uint8_t *data, int len);
|
||||
|
||||
/* ======================== 内部状态 ======================== */
|
||||
typedef enum {
|
||||
HA_STATE_INIT,
|
||||
HA_STATE_DISCONNECTED,
|
||||
HA_STATE_CONNECTING,
|
||||
HA_STATE_WS_UPGRADING,
|
||||
HA_STATE_HELLO_SENT,
|
||||
HA_STATE_BIND_SENT,
|
||||
HA_STATE_READY,
|
||||
HA_STATE_STOPPING,
|
||||
} ha_state_t;
|
||||
|
||||
/* 语音数据聚合缓冲区 */
|
||||
typedef struct {
|
||||
char req_id[128];
|
||||
char kind[64];
|
||||
char mime[64];
|
||||
int total;
|
||||
uint8_t *data;
|
||||
int len;
|
||||
int cap;
|
||||
} ha_speech_accum_t;
|
||||
|
||||
struct ha_client {
|
||||
ha_config_t config; /* 拷贝的配置 */
|
||||
ha_state_t state;
|
||||
int reconnect_cnt; /* 当前重连次数 */
|
||||
ha_ws_t ws; /* WS 连接 */
|
||||
|
||||
/* JSON 构建缓冲区 */
|
||||
char json_buf[4096];
|
||||
ha_json_builder_t jb;
|
||||
|
||||
/* 语音数据聚合 */
|
||||
ha_speech_accum_t speech;
|
||||
};
|
||||
|
||||
/* ======================== 辅助函数 ======================== */
|
||||
|
||||
static void set_sockbuf(ha_client_t *c, int i) { (void)c; (void)i; }
|
||||
|
||||
/* Base64 编码表 */
|
||||
static const char b64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
|
||||
int ha_base64_encode(const uint8_t *data, int len, char *out, int out_len) {
|
||||
int needed = ((len + 2) / 3) * 4 + 1;
|
||||
if (out_len < needed) {
|
||||
if (out_len > 0) out[0] = '\0';
|
||||
return needed;
|
||||
}
|
||||
int i = 0, j = 0;
|
||||
while (i < len) {
|
||||
int rem = len - i;
|
||||
uint8_t b0 = data[i++];
|
||||
uint8_t b1 = (rem > 1) ? data[i++] : 0;
|
||||
uint8_t b2 = (rem > 2) ? data[i++] : 0;
|
||||
out[j++] = b64[b0 >> 2];
|
||||
out[j++] = b64[((b0 & 0x03) << 4) | (b1 >> 4)];
|
||||
out[j++] = (rem > 1) ? b64[((b1 & 0x0F) << 2) | (b2 >> 6)] : '=';
|
||||
out[j++] = (rem > 2) ? b64[b2 & 0x3F] : '=';
|
||||
}
|
||||
out[j] = '\0';
|
||||
return j;
|
||||
}
|
||||
|
||||
/* ======================== JSON 构建辅助 ======================== */
|
||||
static void json_init(ha_client_t *c) {
|
||||
ha_json_builder_init(&c->jb, c->json_buf, sizeof(c->json_buf));
|
||||
}
|
||||
|
||||
/* ======================== WS 发送 JSON ======================== */
|
||||
static int ws_send_json(ha_client_t *c) {
|
||||
return ha_ws_send_text(&c->ws, c->json_buf);
|
||||
}
|
||||
|
||||
/* ======================== 协议消息构造 ======================== */
|
||||
|
||||
/* 构建 hello 消息 */
|
||||
static int send_hello(ha_client_t *c) {
|
||||
json_init(c);
|
||||
ha_json_builder_begin_object(&c->jb);
|
||||
ha_json_builder_string(&c->jb, "op", "hello");
|
||||
ha_json_builder_key(&c->jb, "device");
|
||||
ha_json_builder_begin_object(&c->jb);
|
||||
ha_json_builder_string(&c->jb, "device_id", c->config.device.device_id);
|
||||
ha_json_builder_string(&c->jb, "name", c->config.device.name);
|
||||
ha_json_builder_string(&c->jb, "kind", c->config.device.kind);
|
||||
/* caps */
|
||||
ha_json_builder_key(&c->jb, "caps");
|
||||
ha_json_builder_begin_array(&c->jb);
|
||||
if (c->config.device.caps) {
|
||||
for (const char **p = c->config.device.caps; *p; p++) {
|
||||
ha_json_builder_add_string(&c->jb, *p);
|
||||
}
|
||||
}
|
||||
ha_json_builder_end_array(&c->jb);
|
||||
/* info 可选 */
|
||||
if (c->config.device.info_json && c->config.device.info_json[0]) {
|
||||
ha_json_builder_string(&c->jb, "info", c->config.device.info_json);
|
||||
}
|
||||
ha_json_builder_end_object(&c->jb); /* device */
|
||||
ha_json_builder_end_object(&c->jb); /* root */
|
||||
return ws_send_json(c);
|
||||
}
|
||||
|
||||
/* 构建 bind 消息 */
|
||||
static int send_bind(ha_client_t *c) {
|
||||
json_init(c);
|
||||
ha_json_builder_begin_object(&c->jb);
|
||||
ha_json_builder_string(&c->jb, "op", "bind");
|
||||
ha_json_builder_string(&c->jb, "device_id", c->config.device.device_id);
|
||||
ha_json_builder_string(&c->jb, "token", c->config.token);
|
||||
ha_json_builder_end_object(&c->jb);
|
||||
return ws_send_json(c);
|
||||
}
|
||||
|
||||
/* ======================== 消息处理 ======================== */
|
||||
|
||||
/* 在 handlers 表中查找命令处理函数 */
|
||||
static ha_cmd_handler_def_t *find_handler(ha_client_t *c, const char *name) {
|
||||
if (!name || !c->config.handlers) return NULL;
|
||||
for (ha_cmd_handler_def_t *h = c->config.handlers; h->command; h++) {
|
||||
if (strcmp(h->command, name) == 0) return h;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* 声明式命令分发:查找 handlers 表 → 调用 handler → 自动回执 */
|
||||
static void handle_cmd_msg(ha_client_t *c, ha_json_node_t *msg) {
|
||||
const char *req_id = ha_json_get_string(msg, "req_id");
|
||||
const char *command = ha_json_get_string(msg, "command");
|
||||
const char *cmd_type = ha_json_get_string(msg, "cmd_type");
|
||||
if (!req_id || !command) return;
|
||||
if (!cmd_type) cmd_type = "homeagent";
|
||||
|
||||
const char *handler_name = NULL;
|
||||
const char *args = command;
|
||||
|
||||
if (strcmp(cmd_type, "shell") == 0) {
|
||||
handler_name = "shell";
|
||||
/* args 保持为完整命令字符串 */
|
||||
} else {
|
||||
/* homeagent-* 命令:提取能力名作为 handler 名 */
|
||||
const char *cap = command;
|
||||
const char *p = command;
|
||||
if (strncmp(p, "homeagent-", 10) == 0) p += 10;
|
||||
const char *space = strchr(p, ' ');
|
||||
if (space) {
|
||||
args = space + 1;
|
||||
/* handler_name 用静态缓冲区 */
|
||||
static char name_buf[128];
|
||||
int n = (int)(space - p);
|
||||
if (n > 127) n = 127;
|
||||
strncpy(name_buf, p, n);
|
||||
name_buf[n] = '\0';
|
||||
handler_name = name_buf;
|
||||
} else {
|
||||
handler_name = p;
|
||||
args = "";
|
||||
}
|
||||
}
|
||||
|
||||
ha_cmd_handler_def_t *def = find_handler(c, handler_name);
|
||||
if (!def) {
|
||||
ha_client_send_result(c, req_id, "error", NULL,
|
||||
"unsupported command");
|
||||
return;
|
||||
}
|
||||
|
||||
/* 调用 handler,填写 result */
|
||||
ha_cmd_result_t result;
|
||||
memset(&result, 0, sizeof(result));
|
||||
ha_status_t st = def->handler(req_id, args, &result, c->config.userdata);
|
||||
|
||||
/* 自动回执 */
|
||||
if (st != HA_OK) {
|
||||
ha_client_send_result(c, req_id, "error", NULL,
|
||||
result.error ? result.error : "handler failed");
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.has_binary && result.binary_data && result.binary_len > 0) {
|
||||
/* 二进制分块回传 */
|
||||
ha_client_send_data_chunked(c, req_id,
|
||||
handler_name, result.binary_mime ? result.binary_mime : "application/octet-stream",
|
||||
result.binary_data, result.binary_len);
|
||||
} else {
|
||||
/* 文本回传 */
|
||||
ha_client_send_result(c, req_id, result.status == 0 ? "ok" : "error",
|
||||
result.output, result.error);
|
||||
}
|
||||
}
|
||||
|
||||
static void handle_speech_start(ha_client_t *c, ha_json_node_t *msg) {
|
||||
const char *req_id = ha_json_get_string(msg, "req_id");
|
||||
const char *kind = ha_json_get_string(msg, "kind");
|
||||
const char *mime = ha_json_get_string(msg, "mime");
|
||||
if (!req_id) return;
|
||||
|
||||
/* 释放旧的聚合数据 */
|
||||
free(c->speech.data);
|
||||
memset(&c->speech, 0, sizeof(c->speech));
|
||||
|
||||
strncpy(c->speech.req_id, req_id, sizeof(c->speech.req_id) - 1);
|
||||
if (kind) strncpy(c->speech.kind, kind, sizeof(c->speech.kind) - 1);
|
||||
if (mime) strncpy(c->speech.mime, mime, sizeof(c->speech.mime) - 1);
|
||||
c->speech.total = ha_json_get_int(msg, "total", 0);
|
||||
}
|
||||
|
||||
static void handle_speech_end(ha_client_t *c, ha_json_node_t *msg) {
|
||||
const char *req_id = ha_json_get_string(msg, "req_id");
|
||||
if (!req_id || strcmp(req_id, c->speech.req_id) != 0) return;
|
||||
|
||||
if (c->config.on_binary && c->speech.data && c->speech.len > 0) {
|
||||
c->config.on_binary(c->speech.req_id, c->speech.kind,
|
||||
c->speech.mime, c->speech.data,
|
||||
c->speech.len, c->config.userdata);
|
||||
}
|
||||
|
||||
free(c->speech.data);
|
||||
memset(&c->speech, 0, sizeof(c->speech));
|
||||
}
|
||||
|
||||
static void handle_text_message(ha_client_t *c, const uint8_t *payload, int len) {
|
||||
/* 解析 JSON */
|
||||
char *tmp = (char *)malloc(len + 1);
|
||||
if (!tmp) return;
|
||||
memcpy(tmp, payload, len);
|
||||
tmp[len] = '\0';
|
||||
|
||||
ha_json_node_t *root = ha_json_parse(tmp);
|
||||
if (!root) { free(tmp); return; }
|
||||
|
||||
const char *op = ha_json_get_string(root, "op");
|
||||
if (!op) { ha_json_free(root); free(tmp); return; }
|
||||
|
||||
switch (c->state) {
|
||||
case HA_STATE_HELLO_SENT:
|
||||
if (strcmp(op, "hello_ack") == 0) {
|
||||
c->state = HA_STATE_BIND_SENT;
|
||||
send_bind(c);
|
||||
}
|
||||
break;
|
||||
case HA_STATE_BIND_SENT:
|
||||
if (strcmp(op, "bind_ack") == 0) {
|
||||
c->state = HA_STATE_READY;
|
||||
if (c->config.on_state) {
|
||||
c->config.on_state(1, c->config.userdata);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case HA_STATE_READY:
|
||||
if (strcmp(op, "cmd") == 0) {
|
||||
handle_cmd_msg(c, root);
|
||||
} else if (strcmp(op, "cmd_speech_start") == 0) {
|
||||
handle_speech_start(c, root);
|
||||
} else if (strcmp(op, "cmd_speech_end") == 0) {
|
||||
handle_speech_end(c, root);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
ha_json_free(root);
|
||||
free(tmp);
|
||||
}
|
||||
|
||||
/* ======================== 连接管理 ======================== */
|
||||
|
||||
static int do_connect(ha_client_t *c) {
|
||||
c->state = HA_STATE_CONNECTING;
|
||||
c->reconnect_cnt++;
|
||||
|
||||
/* 解析 server 地址 */
|
||||
char host[256] = {0};
|
||||
uint16_t port = 9890;
|
||||
const char *p = c->config.server;
|
||||
if (!p) return -1;
|
||||
|
||||
/* 去掉 ws:// 前缀 */
|
||||
if (strncmp(p, "ws://", 5) == 0) p += 5;
|
||||
else if (strncmp(p, "wss://", 6) == 0) p += 6;
|
||||
|
||||
/* 提取 host:port */
|
||||
const char *colon = strchr(p, ':');
|
||||
const char *slash = strchr(p, '/');
|
||||
if (colon && (!slash || colon < slash)) {
|
||||
int host_len = (int)(colon - p);
|
||||
if (host_len > (int)sizeof(host) - 1) host_len = sizeof(host) - 1;
|
||||
memcpy(host, p, host_len);
|
||||
host[host_len] = '\0';
|
||||
port = (uint16_t)atoi(colon + 1);
|
||||
} else {
|
||||
int host_len = (slash ? (int)(slash - p) : (int)strlen(p));
|
||||
if (host_len > (int)sizeof(host) - 1) host_len = sizeof(host) - 1;
|
||||
memcpy(host, p, host_len);
|
||||
host[host_len] = '\0';
|
||||
}
|
||||
|
||||
c->state = HA_STATE_WS_UPGRADING;
|
||||
if (ha_ws_connect(&c->ws, &c->config.transport, host, port,
|
||||
"/api/v1/device/ws", c->config.token) != 0) {
|
||||
c->state = HA_STATE_DISCONNECTED;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* 发送 hello */
|
||||
c->state = HA_STATE_HELLO_SENT;
|
||||
if (send_hello(c) != 0) {
|
||||
ha_ws_close(&c->ws);
|
||||
c->state = HA_STATE_DISCONNECTED;
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ======================== 公共 API ======================== */
|
||||
|
||||
ha_client_t *ha_client_new(const ha_config_t *config) {
|
||||
ha_client_t *c = (ha_client_t *)calloc(1, sizeof(ha_client_t));
|
||||
if (!c) return NULL;
|
||||
memcpy(&c->config, config, sizeof(ha_config_t));
|
||||
c->state = HA_STATE_INIT;
|
||||
c->reconnect_cnt = 0;
|
||||
return c;
|
||||
}
|
||||
|
||||
ha_status_t ha_client_start(ha_client_t *client) {
|
||||
if (!client) return HA_ERR_INVALID;
|
||||
if (client->state != HA_STATE_INIT) return HA_ERR_GENERIC;
|
||||
|
||||
/* 默认心跳间隔 30 秒 */
|
||||
if (client->config.ping_interval <= 0) {
|
||||
client->config.ping_interval = 30;
|
||||
}
|
||||
|
||||
if (do_connect(client) != 0) {
|
||||
return HA_ERR_TRANSPORT;
|
||||
}
|
||||
|
||||
/* 等待 bind_ack(最多 5 秒) */
|
||||
int wait_ms = 5000;
|
||||
int step = 50;
|
||||
while (wait_ms > 0 && client->state != HA_STATE_READY) {
|
||||
/* 处理一帧 */
|
||||
ha_status_t st = ha_client_process(client);
|
||||
if (st != HA_OK && st != HA_ERR_DISCONNECTED) {
|
||||
return st;
|
||||
}
|
||||
if (client->state == HA_STATE_READY) return HA_OK;
|
||||
|
||||
/* 简单延时:靠 process 中的 recv 阻塞 */
|
||||
wait_ms -= step;
|
||||
}
|
||||
|
||||
return (client->state == HA_STATE_READY) ? HA_OK : HA_ERR_TIMEOUT;
|
||||
}
|
||||
|
||||
ha_status_t ha_client_process(ha_client_t *client) {
|
||||
if (!client) return HA_ERR_INVALID;
|
||||
|
||||
if (client->state == HA_STATE_STOPPING) {
|
||||
return HA_ERR_DISCONNECTED;
|
||||
}
|
||||
|
||||
/* 断线重连 */
|
||||
if (client->state == HA_STATE_DISCONNECTED ||
|
||||
client->state == HA_STATE_INIT) {
|
||||
if (client->config.max_reconnect >= 0 &&
|
||||
client->reconnect_cnt > client->config.max_reconnect) {
|
||||
return HA_ERR_DISCONNECTED;
|
||||
}
|
||||
/* 非阻塞模式:不在这里阻塞等待重连,返回 HA_ERR_DISCONNECTED */
|
||||
return HA_ERR_DISCONNECTED;
|
||||
}
|
||||
|
||||
if (!client->ws.connected) {
|
||||
client->state = HA_STATE_DISCONNECTED;
|
||||
if (client->config.on_state) {
|
||||
client->config.on_state(0, client->config.userdata);
|
||||
}
|
||||
return HA_ERR_DISCONNECTED;
|
||||
}
|
||||
|
||||
/* 尝试读取一帧 */
|
||||
const uint8_t *payload = NULL;
|
||||
int len = 0;
|
||||
int ret = ha_ws_read_frame(&client->ws, &payload, &len);
|
||||
|
||||
if (ret < 0) {
|
||||
/* 连接断开 */
|
||||
client->state = HA_STATE_DISCONNECTED;
|
||||
if (client->config.on_state) {
|
||||
client->config.on_state(0, client->config.userdata);
|
||||
}
|
||||
return HA_ERR_DISCONNECTED;
|
||||
}
|
||||
|
||||
switch (ret) {
|
||||
case WS_OPCODE_TEXT:
|
||||
handle_text_message(client, payload, len);
|
||||
break;
|
||||
case WS_OPCODE_BINARY:
|
||||
/* 二进制帧:如果处于语音聚合状态,追加数据 */
|
||||
if (client->speech.req_id[0] && payload) {
|
||||
int new_len = client->speech.len + len;
|
||||
if (new_len > client->speech.cap) {
|
||||
int new_cap = client->speech.cap ? client->speech.cap * 2 : 4096;
|
||||
while (new_cap < new_len) new_cap *= 2;
|
||||
uint8_t *nd = (uint8_t *)realloc(client->speech.data, new_cap);
|
||||
if (!nd) break;
|
||||
client->speech.data = nd;
|
||||
client->speech.cap = new_cap;
|
||||
}
|
||||
memcpy(client->speech.data + client->speech.len, payload, len);
|
||||
client->speech.len = new_len;
|
||||
}
|
||||
break;
|
||||
case WS_OPCODE_PING:
|
||||
/* 回复 pong */
|
||||
ha_ws_send_frame(&client->ws, WS_OPCODE_PONG, NULL, 0);
|
||||
break;
|
||||
case WS_OPCODE_PONG:
|
||||
/* 收到 pong,忽略 */
|
||||
break;
|
||||
case WS_OPCODE_CLOSE:
|
||||
client->state = HA_STATE_DISCONNECTED;
|
||||
if (client->config.on_state) {
|
||||
client->config.on_state(0, client->config.userdata);
|
||||
}
|
||||
return HA_ERR_DISCONNECTED;
|
||||
}
|
||||
|
||||
return HA_OK;
|
||||
}
|
||||
|
||||
void ha_client_send_result(ha_client_t *client, const char *req_id,
|
||||
const char *status, const char *output,
|
||||
const char *error) {
|
||||
if (!client || client->state != HA_STATE_READY) return;
|
||||
json_init(client);
|
||||
ha_json_builder_begin_object(&client->jb);
|
||||
ha_json_builder_string(&client->jb, "op", "cmd_result");
|
||||
ha_json_builder_string(&client->jb, "req_id", req_id);
|
||||
ha_json_builder_string(&client->jb, "status", status ? status : "ok");
|
||||
ha_json_builder_string(&client->jb, "device_id", client->config.device.device_id);
|
||||
if (output && output[0]) {
|
||||
ha_json_builder_string(&client->jb, "output", output);
|
||||
}
|
||||
if (error && error[0]) {
|
||||
ha_json_builder_string(&client->jb, "error", error);
|
||||
}
|
||||
ha_json_builder_end_object(&client->jb);
|
||||
ws_send_json(client);
|
||||
}
|
||||
|
||||
void ha_client_send_data_chunked(ha_client_t *client, const char *req_id,
|
||||
const char *kind, const char *mime,
|
||||
const uint8_t *data, int len) {
|
||||
if (!client || client->state != HA_STATE_READY) return;
|
||||
|
||||
/* cmd_data_start */
|
||||
json_init(client);
|
||||
ha_json_builder_begin_object(&client->jb);
|
||||
ha_json_builder_string(&client->jb, "op", "cmd_data_start");
|
||||
ha_json_builder_string(&client->jb, "req_id", req_id);
|
||||
ha_json_builder_string(&client->jb, "kind", kind ? kind : "data");
|
||||
ha_json_builder_string(&client->jb, "mime", mime ? mime : "application/octet-stream");
|
||||
ha_json_builder_int(&client->jb, "total", len);
|
||||
ha_json_builder_int(&client->jb, "chunk_size", 8192);
|
||||
ha_json_builder_end_object(&client->jb);
|
||||
ws_send_json(client);
|
||||
|
||||
/* 二进制帧分块发送 */
|
||||
int off = 0;
|
||||
while (off < len) {
|
||||
int chunk = len - off;
|
||||
if (chunk > 8192) chunk = 8192;
|
||||
if (ha_ws_send_binary(&client->ws, data + off, chunk) != 0) return;
|
||||
off += chunk;
|
||||
}
|
||||
|
||||
/* cmd_data_end */
|
||||
json_init(client);
|
||||
ha_json_builder_begin_object(&client->jb);
|
||||
ha_json_builder_string(&client->jb, "op", "cmd_data_end");
|
||||
ha_json_builder_string(&client->jb, "req_id", req_id);
|
||||
ha_json_builder_string(&client->jb, "status", "ok");
|
||||
ha_json_builder_int(&client->jb, "total", len);
|
||||
ha_json_builder_end_object(&client->jb);
|
||||
ws_send_json(client);
|
||||
}
|
||||
|
||||
void ha_client_send_event(ha_client_t *client, const char *type,
|
||||
const char *detail) {
|
||||
if (!client || client->state != HA_STATE_READY) return;
|
||||
json_init(client);
|
||||
ha_json_builder_begin_object(&client->jb);
|
||||
ha_json_builder_string(&client->jb, "op", "event");
|
||||
ha_json_builder_string(&client->jb, "device_id", client->config.device.device_id);
|
||||
ha_json_builder_string(&client->jb, "type", type ? type : "");
|
||||
if (detail && detail[0]) {
|
||||
ha_json_builder_string(&client->jb, "payload", detail);
|
||||
}
|
||||
ha_json_builder_end_object(&client->jb);
|
||||
ws_send_json(client);
|
||||
}
|
||||
|
||||
void ha_client_send_status(ha_client_t *client, const char *status) {
|
||||
if (!client || client->state != HA_STATE_READY) return;
|
||||
json_init(client);
|
||||
ha_json_builder_begin_object(&client->jb);
|
||||
ha_json_builder_string(&client->jb, "op", "status");
|
||||
ha_json_builder_string(&client->jb, "device_id", client->config.device.device_id);
|
||||
ha_json_builder_string(&client->jb, "status", status ? status : "online");
|
||||
ha_json_builder_end_object(&client->jb);
|
||||
ws_send_json(client);
|
||||
}
|
||||
|
||||
void ha_client_stop(ha_client_t *client) {
|
||||
if (!client) return;
|
||||
client->state = HA_STATE_STOPPING;
|
||||
if (client->ws.connected) {
|
||||
ha_ws_close(&client->ws);
|
||||
}
|
||||
}
|
||||
|
||||
void ha_client_destroy(ha_client_t *client) {
|
||||
if (!client) return;
|
||||
ha_client_stop(client);
|
||||
free(client->speech.data);
|
||||
free(client);
|
||||
}
|
||||
|
||||
/* ======================== 工具函数 ======================== */
|
||||
|
||||
void ha_cmd_parse_homeagent(const char *command, const char **cap,
|
||||
const char **args) {
|
||||
*cap = command;
|
||||
*args = "";
|
||||
|
||||
if (!command) {
|
||||
*cap = "";
|
||||
return;
|
||||
}
|
||||
|
||||
/* 去掉 homeagent- 前缀 */
|
||||
const char *p = command;
|
||||
if (strncmp(p, "homeagent-", 10) == 0) {
|
||||
p += 10;
|
||||
}
|
||||
|
||||
/* 按空格分割 */
|
||||
const char *space = strchr(p, ' ');
|
||||
if (space) {
|
||||
/* cap 指向 p 但不包含空格,需要临时拷贝 */
|
||||
/* 返回指针到原始字符串,调用方用 strncpy 取出 */
|
||||
*cap = command; /* 调用方应使用 ha_cmd_parse_homeagent 的要小心 */
|
||||
/* 实际上,最简单的方式是原地修改,但 const 不允许 */
|
||||
/* 用静态缓冲区或让调用方自己处理 */
|
||||
static char cap_buf[256];
|
||||
int n = (int)(space - p);
|
||||
if (n > 255) n = 255;
|
||||
strncpy(cap_buf, p, n);
|
||||
cap_buf[n] = '\0';
|
||||
*cap = cap_buf;
|
||||
*args = space + 1;
|
||||
} else {
|
||||
static char cap_buf[256];
|
||||
strncpy(cap_buf, p, sizeof(cap_buf) - 1);
|
||||
cap_buf[sizeof(cap_buf) - 1] = '\0';
|
||||
*cap = cap_buf;
|
||||
*args = "";
|
||||
}
|
||||
}
|
||||
|
||||
void ha_cmd_parse_json(const char *command, const char **action,
|
||||
const char **json_str) {
|
||||
*action = "";
|
||||
*json_str = "";
|
||||
|
||||
if (!command) return;
|
||||
|
||||
const char *p = command;
|
||||
if (strncmp(p, "homeagent-", 10) == 0) {
|
||||
p += 10;
|
||||
}
|
||||
|
||||
const char *brace = strchr(p, '{');
|
||||
if (brace) {
|
||||
static char act_buf[256];
|
||||
int n = (int)(brace - p);
|
||||
while (n > 0 && (p[n - 1] == ' ' || p[n - 1] == '\t')) n--;
|
||||
if (n > 255) n = 255;
|
||||
strncpy(act_buf, p, n);
|
||||
act_buf[n] = '\0';
|
||||
*action = act_buf;
|
||||
*json_str = brace;
|
||||
} else {
|
||||
static char act_buf[256];
|
||||
strncpy(act_buf, p, sizeof(act_buf) - 1);
|
||||
*action = act_buf;
|
||||
}
|
||||
}
|
||||
|
||||
const char *ha_version(void) {
|
||||
return HA_VERSION;
|
||||
}
|
||||
|
||||
@ -1,325 +0,0 @@
|
||||
#include "ha_ws.h"
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
/* WS GUID 用于计算 Accept 值 */
|
||||
#define WS_GUID "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
||||
|
||||
/* ======================== Base64 编码(用于 WS key) ======================== */
|
||||
static const char b64t[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
|
||||
static void base64_encode_bin(const uint8_t *in, int in_len, char *out) {
|
||||
int i = 0, j = 0;
|
||||
uint8_t b[3];
|
||||
while (i < in_len) {
|
||||
int rem = in_len - i;
|
||||
if (rem >= 3) {
|
||||
b[0] = in[i++]; b[1] = in[i++]; b[2] = in[i++];
|
||||
out[j++] = b64t[b[0] >> 2];
|
||||
out[j++] = b64t[((b[0] & 0x03) << 4) | (b[1] >> 4)];
|
||||
out[j++] = b64t[((b[1] & 0x0F) << 2) | (b[2] >> 6)];
|
||||
out[j++] = b64t[b[2] & 0x3F];
|
||||
} else if (rem == 2) {
|
||||
b[0] = in[i++]; b[1] = in[i++];
|
||||
out[j++] = b64t[b[0] >> 2];
|
||||
out[j++] = b64t[((b[0] & 0x03) << 4) | (b[1] >> 4)];
|
||||
out[j++] = b64t[(b[1] & 0x0F) << 2];
|
||||
out[j++] = '=';
|
||||
} else {
|
||||
b[0] = in[i++];
|
||||
out[j++] = b64t[b[0] >> 2];
|
||||
out[j++] = b64t[(b[0] & 0x03) << 4];
|
||||
out[j++] = '=';
|
||||
out[j++] = '=';
|
||||
}
|
||||
}
|
||||
out[j] = '\0';
|
||||
}
|
||||
|
||||
/* 简单伪随机数生成器 */
|
||||
static uint32_t ws_rand_state = 0;
|
||||
static void ws_rand_seed(uint32_t seed) { ws_rand_state = seed; }
|
||||
static uint32_t ws_rand(void) {
|
||||
ws_rand_state = ws_rand_state * 1103515245 + 12345;
|
||||
return ws_rand_state;
|
||||
}
|
||||
|
||||
/* 生成 WS 握手 key */
|
||||
static void ws_gen_key(char *out) {
|
||||
uint8_t buf[16];
|
||||
for (int i = 0; i < 16; i++) {
|
||||
buf[i] = (uint8_t)(ws_rand() & 0xFF);
|
||||
}
|
||||
base64_encode_bin(buf, 16, out);
|
||||
}
|
||||
|
||||
/* ======================== 从传输层接收指定字节数 ======================== */
|
||||
static int recv_all(ha_ws_t *ws, uint8_t *buf, int len) {
|
||||
int pos = 0;
|
||||
while (pos < len) {
|
||||
int n = ws->transport->recv(ws->transport->ctx, buf + pos, len - pos);
|
||||
if (n <= 0) return -1;
|
||||
pos += n;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ======================== 发送 WS 帧 ======================== */
|
||||
int ha_ws_send_frame(ha_ws_t *ws, int opcode, const uint8_t *payload, int len) {
|
||||
uint8_t hdr[14]; /* 最大帧头:2 + 8 + 4 = 14 */
|
||||
int hdr_len = 0;
|
||||
|
||||
hdr[0] = 0x80 | opcode; /* FIN + opcode */
|
||||
hdr_len = 2;
|
||||
|
||||
int ext_len = 0;
|
||||
if (len < 126) {
|
||||
hdr[1] = 0x80 | len; /* mask bit + length */
|
||||
} else if (len < 65536) {
|
||||
hdr[1] = 0x80 | 126;
|
||||
hdr_len = 4;
|
||||
hdr[2] = (uint8_t)(len >> 8);
|
||||
hdr[3] = (uint8_t)(len & 0xFF);
|
||||
ext_len = 2;
|
||||
} else {
|
||||
hdr[1] = 0x80 | 127;
|
||||
hdr_len = 10;
|
||||
uint64_t l = (uint64_t)len;
|
||||
for (int i = 8; i > 0; i--) {
|
||||
hdr[1 + i] = (uint8_t)(l & 0xFF);
|
||||
l >>= 8;
|
||||
}
|
||||
ext_len = 8;
|
||||
}
|
||||
|
||||
/* mask key */
|
||||
uint8_t mask_key[4];
|
||||
mask_key[0] = (uint8_t)(ws_rand() & 0xFF);
|
||||
mask_key[1] = (uint8_t)(ws_rand() & 0xFF);
|
||||
mask_key[2] = (uint8_t)(ws_rand() & 0xFF);
|
||||
mask_key[3] = (uint8_t)(ws_rand() & 0xFF);
|
||||
|
||||
int mask_off = 2 + ext_len;
|
||||
hdr[mask_off] = mask_key[0];
|
||||
hdr[mask_off + 1] = mask_key[1];
|
||||
hdr[mask_off + 2] = mask_key[2];
|
||||
hdr[mask_off + 3] = mask_key[3];
|
||||
hdr_len = mask_off + 4;
|
||||
|
||||
/* 发送帧头 */
|
||||
if (ws->transport->send(ws->transport->ctx, hdr, hdr_len) != hdr_len) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* 发送掩码后的 payload */
|
||||
if (len > 0) {
|
||||
/* 如果 payload 不大,用栈缓冲区 */
|
||||
uint8_t stack_buf[2048];
|
||||
uint8_t *masked = (len <= (int)sizeof(stack_buf)) ? stack_buf : (uint8_t *)malloc(len);
|
||||
if (!masked) return -1;
|
||||
|
||||
for (int i = 0; i < len; i++) {
|
||||
masked[i] = payload[i] ^ mask_key[i & 3];
|
||||
}
|
||||
|
||||
int ret = (ws->transport->send(ws->transport->ctx, masked, len) == len) ? 0 : -1;
|
||||
|
||||
if (masked != stack_buf) free(masked);
|
||||
if (ret != 0) return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ======================== 公共 API ======================== */
|
||||
|
||||
int ha_ws_connect(ha_ws_t *ws, ha_transport_t *transport,
|
||||
const char *host, uint16_t port,
|
||||
const char *path, const char *token) {
|
||||
memset(ws, 0, sizeof(ha_ws_t));
|
||||
ws->transport = transport;
|
||||
ws->connected = 0;
|
||||
|
||||
strncpy(ws->host, host, sizeof(ws->host) - 1);
|
||||
ws->port = port;
|
||||
strncpy(ws->path, path, sizeof(ws->path) - 1);
|
||||
if (token) strncpy(ws->token, token, sizeof(ws->token) - 1);
|
||||
|
||||
/* 种子 */
|
||||
ws_rand_seed((uint32_t)(uintptr_t)ws ^ (uint32_t)port);
|
||||
|
||||
/* 1. TCP 连接 */
|
||||
if (transport->connect(transport->ctx, host, port) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* 2. 发送 WS 升级请求 */
|
||||
char key[32];
|
||||
ws_gen_key(key);
|
||||
|
||||
char req[1024];
|
||||
int n = snprintf(req, sizeof(req),
|
||||
"GET %s HTTP/1.1\r\n"
|
||||
"Host: %s:%u\r\n"
|
||||
"Upgrade: websocket\r\n"
|
||||
"Connection: Upgrade\r\n"
|
||||
"Sec-WebSocket-Key: %s\r\n"
|
||||
"Sec-WebSocket-Version: 13\r\n"
|
||||
"\r\n",
|
||||
path, host, (unsigned)port, key);
|
||||
|
||||
/* 如果 token 存在,加到路径参数中 */
|
||||
if (token && token[0]) {
|
||||
n = snprintf(req, sizeof(req),
|
||||
"GET %s?token=%s HTTP/1.1\r\n"
|
||||
"Host: %s:%u\r\n"
|
||||
"Upgrade: websocket\r\n"
|
||||
"Connection: Upgrade\r\n"
|
||||
"Sec-WebSocket-Key: %s\r\n"
|
||||
"Sec-WebSocket-Version: 13\r\n"
|
||||
"\r\n",
|
||||
path, token, host, (unsigned)port, key);
|
||||
}
|
||||
|
||||
if (transport->send(transport->ctx, (uint8_t *)req, n) != n) {
|
||||
transport->close(transport->ctx);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* 3. 读取响应头(直到 \r\n\r\n) */
|
||||
char resp[1024];
|
||||
int resp_len = 0;
|
||||
int found = 0;
|
||||
while (resp_len < (int)sizeof(resp) - 1) {
|
||||
int n = transport->recv(transport->ctx, (uint8_t *)(resp + resp_len), 1);
|
||||
if (n <= 0) {
|
||||
transport->close(transport->ctx);
|
||||
return -1;
|
||||
}
|
||||
resp_len += n;
|
||||
resp[resp_len] = '\0';
|
||||
if (resp_len >= 4 && strcmp(resp + resp_len - 4, "\r\n\r\n") == 0) {
|
||||
found = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
transport->close(transport->ctx);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* 4. 检查状态码 101 */
|
||||
if (strstr(resp, " 101 ") == NULL) {
|
||||
transport->close(transport->ctx);
|
||||
return -1;
|
||||
}
|
||||
|
||||
ws->connected = 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ha_ws_send_text(ha_ws_t *ws, const char *text) {
|
||||
if (!ws->connected) return -1;
|
||||
return ha_ws_send_frame(ws, WS_OPCODE_TEXT, (const uint8_t *)text, (int)strlen(text));
|
||||
}
|
||||
|
||||
int ha_ws_send_binary(ha_ws_t *ws, const uint8_t *data, int len) {
|
||||
if (!ws->connected) return -1;
|
||||
return ha_ws_send_frame(ws, WS_OPCODE_BINARY, data, len);
|
||||
}
|
||||
|
||||
int ha_ws_send_ping(ha_ws_t *ws) {
|
||||
if (!ws->connected) return -1;
|
||||
return ha_ws_send_frame(ws, WS_OPCODE_PING, NULL, 0);
|
||||
}
|
||||
|
||||
int ha_ws_read_frame(ha_ws_t *ws, const uint8_t **payload, int *len) {
|
||||
if (!ws->connected) return -1;
|
||||
|
||||
*payload = NULL;
|
||||
*len = 0;
|
||||
|
||||
/* 读取帧头:2 字节 */
|
||||
uint8_t hdr[2];
|
||||
if (recv_all(ws, hdr, 2) != 0) {
|
||||
ws->connected = 0;
|
||||
return -1;
|
||||
}
|
||||
|
||||
int opcode = hdr[0] & 0x0F;
|
||||
int masked = (hdr[1] & 0x80) ? 1 : 0;
|
||||
uint64_t frame_len = hdr[1] & 0x7F;
|
||||
|
||||
if (frame_len == 126) {
|
||||
uint8_t ext[2];
|
||||
if (recv_all(ws, ext, 2) != 0) { ws->connected = 0; return -1; }
|
||||
frame_len = ((uint64_t)ext[0] << 8) | ext[1];
|
||||
} else if (frame_len == 127) {
|
||||
uint8_t ext[8];
|
||||
if (recv_all(ws, ext, 8) != 0) { ws->connected = 0; return -1; }
|
||||
frame_len = 0;
|
||||
for (int i = 0; i < 8; i++) {
|
||||
frame_len = (frame_len << 8) | ext[i];
|
||||
}
|
||||
}
|
||||
|
||||
/* 读取 mask key */
|
||||
uint8_t mask_key[4] = {0, 0, 0, 0};
|
||||
if (masked) {
|
||||
if (recv_all(ws, mask_key, 4) != 0) { ws->connected = 0; return -1; }
|
||||
}
|
||||
|
||||
/* 限制帧大小 */
|
||||
if (frame_len > sizeof(ws->read_buf)) {
|
||||
/* 帧太大,跳过 payload */
|
||||
uint64_t skip = frame_len;
|
||||
uint8_t tmp[256];
|
||||
while (skip > 0) {
|
||||
int to_skip = (skip > sizeof(tmp)) ? (int)sizeof(tmp) : (int)skip;
|
||||
if (recv_all(ws, tmp, to_skip) != 0) { ws->connected = 0; return -1; }
|
||||
skip -= to_skip;
|
||||
}
|
||||
return -1; /* 返回错误,帧太大 */
|
||||
}
|
||||
|
||||
/* 读取 payload */
|
||||
if (frame_len > 0) {
|
||||
if (recv_all(ws, ws->read_buf, (int)frame_len) != 0) {
|
||||
ws->connected = 0;
|
||||
return -1;
|
||||
}
|
||||
/* 如果有 mask,解掩码 */
|
||||
if (masked) {
|
||||
for (uint64_t i = 0; i < frame_len; i++) {
|
||||
ws->read_buf[i] ^= mask_key[i & 3];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*payload = ws->read_buf;
|
||||
*len = (int)frame_len;
|
||||
|
||||
switch (opcode) {
|
||||
case WS_OPCODE_CLOSE:
|
||||
ws->connected = 0;
|
||||
return WS_OPCODE_CLOSE;
|
||||
case WS_OPCODE_PING:
|
||||
return WS_OPCODE_PING;
|
||||
case WS_OPCODE_PONG:
|
||||
return WS_OPCODE_PONG;
|
||||
case WS_OPCODE_TEXT:
|
||||
case WS_OPCODE_BINARY:
|
||||
return opcode;
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
void ha_ws_close(ha_ws_t *ws) {
|
||||
if (ws->connected) {
|
||||
ha_ws_send_frame(ws, WS_OPCODE_CLOSE, NULL, 0);
|
||||
ws->connected = 0;
|
||||
}
|
||||
ws->transport->close(ws->transport->ctx);
|
||||
}
|
||||
@ -1,62 +0,0 @@
|
||||
#ifndef HA_WS_H
|
||||
#define HA_WS_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include "../include/ha_remotedevice.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ======================== WS 帧类型 ======================== */
|
||||
#define WS_OPCODE_CONTINUATION 0x0
|
||||
#define WS_OPCODE_TEXT 0x1
|
||||
#define WS_OPCODE_BINARY 0x2
|
||||
#define WS_OPCODE_CLOSE 0x8
|
||||
#define WS_OPCODE_PING 0x9
|
||||
#define WS_OPCODE_PONG 0xA
|
||||
|
||||
/* ======================== WS 连接 ======================== */
|
||||
typedef struct {
|
||||
ha_transport_t *transport; /* 用户实现的传输层 */
|
||||
int connected; /* 是否已连接 */
|
||||
uint8_t read_buf[8192]; /* 读缓冲区 */
|
||||
int read_pos; /* 缓冲区中有效数据起始位置 */
|
||||
int read_len; /* 缓冲区中有效数据长度 */
|
||||
char host[256]; /* 缓存目标地址 */
|
||||
uint16_t port;
|
||||
char path[256];
|
||||
char token[256];
|
||||
} ha_ws_t;
|
||||
|
||||
/* 创建 WS 连接。返回 0 成功,非 0 失败。 */
|
||||
int ha_ws_connect(ha_ws_t *ws, ha_transport_t *transport,
|
||||
const char *host, uint16_t port,
|
||||
const char *path, const char *token);
|
||||
|
||||
/* 发送文本帧。返回 0 成功。 */
|
||||
int ha_ws_send_text(ha_ws_t *ws, const char *text);
|
||||
|
||||
/* 发送二进制帧。返回 0 成功。 */
|
||||
int ha_ws_send_binary(ha_ws_t *ws, const uint8_t *data, int len);
|
||||
|
||||
/* 发送 ping。返回 0 成功。 */
|
||||
int ha_ws_send_ping(ha_ws_t *ws);
|
||||
|
||||
/* 读取一帧。
|
||||
* 返回 opcode (0x1/0x2/0x8/0x9/0xA),-1 表示关闭或错误。
|
||||
* payload 和 len 指向内部缓冲区,在下次调用前有效。 */
|
||||
int ha_ws_read_frame(ha_ws_t *ws, const uint8_t **payload, int *len);
|
||||
|
||||
/* 发送原始 WS 帧(内部使用,用于回复 ping) */
|
||||
int ha_ws_send_frame(ha_ws_t *ws, int opcode, const uint8_t *payload, int len);
|
||||
|
||||
/* 关闭 WS 连接 */
|
||||
void ha_ws_close(ha_ws_t *ws);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* HA_WS_H */
|
||||
File diff suppressed because it is too large
Load Diff
@ -9,6 +9,14 @@ type KnowledgeAPI interface {
|
||||
|
||||
// Knowledge represents a knowledge entry.
|
||||
type Knowledge struct {
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
Name string `json:"name"`
|
||||
// Category 是该条目的父分类路径(如 "tech/go"),根下条目为空。
|
||||
//
|
||||
// 为何加这个字段:对外服务(kbtree)要做**暴露范围过滤**就必须知道
|
||||
// 每条结果属于哪个分类 —— 过滤只能发生在服务端(客户端过滤等于
|
||||
// 没过滤,范围外内容已经随响应发出去了)。
|
||||
// 之前这里只有 Name/Content,内核明明返回了 Category 却在
|
||||
// knowledge_impl.SearchIn 的拷贝里丢掉,导致外部无法按分类判定。
|
||||
Category string `json:"category,omitempty"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
101
sdk/plugin.go
101
sdk/plugin.go
@ -74,6 +74,40 @@ func ValidRecallPolicy(policy string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// 场面策略:决定一次输入是否参与**场面识别**(场景式记忆)。
|
||||
//
|
||||
// 与前两项再正交一轴: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 保持开,声明项只作为插件
|
||||
// 将来确实需要时的闸门。
|
||||
const (
|
||||
ScenePolicyAuto = "auto"
|
||||
ScenePolicyNone = "none"
|
||||
)
|
||||
|
||||
// ValidScenePolicy 校验场面策略取值;空串等价于 ScenePolicyAuto。
|
||||
func ValidScenePolicy(policy string) bool {
|
||||
switch policy {
|
||||
case "", ScenePolicyAuto, ScenePolicyNone:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// InjectOptions 声明一次注入行为在记忆层与上下文层的表现。
|
||||
//
|
||||
// 零值 = 记入记忆 + 不裁剪上下文,与历史行为(三参数注入方法)完全一致,
|
||||
@ -102,7 +136,11 @@ type InjectOptions struct {
|
||||
// 空串 = 默认(输入/注入 auto,即保持既有「每条输入都召回」的行为);
|
||||
// RecallPolicyNone 显式关闭(如中断通知的 meta 文本不该据它召回)。
|
||||
RecallPolicy string
|
||||
CleanerName string
|
||||
// ScenePolicy 声明此次注入是否参与场面识别(场景式记忆)。
|
||||
// 空串 = 默认参与(保持既有行为);ScenePolicyNone 显式关闭,
|
||||
// 适用于不产生任何场面指纹的纯内部信号(心跳、自循环、内部状态)。
|
||||
ScenePolicy string
|
||||
CleanerName string
|
||||
|
||||
// Priority 声明**中断注入**的优先级(仅 InjectInterrupt* 有意义)。
|
||||
//
|
||||
@ -132,6 +170,7 @@ const (
|
||||
// Cleaner: 计算层过滤函数,不改原文;仅在向量化/jieba/蒸馏/存档提取关键词时调用
|
||||
// ContextPolicy: 此通道的输入到达后是否据此裁剪上下文,默认 none(不裁剪)
|
||||
// RecallPolicy: 此通道的输入到达后是否据此召回相关记忆,默认 auto(召回)
|
||||
// ScenePolicy: 此通道的输入到达后是否参与场面识别(场景式记忆),默认 auto(参与)
|
||||
//
|
||||
// JSON tag 是必需的:通道定义要跨进程传给内核,而 Cleaner 是函数(必须忽略)。
|
||||
// 没有 tag 时既无法整体 marshal(func 不支持),又会诱使调用方手写字段白名单——
|
||||
@ -142,6 +181,8 @@ type ChannelDef struct {
|
||||
ContextPolicy string `json:"context_policy,omitempty"`
|
||||
// RecallPolicy 见 InjectOptions.RecallPolicy;空串等价 auto(保持既有行为)。
|
||||
RecallPolicy string `json:"recall_policy,omitempty"`
|
||||
// ScenePolicy 见 InjectOptions.ScenePolicy;空串等价 auto(保持既有行为)。
|
||||
ScenePolicy string `json:"scene_policy,omitempty"`
|
||||
}
|
||||
|
||||
// StageContext provides context for stage handlers.
|
||||
@ -199,6 +240,41 @@ type ToolResult struct {
|
||||
Result interface{} `json:"result"`
|
||||
}
|
||||
|
||||
// ToolError 描述一次工具调用的失败原因。
|
||||
//
|
||||
// 存在的理由:失败若只表达为文本,模型无法定位到字段,只能原样重试
|
||||
// (实测 cmd_run 失败率 34%~48%,全部源于同一个成因:参数被截断或
|
||||
// JSON 写坏,工具却只回报 "command is required" 这类与真因无关的错)。
|
||||
//
|
||||
// ⚠️ 零值语义:插件**不必**改用本类型。内核的失败识别同时兼容既有三种约定
|
||||
// ({"error":…}、{"isError":true,…}、显式 error 返回),见 core.isToolError。
|
||||
// 本类型是给**新写**的工具用的可选项,不是迁移要求。
|
||||
type ToolError struct {
|
||||
// Field 是出错的参数字段名(参数校验失败时填)。
|
||||
Field string `json:"field,omitempty"`
|
||||
// Reason 是机器可读的原因码:required / type / unauthorized / timeout / not_found。
|
||||
Reason string `json:"reason"`
|
||||
// Detail 是人类可读的补充说明。
|
||||
Detail string `json:"detail,omitempty"`
|
||||
// Hint 是给模型的可执行指引(该改什么、不要重试什么)。
|
||||
Hint string `json:"hint,omitempty"`
|
||||
}
|
||||
|
||||
// Error 实现 error,便于工具同时走 (ToolError, error) 通道。
|
||||
func (e *ToolError) Error() string {
|
||||
if e == nil {
|
||||
return ""
|
||||
}
|
||||
s := e.Reason
|
||||
if e.Field != "" {
|
||||
s = e.Field + ": " + s
|
||||
}
|
||||
if e.Detail != "" {
|
||||
s += " (" + e.Detail + ")"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// ToolDef describes a tool that the plugin exposes.
|
||||
type ToolDef struct {
|
||||
Name string `json:"name"`
|
||||
@ -212,6 +288,29 @@ type ToolDef struct {
|
||||
// ""(默认 none) / RecallPolicyNone / RecallPolicyAuto。
|
||||
// 默认 none:多数工具输出是噪声;需要「取回真实内容后据它召回」的工具(如 qq_get_message)应显式声明 auto。
|
||||
RecallPolicy string `json:"recall_policy,omitempty"`
|
||||
// ParallelSafe 声明此工具**可以被并发执行**(同一批多个 tool_call 同时跑)。
|
||||
//
|
||||
// ⚠️ 零值 false 是刻意的:存量插件不改一行就得到**保守**行为
|
||||
//(整批串行),不会因升级被意外并发。声明它是**责任**而非特权。
|
||||
//
|
||||
// 判据(三者皆满足才可并发):
|
||||
// · handler 自身线程安全(不持有跨调用的可变状态)
|
||||
// · 不与同批其它工具争抢同一资源(SQLite 写、设备、同一输出通道)
|
||||
// · 执行顺序无关(顺序敏感的工具应留 false,由内核保序)
|
||||
ParallelSafe bool `json:"parallel_safe,omitempty"`
|
||||
// Serial 声明本工具**必须**串行 —— ParallelSafe 的反向标记。
|
||||
//
|
||||
// 为什么需要它:ParallelSafe 的零值 false 已经表达"安全/串行",
|
||||
// 插件无法区分"我没想过"和"我确认过必须串行"。一旦工具作者需要
|
||||
// 把"这里**故意**串行,是有原因的"写进代码(而不只是没填),
|
||||
// 这个区分就是必需的 —— 否则只能靠命名约定传递意图。
|
||||
//
|
||||
// 适用场景:读操作但有隐含顺序约束(终端 read/resize 这类共享会话
|
||||
// 状态)、或写操作虽已加锁但需要串行以获得可预测的交错顺序。
|
||||
//
|
||||
// 判据优先级:**Serial 胜出**。显式声明"必须串行"不允许被
|
||||
// ParallelSafe 或任何默认值覆盖。
|
||||
Serial bool `json:"serial,omitempty"`
|
||||
}
|
||||
|
||||
// IOInjector provides methods for injecting input and interrupts into the agent pipeline.
|
||||
|
||||
Reference in New Issue
Block a user