diff --git a/docs/api/bridge.md b/docs/api/bridge.md
index 02e57dd..146ef3a 100644
--- a/docs/api/bridge.md
+++ b/docs/api/bridge.md
@@ -12,7 +12,7 @@ type APIRegistrar func(name string) error
APIRegistrar registers a plugin API for external access.
-`plugin.go:311`
+`plugin.go:410`
### `InputChannelRegistrar`
@@ -22,7 +22,7 @@ type InputChannelRegistrar func(name string, def ChannelDef) error
InputChannelRegistrar registers an input channel with its memory behavior.
-`plugin.go:314`
+`plugin.go:413`
### `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.
-`plugin.go:317`
+`plugin.go:416`
### `OutputChannelUnregistrar`
@@ -46,7 +46,7 @@ OutputChannelUnregistrar 注销一个输出通道。
动态通道 —— 典型是远程设备:`device/` 只在设备在线期间存在,设备掉线后
必须注销,否则 output_list_channels 会一直列着它、模型会往一个死通道发消息。
-`plugin.go:324`
+`plugin.go:423`
### `PluginSDK.SetDocMemoryAPI`
@@ -57,7 +57,7 @@ OutputChannelUnregistrar 注销一个输出通道。
func (s *PluginSDK) SetDocMemoryAPI(dm DocMemoryAPI)
```
-`plugin.go:619`
+`plugin.go:718`
### `PluginSDK.SetEventSubscriber`
@@ -68,7 +68,7 @@ func (s *PluginSDK) SetDocMemoryAPI(dm DocMemoryAPI)
func (s *PluginSDK) SetEventSubscriber(es EventSubscriber)
```
-`plugin.go:643`
+`plugin.go:742`
### `PluginSDK.SetIOInjector`
@@ -81,7 +81,7 @@ func (s *PluginSDK) SetIOInjector(io IOInjector)
SetIOInjector sets the IO injector (called by the core at startup).
-`plugin.go:600`
+`plugin.go:699`
### `PluginSDK.SetInputChannelRegistrar`
@@ -94,7 +94,7 @@ func (s *PluginSDK) SetInputChannelRegistrar(r InputChannelRegistrar)
SetInputChannelRegistrar sets the input channel registrar (called by the core at startup).
-`plugin.go:593`
+`plugin.go:692`
### `PluginSDK.SetKnowledgeAPI`
@@ -105,7 +105,7 @@ SetInputChannelRegistrar sets the input channel registrar (called by the core at
func (s *PluginSDK) SetKnowledgeAPI(kn KnowledgeAPI)
```
-`plugin.go:625`
+`plugin.go:724`
### `PluginSDK.SetLLMAPI`
@@ -116,7 +116,7 @@ func (s *PluginSDK) SetKnowledgeAPI(kn KnowledgeAPI)
func (s *PluginSDK) SetLLMAPI(llm LLMAPI)
```
-`plugin.go:631`
+`plugin.go:730`
### `PluginSDK.SetMemoryAPI`
@@ -129,7 +129,7 @@ func (s *PluginSDK) SetMemoryAPI(mem MemoryAPI)
SetMemoryAPI sets the memory API (called by the core at startup).
-`plugin.go:607`
+`plugin.go:706`
### `PluginSDK.SetOutputChannelRegistrar`
@@ -142,7 +142,7 @@ func (s *PluginSDK) SetOutputChannelRegistrar(r OutputChannelRegistrar)
SetOutputChannelRegistrar sets the output channel registrar (called by the core at startup).
-`plugin.go:579`
+`plugin.go:678`
### `PluginSDK.SetOutputChannelUnregistrar`
@@ -155,7 +155,7 @@ func (s *PluginSDK) SetOutputChannelUnregistrar(r OutputChannelUnregistrar)
SetOutputChannelUnregistrar sets the output channel unregistrar (called by the core at startup).
-`plugin.go:586`
+`plugin.go:685`
### `PluginSDK.SetPluginMgrAPI`
@@ -168,7 +168,7 @@ func (s *PluginSDK) SetPluginMgrAPI(pm PluginMgrAPI)
SetPluginMgrAPI sets the plugin manager API (called by the bridge at startup).
-`plugin.go:650`
+`plugin.go:749`
### `PluginSDK.SetSocialAPI`
@@ -179,7 +179,7 @@ SetPluginMgrAPI sets the plugin manager API (called by the bridge at startup).
func (s *PluginSDK) SetSocialAPI(social SocialAPI)
```
-`plugin.go:637`
+`plugin.go:736`
### `PluginSDK.SetTextMemoryAPI`
@@ -190,7 +190,7 @@ func (s *PluginSDK) SetSocialAPI(social SocialAPI)
func (s *PluginSDK) SetTextMemoryAPI(tm TextMemoryAPI)
```
-`plugin.go:613`
+`plugin.go:712`
### `ToolRegistrar`
@@ -200,5 +200,5 @@ type ToolRegistrar func(name string, def ToolDef, handler ToolHandler) error
ToolRegistrar registers a tool dynamically.
-`plugin.go:305`
+`plugin.go:404`
diff --git a/docs/api/builtin-only.md b/docs/api/builtin-only.md
index cf916b4..a0fc716 100644
--- a/docs/api/builtin-only.md
+++ b/docs/api/builtin-only.md
@@ -17,7 +17,7 @@ const PriorityL4
PriorityL4 仅内核级(内置)插件可用;外部插件声明会被夹到 L3。
-`plugin.go:127`
+`plugin.go:165`
### `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).
-`plugin.go:451`
+`plugin.go:550`
### `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)
```
-`plugin.go:643`
+`plugin.go:742`
### `PluginSDK.SetOutputChannelUnregistrar`
@@ -54,7 +54,7 @@ func (s *PluginSDK) SetOutputChannelUnregistrar(r OutputChannelUnregistrar)
SetOutputChannelUnregistrar sets the output channel unregistrar (called by the core at startup).
-`plugin.go:586`
+`plugin.go:685`
### `PluginSDK.UnregisterOutputChannel`
@@ -67,7 +67,7 @@ func (s *PluginSDK) UnregisterOutputChannel(name string) error
UnregisterOutputChannel 注销一个输出通道(动态通道随资源生灭时必须调用)。
-`plugin.go:544`
+`plugin.go:643`
### `EventSubscriber.Subscribe`
diff --git a/docs/api/channels.md b/docs/api/channels.md
index ae4f64a..954c333 100644
--- a/docs/api/channels.md
+++ b/docs/api/channels.md
@@ -33,7 +33,7 @@ for routing the agent's response.
InjectInputMedia(source, channel, text string, blocks []ContentBlock)
```
-`plugin.go:230`
+`plugin.go:329`
### `IOInjector.InjectInputMediaOpts`
@@ -41,7 +41,7 @@ InjectInputMedia(source, channel, text string, blocks []ContentBlock)
InjectInputMediaOpts(source, channel, text string, blocks []ContentBlock, opts InjectOptions)
```
-`plugin.go:241`
+`plugin.go:340`
### `IOInjector.InjectInputMediaSync`
@@ -49,7 +49,7 @@ InjectInputMediaOpts(source, channel, text string, blocks []ContentBlock, opts I
InjectInputMediaSync(source, channel, text string, blocks []ContentBlock) string
```
-`plugin.go:231`
+`plugin.go:330`
### `IOInjector.InjectInputMediaSyncOpts`
@@ -57,7 +57,7 @@ InjectInputMediaSync(source, channel, text string, blocks []ContentBlock) string
InjectInputMediaSyncOpts(source, channel, text string, blocks []ContentBlock, opts InjectOptions) string
```
-`plugin.go:242`
+`plugin.go:341`
### `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,` |
-`plugin.go:226`
+`plugin.go:325`
### `IOInjector.InjectInputSyncOpts`
@@ -83,7 +83,7 @@ InjectInputSync 注入输入事件并同步等待 agent 回复,返回回复文
InjectInputSyncOpts(source, channel, text string, opts InjectOptions) string
```
-`plugin.go:240`
+`plugin.go:339`
### `IOInjector.InjectInterruptMedia`
@@ -91,7 +91,7 @@ InjectInputSyncOpts(source, channel, text string, opts InjectOptions) string
InjectInterruptMedia(source, channel, text string, blocks []ContentBlock)
```
-`plugin.go:232`
+`plugin.go:331`
### `IOInjector.InjectInterruptMediaOpts`
@@ -99,7 +99,7 @@ InjectInterruptMedia(source, channel, text string, blocks []ContentBlock)
InjectInterruptMediaOpts(source, channel, text string, blocks []ContentBlock, opts InjectOptions)
```
-`plugin.go:243`
+`plugin.go:342`
### `IOInjector.InjectInterruptText`
@@ -107,7 +107,7 @@ InjectInterruptMediaOpts(source, channel, text string, blocks []ContentBlock, op
InjectInterruptText(source, channel, text string)
```
-`plugin.go:221`
+`plugin.go:320`
### `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{` |
-`plugin.go:239`
+`plugin.go:338`
### `IOInjector.InjectText`
@@ -132,7 +132,7 @@ InjectInterruptTextOpts(source, channel, text string, opts InjectOptions)
InjectText(source, channel, text string)
```
-`plugin.go:222`
+`plugin.go:321`
### `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))` |
-`plugin.go:223`
+`plugin.go:322`
### `IOInjector.InjectTextOpts`
@@ -159,7 +159,7 @@ InjectTextOpts(source, channel, text string, opts InjectOptions)
上面那些不带 opts 的方法等价于传零值 InjectOptions(记入记忆 + 不裁剪),
保留它们是为了不破坏已有插件;新代码应当用 Opts 变体把意图写清楚。
-`plugin.go:238`
+`plugin.go:337`
### `IOInjector.SetToolBlocks`
@@ -170,7 +170,7 @@ SetToolBlocks(blocks []ContentBlock)
SetToolBlocks 插件工具注入多模态内容块(image_url/audio_url),内核在下一条
tool message 的 content 数组里带上这些块,让模型在后续轮次看到图/听到音频。
-`plugin.go:229`
+`plugin.go:328`
### `CapAudio`
@@ -180,7 +180,7 @@ const CapAudio
Output capability flags
-`plugin.go:331`
+`plugin.go:430`
### `CapFile`
@@ -190,7 +190,7 @@ const CapFile
Output capability flags
-`plugin.go:329`
+`plugin.go:428`
### `CapImage`
@@ -200,7 +200,7 @@ const CapImage
Output capability flags
-`plugin.go:330`
+`plugin.go:429`
### `CapStructured`
@@ -210,7 +210,7 @@ const CapStructured
Output capability flags
-`plugin.go:332`
+`plugin.go:431`
### `CapText`
@@ -220,7 +220,7 @@ const CapText
Output capability flags
-`plugin.go:328`
+`plugin.go:427`
### `ChannelDef`
@@ -233,12 +233,13 @@ NoMemory: 此通道输入/输出不参与记忆计算(向量化/关键词提
Cleaner: 计算层过滤函数,不改原文;仅在向量化/jieba/蒸馏/存档提取关键词时调用
ContextPolicy: 此通道的输入到达后是否据此裁剪上下文,默认 none(不裁剪)
RecallPolicy: 此通道的输入到达后是否据此召回相关记忆,默认 auto(召回)
+ScenePolicy: 此通道的输入到达后是否参与场面识别(场景式记忆),默认 auto(参与)
JSON tag 是必需的:通道定义要跨进程传给内核,而 Cleaner 是函数(必须忽略)。
没有 tag 时既无法整体 marshal(func 不支持),又会诱使调用方手写字段白名单——
那样新增字段会被静默丢掉。
-`plugin.go:139`
+`plugin.go:178`
### `ContextPolicyNone`
@@ -280,7 +281,7 @@ blocks 会落进媒体存储被记忆引用捕获,同时作为当前轮 conten
的「下一轮 tool message」语义。
等价于 InjectInputMediaOpts(..., InjectOptions{})。
-`plugin.go:706`
+`plugin.go:805`
### `PluginSDK.InjectInputMediaOpts`
@@ -290,7 +291,7 @@ func (s *PluginSDK) InjectInputMediaOpts(source, channel, text string, blocks []
InjectInputMediaOpts 注入带媒体块的输入,并声明记忆/裁剪行为。
-`plugin.go:745`
+`plugin.go:844`
### `PluginSDK.InjectInputMediaSync`
@@ -301,7 +302,7 @@ func (s *PluginSDK) InjectInputMediaSync(source, channel, text string, blocks []
InjectInputMediaSync 注入带媒体内容块的输入并同步等待 agent 回复。
等价于 InjectInputMediaSyncOpts(..., InjectOptions{})。
-`plugin.go:712`
+`plugin.go:811`
### `PluginSDK.InjectInputMediaSyncOpts`
@@ -311,7 +312,7 @@ func (s *PluginSDK) InjectInputMediaSyncOpts(source, channel, text string, block
InjectInputMediaSyncOpts 注入带媒体块的输入并同步等待回复,同时声明记忆/裁剪行为。
-`plugin.go:752`
+`plugin.go:851`
### `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,` |
-`plugin.go:697`
+`plugin.go:796`
### `PluginSDK.InjectInputSyncOpts`
@@ -340,7 +341,7 @@ func (s *PluginSDK) InjectInputSyncOpts(source, channel, text string, opts Injec
InjectInputSyncOpts 注入输入并同步等待回复,同时在这次注入上声明记忆/裁剪行为。
-`plugin.go:736`
+`plugin.go:835`
### `PluginSDK.InjectInterruptMedia`
@@ -351,7 +352,7 @@ func (s *PluginSDK) InjectInterruptMedia(source, channel, text string, blocks []
InjectInterruptMedia 注入带媒体内容块的中断,可抢占当前 LLM 处理。
blocks 随中断消息一起发给模型。
-`plugin.go:769`
+`plugin.go:868`
### `PluginSDK.InjectInterruptMediaOpts`
@@ -361,7 +362,7 @@ func (s *PluginSDK) InjectInterruptMediaOpts(source, channel, text string, block
InjectInterruptMediaOpts 注入带媒体块的中断,并声明记忆/裁剪行为。
-`plugin.go:761`
+`plugin.go:860`
### `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{}):记入记忆、不裁剪。
-`plugin.go:678`
+`plugin.go:777`
### `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{` |
-`plugin.go:729`
+`plugin.go:828`
### `InjectOptions`
@@ -424,7 +425,7 @@ CleanerName: 此次注入的内容用哪个**已注册的通道 cleaner** 清
而注入内容往往带 ANSI/JSON 包装,需要清洗后才是有效内容;
不指定就只能退到「按 source 查不到就不清洗」。
-`plugin.go:98`
+`plugin.go:132`
### `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{}):记入记忆、不裁剪。
-`plugin.go:684`
+`plugin.go:783`
### `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))` |
-`plugin.go:690`
+`plugin.go:789`
### `PluginSDK.InjectTextOpts`
@@ -462,7 +463,7 @@ func (s *PluginSDK) InjectTextOpts(source, channel, text string, opts InjectOpti
InjectTextOpts 注入文本到 agent,并在这一次注入上声明记忆与裁剪行为。
-`plugin.go:719`
+`plugin.go:818`
### `PriorityL1`
@@ -476,7 +477,7 @@ L1..L3 任何插件都可声明;**L4 只有内核级插件**(编译期内置
如 cli/webui/timer)才能声明——它用于实现真正的“立即打断”能力,
例如 WebUI 的终止按钮。外部插件(走 proc 桥)声明 L4 会被内核夹到 L3。
-`plugin.go:123`
+`plugin.go:161`
### `PriorityL2`
@@ -490,7 +491,7 @@ L1..L3 任何插件都可声明;**L4 只有内核级插件**(编译期内置
如 cli/webui/timer)才能声明——它用于实现真正的“立即打断”能力,
例如 WebUI 的终止按钮。外部插件(走 proc 桥)声明 L4 会被内核夹到 L3。
-`plugin.go:124`
+`plugin.go:162`
### `PriorityL3`
@@ -504,7 +505,7 @@ L1..L3 任何插件都可声明;**L4 只有内核级插件**(编译期内置
如 cli/webui/timer)才能声明——它用于实现真正的“立即打断”能力,
例如 WebUI 的终止按钮。外部插件(走 proc 桥)声明 L4 会被内核夹到 L3。
-`plugin.go:125`
+`plugin.go:163`
### `PriorityL4`
@@ -517,7 +518,7 @@ const PriorityL4
PriorityL4 仅内核级(内置)插件可用;外部插件声明会被夹到 L3。
-`plugin.go:127`
+`plugin.go:165`
### `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})` |
-`plugin.go:566`
+`plugin.go:665`
### `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{` |
-`plugin.go:533`
+`plugin.go:632`
### `PluginSDK.SetToolBlocks`
@@ -625,7 +626,7 @@ func (s *PluginSDK) SetToolBlocks(blocks []ContentBlock)
SetToolBlocks 在工具处理函数内注入多模态内容块,内核在下一条 tool message
的 content 数组里带上它们。需要「本轮就让模型看到」时用 InjectInputMedia。
-`plugin.go:777`
+`plugin.go:876`
### `ValidContextPolicy`
diff --git a/docs/api/events.md b/docs/api/events.md
index 7735675..06315f6 100644
--- a/docs/api/events.md
+++ b/docs/api/events.md
@@ -22,7 +22,7 @@ controls which events are delivered.
Subscribe(eventType EventType, handler EventHandler) func()
```
-`plugin.go:279`
+`plugin.go:378`
### `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.
-`plugin.go:265`
+`plugin.go:364`
### `EventHandler`
@@ -42,7 +42,7 @@ type EventHandler func(evt *Event)
EventHandler processes a system event.
-`plugin.go:273`
+`plugin.go:372`
### `EventType`
@@ -52,7 +52,7 @@ type EventType string
EventType identifies the kind of system event.
-`plugin.go:247`
+`plugin.go:346`
### `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).
-`plugin.go:451`
+`plugin.go:550`
diff --git a/docs/api/lifecycle.md b/docs/api/lifecycle.md
index f36a3bc..c43ebfc 100644
--- a/docs/api/lifecycle.md
+++ b/docs/api/lifecycle.md
@@ -57,7 +57,7 @@ IsPluginDisabled(name string) bool
IsPluginDisabled 查询插件是否被禁用。
-`plugin.go:290`
+`plugin.go:389`
### `PluginMgrAPI.ListLoadedPlugins`
@@ -67,7 +67,7 @@ ListLoadedPlugins() []string
ListLoadedPlugins 列出已加载插件。
-`plugin.go:288`
+`plugin.go:387`
### `PluginMgrAPI.ReloadOne`
@@ -77,7 +77,7 @@ ReloadOne(name string) error
ReloadOne 重载单个插件(停止后重新加载)。
-`plugin.go:286`
+`plugin.go:385`
### `PluginSDK.AutoRestart`
@@ -87,7 +87,7 @@ func (s *PluginSDK) AutoRestart() bool
AutoRestart 返回插件是否允许自动重启。
-`plugin.go:796`
+`plugin.go:895`
### `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.
-`plugin.go:658`
+`plugin.go:757`
### `PluginSDK.PluginName`
@@ -108,7 +108,7 @@ func (s *PluginSDK) PluginName() string
PluginName returns the name of the plugin.
-`plugin.go:402`
+`plugin.go:501`
### `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)` |
-`plugin.go:831`
+`plugin.go:930`
### `PluginSDK.RegisterPluginAPI`
@@ -139,7 +139,7 @@ func (s *PluginSDK) RegisterPluginAPI(name string) error
RegisterPluginAPI registers this plugin's API for access by other plugins.
-`plugin.go:507`
+`plugin.go:606`
### `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() })` |
-`plugin.go:806`
+`plugin.go:905`
### `PluginSDK.RunOnRemoveHandlers`
@@ -170,7 +170,7 @@ func (s *PluginSDK) RunOnRemoveHandlers()
RunOnRemoveHandlers 执行全部已注册的 onRemove handler(后注册先执行,执行后清空,幂等)。
由内核在卸载插件(registry.RemovePlugin)时、插件 Stop() 之后执行。
-`plugin.go:842`
+`plugin.go:941`
### `PluginSDK.RunStopHandlers`
@@ -181,7 +181,7 @@ func (s *PluginSDK) RunStopHandlers()
RunStopHandlers 执行全部已注册的 stop handler(后注册先执行,执行后清空,幂等)。
由内核(内置插件)或插件桥接层(外部插件 z_bridge 的 StopPlugin)在调用插件 Stop() 前执行。
-`plugin.go:817`
+`plugin.go:916`
### `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)` |
-`plugin.go:789`
+`plugin.go:888`
diff --git a/docs/api/llm.md b/docs/api/llm.md
index b255e12..f8a05da 100644
--- a/docs/api/llm.md
+++ b/docs/api/llm.md
@@ -46,5 +46,5 @@ func (s *PluginSDK) LLM() LLMAPI
LLM returns the LLM provider API (may be nil if not available).
-`plugin.go:437`
+`plugin.go:536`
diff --git a/docs/api/memory.md b/docs/api/memory.md
index 579e464..b0a8a14 100644
--- a/docs/api/memory.md
+++ b/docs/api/memory.md
@@ -236,7 +236,7 @@ func (s *PluginSDK) DocMemory() DocMemoryAPI
DocMemory returns the document memory API (may be nil if not available).
-`plugin.go:423`
+`plugin.go:522`
### `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"),根下条目为空。 // // 为何加这个字段:对�� …
```
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 {` |
-`plugin.go:430`
+`plugin.go:529`
### `MediaAttachment`
@@ -308,7 +308,7 @@ func (s *PluginSDK) Memory() MemoryAPI
Memory returns the graph memory API (may be nil if not available).
-`plugin.go:409`
+`plugin.go:508`
### `PersonProfile`
@@ -338,7 +338,7 @@ func (s *PluginSDK) Social() SocialAPI
Social returns the social graph API (may be nil if not available).
-`plugin.go:444`
+`plugin.go:543`
### `SocialRelation`
@@ -366,7 +366,7 @@ func (s *PluginSDK) TextMemory() TextMemoryAPI
TextMemory returns the text memory API (may be nil if not available).
-`plugin.go:416`
+`plugin.go:515`
### `Triple`
diff --git a/docs/api/misc.md b/docs/api/misc.md
index 7892bdf..e21e2e0 100644
--- a/docs/api/misc.md
+++ b/docs/api/misc.md
@@ -79,7 +79,7 @@ controls which events are delivered.
Subscribe(eventType EventType, handler EventHandler) func()
```
-`plugin.go:279`
+`plugin.go:378`
## `IOInjector`
@@ -110,7 +110,7 @@ for routing the agent's response.
InjectInputMedia(source, channel, text string, blocks []ContentBlock)
```
-`plugin.go:230`
+`plugin.go:329`
### `IOInjector.InjectInputMediaOpts`
@@ -118,7 +118,7 @@ InjectInputMedia(source, channel, text string, blocks []ContentBlock)
InjectInputMediaOpts(source, channel, text string, blocks []ContentBlock, opts InjectOptions)
```
-`plugin.go:241`
+`plugin.go:340`
### `IOInjector.InjectInputMediaSync`
@@ -126,7 +126,7 @@ InjectInputMediaOpts(source, channel, text string, blocks []ContentBlock, opts I
InjectInputMediaSync(source, channel, text string, blocks []ContentBlock) string
```
-`plugin.go:231`
+`plugin.go:330`
### `IOInjector.InjectInputMediaSyncOpts`
@@ -134,7 +134,7 @@ InjectInputMediaSync(source, channel, text string, blocks []ContentBlock) string
InjectInputMediaSyncOpts(source, channel, text string, blocks []ContentBlock, opts InjectOptions) string
```
-`plugin.go:242`
+`plugin.go:341`
### `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,` |
-`plugin.go:226`
+`plugin.go:325`
### `IOInjector.InjectInputSyncOpts`
@@ -160,7 +160,7 @@ InjectInputSync 注入输入事件并同步等待 agent 回复,返回回复文
InjectInputSyncOpts(source, channel, text string, opts InjectOptions) string
```
-`plugin.go:240`
+`plugin.go:339`
### `IOInjector.InjectInterruptMedia`
@@ -168,7 +168,7 @@ InjectInputSyncOpts(source, channel, text string, opts InjectOptions) string
InjectInterruptMedia(source, channel, text string, blocks []ContentBlock)
```
-`plugin.go:232`
+`plugin.go:331`
### `IOInjector.InjectInterruptMediaOpts`
@@ -176,7 +176,7 @@ InjectInterruptMedia(source, channel, text string, blocks []ContentBlock)
InjectInterruptMediaOpts(source, channel, text string, blocks []ContentBlock, opts InjectOptions)
```
-`plugin.go:243`
+`plugin.go:342`
### `IOInjector.InjectInterruptText`
@@ -184,7 +184,7 @@ InjectInterruptMediaOpts(source, channel, text string, blocks []ContentBlock, op
InjectInterruptText(source, channel, text string)
```
-`plugin.go:221`
+`plugin.go:320`
### `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{` |
-`plugin.go:239`
+`plugin.go:338`
### `IOInjector.InjectText`
@@ -209,7 +209,7 @@ InjectInterruptTextOpts(source, channel, text string, opts InjectOptions)
InjectText(source, channel, text string)
```
-`plugin.go:222`
+`plugin.go:321`
### `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))` |
-`plugin.go:223`
+`plugin.go:322`
### `IOInjector.InjectTextOpts`
@@ -236,7 +236,7 @@ InjectTextOpts(source, channel, text string, opts InjectOptions)
上面那些不带 opts 的方法等价于传零值 InjectOptions(记入记忆 + 不裁剪),
保留它们是为了不破坏已有插件;新代码应当用 Opts 变体把意图写清楚。
-`plugin.go:238`
+`plugin.go:337`
### `IOInjector.SetToolBlocks`
@@ -247,7 +247,7 @@ SetToolBlocks(blocks []ContentBlock)
SetToolBlocks 插件工具注入多模态内容块(image_url/audio_url),内核在下一条
tool message 的 content 数组里带上这些块,让模型在后续轮次看到图/听到音频。
-`plugin.go:229`
+`plugin.go:328`
## `KnowledgeAPI`
@@ -422,7 +422,7 @@ IsPluginDisabled(name string) bool
IsPluginDisabled 查询插件是否被禁用。
-`plugin.go:290`
+`plugin.go:389`
### `PluginMgrAPI.ListLoadedPlugins`
@@ -432,7 +432,7 @@ ListLoadedPlugins() []string
ListLoadedPlugins 列出已加载插件。
-`plugin.go:288`
+`plugin.go:387`
### `PluginMgrAPI.ReloadOne`
@@ -442,7 +442,7 @@ ReloadOne(name string) error
ReloadOne 重载单个插件(停止后重新加载)。
-`plugin.go:286`
+`plugin.go:385`
## `SettingsAPI`
@@ -680,7 +680,7 @@ Append(evt TextEvent) error
type AudioURL struct { URL string `json:"url"` }
```
-`plugin.go:867`
+`plugin.go:966`
### `EffectiveProxyAuth`
@@ -692,13 +692,32 @@ EffectiveProxyAuth 返回生效的鉴权模式(空串归一化为 ProxyAuthHom
`proxy.go:169`
+### `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") {` |
+
+`plugin.go:264`
+
### `ImageURL`
```go
type ImageURL struct { URL string `json:"url"` Detail string `json:"detail,omitempty"` }
```
-`plugin.go:862`
+`plugin.go:961`
### `MemItem`
@@ -708,7 +727,7 @@ type MemItem struct { Role string `json:"role"` Content string `json:"content"`
MemItem represents a memory item in stage context.
-`plugin.go:179`
+`plugin.go:220`
### `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.
-`plugin.go:337`
+`plugin.go:436`
### `ProxyAuthHomeAgent`
@@ -877,6 +896,64 @@ SDKVersion 是对外暴露的 SDK 版本号。
`plugin.go:10`
+### `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 保持开,声明项只作为插件
+将来确实需要时的闸门。
+
+`plugin.go:98`
+
+### `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 保持开,声明项只作为插件
+将来确实需要时的闸门。
+
+`plugin.go:99`
+
### `PluginSDK.SetProxyRegistrar`
```go
@@ -887,6 +964,24 @@ SetProxyRegistrar 由内核注入。插件不直接调它(与 SetInputChannelR
`proxy.go:309`
+### `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。
+本类型是给**新写**的工具用的可选项,不是迁移要求。
+
+`plugin.go:252`
+
### `PluginSDK.UnregisterOutputChannel`
!!! warning "仅内核内置插件可用"
@@ -898,7 +993,7 @@ func (s *PluginSDK) UnregisterOutputChannel(name string) error
UnregisterOutputChannel 注销一个输出通道(动态通道随资源生灭时必须调用)。
-`plugin.go:544`
+`plugin.go:643`
### `ValidProxyAuth`
@@ -923,6 +1018,16 @@ ValidProxyHostLabel 校验子域名标签是否合法(DNS label 规则)。
`proxy.go:180`
+### `ValidScenePolicy`
+
+```go
+func ValidScenePolicy(policy string) bool
+```
+
+ValidScenePolicy 校验场面策略取值;空串等价于 ScenePolicyAuto。
+
+`plugin.go:103`
+
### `ValidateProxyDef`
```go
diff --git a/docs/api/settings.md b/docs/api/settings.md
index c3fdd70..62403bd 100644
--- a/docs/api/settings.md
+++ b/docs/api/settings.md
@@ -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{` |
-`plugin.go:406`
+`plugin.go:505`
diff --git a/docs/api/stages.md b/docs/api/stages.md
index 69d7116..779d649 100644
--- a/docs/api/stages.md
+++ b/docs/api/stages.md
@@ -10,7 +10,7 @@
func (c *StageContext) IsResponded() bool
```
-`plugin.go:172`
+`plugin.go:213`
### `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()` |
-`plugin.go:170`
+`plugin.go:211`
### `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()` |
-`plugin.go:168`
+`plugin.go:209`
### `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()` |
-`plugin.go:169`
+`plugin.go:210`
### `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 {` |
-`plugin.go:472`
+`plugin.go:571`
### `Stage`
@@ -103,7 +103,7 @@ type StageContext struct { mu sync.RWMutex RawMessage string UserID string Group
StageContext provides context for stage handlers.
-`plugin.go:148`
+`plugin.go:189`
### `StageHandler`
@@ -123,7 +123,7 @@ type StageRegistrar func(stage Stage, handler StageHandler)
StageRegistrar registers a stage handler.
-`plugin.go:308`
+`plugin.go:407`
### `StageScope`
@@ -133,7 +133,7 @@ type StageScope int
StageScope controls which events a stage handler receives.
-`plugin.go:294`
+`plugin.go:393`
### `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()` |
-`plugin.go:171`
+`plugin.go:212`
diff --git a/docs/api/tools.md b/docs/api/tools.md
index a47f98e..e27d76b 100644
--- a/docs/api/tools.md
+++ b/docs/api/tools.md
@@ -14,7 +14,7 @@ ContentBlock 是多模态内容块(OpenAI 格式:text/image_url/audio_url)
插件工具返回结果时可用 PluginSDK.SetToolBlocks 注入,让下一轮 LLM
请求在 tool message 的 content 数组里带上图片/音频,实现"模型看图/听音频"。
-`plugin.go:855`
+`plugin.go:954`
### `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{` |
-`plugin.go:458`
+`plugin.go:557`
### `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.
-`plugin.go:186`
+`plugin.go:227`
### `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.
-`plugin.go:203`
+`plugin.go:279`
### `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.
-`plugin.go:194`
+`plugin.go:235`
diff --git a/docs/assets/api-index.json b/docs/assets/api-index.json
index dbf17c2..2e08660 100644
--- a/docs/assets/api-index.json
+++ b/docs/assets/api-index.json
@@ -8,7 +8,7 @@
"p": "bridge",
"b": false,
"f": "plugin.go",
- "l": 311
+ "l": 410
},
{
"n": "Add",
@@ -47,7 +47,7 @@
"p": "misc",
"b": false,
"f": "plugin.go",
- "l": 867
+ "l": 966
},
{
"n": "AutoRestart",
@@ -58,7 +58,7 @@
"p": "lifecycle",
"b": false,
"f": "plugin.go",
- "l": 796,
+ "l": 895,
"g": [
"自动重启",
"崩溃自愈",
@@ -75,7 +75,7 @@
"p": "channels",
"b": false,
"f": "plugin.go",
- "l": 331,
+ "l": 430,
"g": [
"音频能力"
]
@@ -89,7 +89,7 @@
"p": "channels",
"b": false,
"f": "plugin.go",
- "l": 329,
+ "l": 428,
"g": [
"文件能力"
]
@@ -103,7 +103,7 @@
"p": "channels",
"b": false,
"f": "plugin.go",
- "l": 330,
+ "l": 429,
"g": [
"图片能力"
]
@@ -117,7 +117,7 @@
"p": "channels",
"b": false,
"f": "plugin.go",
- "l": 332,
+ "l": 431,
"g": [
"结构化能力"
]
@@ -131,7 +131,7 @@
"p": "channels",
"b": false,
"f": "plugin.go",
- "l": 328,
+ "l": 427,
"g": [
"文本能力"
]
@@ -145,7 +145,7 @@
"p": "channels",
"b": false,
"f": "plugin.go",
- "l": 139,
+ "l": 178,
"g": [
"通道",
"频道",
@@ -190,7 +190,7 @@
"p": "tools",
"b": false,
"f": "plugin.go",
- "l": 855,
+ "l": 954,
"g": [
"多模态内容块",
"图片块",
@@ -295,7 +295,7 @@
"p": "memory",
"b": false,
"f": "plugin.go",
- "l": 423,
+ "l": 522,
"g": [
"记忆",
"内存",
@@ -353,6 +353,17 @@
"记忆主体"
]
},
+ {
+ "n": "Error",
+ "s": "func (e *ToolError) Error() string",
+ "d": "Error 实现 error,便于工具同时走 (ToolError, error) 通道。",
+ "k": "method",
+ "r": "ToolError",
+ "p": "misc",
+ "b": false,
+ "f": "plugin.go",
+ "l": 264
+ },
{
"n": "Event",
"s": "type Event struct { Type EventType `json:\"type\"` Source string `json:\"source\"` Payload map[string]interface{} `json:\"payload\"` T …",
@@ -362,7 +373,7 @@
"p": "events",
"b": false,
"f": "plugin.go",
- "l": 265,
+ "l": 364,
"g": [
"事件",
"订阅"
@@ -377,7 +388,7 @@
"p": "constants",
"b": false,
"f": "plugin.go",
- "l": 252,
+ "l": 351,
"g": [
"事件",
"订阅"
@@ -392,7 +403,7 @@
"p": "constants",
"b": false,
"f": "plugin.go",
- "l": 251,
+ "l": 350,
"g": [
"事件",
"订阅"
@@ -407,7 +418,7 @@
"p": "constants",
"b": false,
"f": "plugin.go",
- "l": 261,
+ "l": 360,
"g": [
"事件",
"订阅"
@@ -422,7 +433,7 @@
"p": "events",
"b": false,
"f": "plugin.go",
- "l": 273,
+ "l": 372,
"g": [
"事件",
"订阅"
@@ -437,7 +448,7 @@
"p": "constants",
"b": false,
"f": "plugin.go",
- "l": 250,
+ "l": 349,
"g": [
"事件",
"订阅"
@@ -452,7 +463,7 @@
"p": "constants",
"b": false,
"f": "plugin.go",
- "l": 254,
+ "l": 353,
"g": [
"事件",
"订阅"
@@ -467,7 +478,7 @@
"p": "constants",
"b": false,
"f": "plugin.go",
- "l": 260,
+ "l": 359,
"g": [
"事件",
"订阅"
@@ -482,7 +493,7 @@
"p": "constants",
"b": false,
"f": "plugin.go",
- "l": 255,
+ "l": 354,
"g": [
"事件",
"订阅",
@@ -510,7 +521,7 @@
"p": "constants",
"b": false,
"f": "plugin.go",
- "l": 256,
+ "l": 355,
"g": [
"事件",
"订阅"
@@ -525,7 +536,7 @@
"p": "constants",
"b": false,
"f": "plugin.go",
- "l": 253,
+ "l": 352,
"g": [
"事件",
"订阅",
@@ -541,7 +552,7 @@
"p": "events",
"b": false,
"f": "plugin.go",
- "l": 247,
+ "l": 346,
"g": [
"事件",
"订阅"
@@ -556,7 +567,7 @@
"p": "events",
"b": true,
"f": "plugin.go",
- "l": 451,
+ "l": 550,
"g": [
"事件",
"订阅",
@@ -682,18 +693,18 @@
"p": "misc",
"b": false,
"f": "plugin.go",
- "l": 862
+ "l": 961
},
{
"n": "InjectInputMedia",
- "s": "func (s *PluginSDK) InjectInputMedia(source, channel, text string, blocks []ContentBlock)",
- "d": "InjectInputMedia 注入带媒体内容块(image_url/audio_url)的输入。",
+ "s": "InjectInputMedia(source, channel, text string, blocks []ContentBlock)",
+ "d": "",
"k": "method",
- "r": "PluginSDK",
+ "r": "IOInjector",
"p": "channels",
"b": false,
"f": "plugin.go",
- "l": 706,
+ "l": 329,
"g": [
"注入输入",
"投喂输入",
@@ -705,14 +716,14 @@
},
{
"n": "InjectInputMedia",
- "s": "InjectInputMedia(source, channel, text string, blocks []ContentBlock)",
- "d": "",
+ "s": "func (s *PluginSDK) InjectInputMedia(source, channel, text string, blocks []ContentBlock)",
+ "d": "InjectInputMedia 注入带媒体内容块(image_url/audio_url)的输入。",
"k": "method",
- "r": "IOInjector",
+ "r": "PluginSDK",
"p": "channels",
"b": false,
"f": "plugin.go",
- "l": 230,
+ "l": 805,
"g": [
"注入输入",
"投喂输入",
@@ -731,7 +742,7 @@
"p": "channels",
"b": false,
"f": "plugin.go",
- "l": 241,
+ "l": 340,
"g": [
"注入输入",
"投喂输入",
@@ -750,7 +761,7 @@
"p": "channels",
"b": false,
"f": "plugin.go",
- "l": 745,
+ "l": 844,
"g": [
"注入输入",
"投喂输入",
@@ -769,7 +780,7 @@
"p": "channels",
"b": false,
"f": "plugin.go",
- "l": 231,
+ "l": 330,
"g": [
"注入输入",
"投喂输入",
@@ -788,26 +799,7 @@
"p": "channels",
"b": false,
"f": "plugin.go",
- "l": 712,
- "g": [
- "注入输入",
- "投喂输入",
- "模拟用户输入",
- "媒体",
- "图片",
- "音频"
- ]
- },
- {
- "n": "InjectInputMediaSyncOpts",
- "s": "func (s *PluginSDK) InjectInputMediaSyncOpts(source, channel, text string, blocks []ContentBlock, opts InjectOptions) string",
- "d": "InjectInputMediaSyncOpts 注入带媒体块的输入并同步等待回复,同时声明记忆/裁剪行为。",
- "k": "method",
- "r": "PluginSDK",
- "p": "channels",
- "b": false,
- "f": "plugin.go",
- "l": 752,
+ "l": 811,
"g": [
"注入输入",
"投喂输入",
@@ -826,7 +818,26 @@
"p": "channels",
"b": false,
"f": "plugin.go",
- "l": 242,
+ "l": 341,
+ "g": [
+ "注入输入",
+ "投喂输入",
+ "模拟用户输入",
+ "媒体",
+ "图片",
+ "音频"
+ ]
+ },
+ {
+ "n": "InjectInputMediaSyncOpts",
+ "s": "func (s *PluginSDK) InjectInputMediaSyncOpts(source, channel, text string, blocks []ContentBlock, opts InjectOptions) string",
+ "d": "InjectInputMediaSyncOpts 注入带媒体块的输入并同步等待回复,同时声明记忆/裁剪行为。",
+ "k": "method",
+ "r": "PluginSDK",
+ "p": "channels",
+ "b": false,
+ "f": "plugin.go",
+ "l": 851,
"g": [
"注入输入",
"投喂输入",
@@ -845,7 +856,7 @@
"p": "channels",
"b": false,
"f": "plugin.go",
- "l": 226,
+ "l": 325,
"g": [
"注入输入",
"投喂输入",
@@ -861,23 +872,7 @@
"p": "channels",
"b": false,
"f": "plugin.go",
- "l": 697,
- "g": [
- "注入输入",
- "投喂输入",
- "模拟用户输入"
- ]
- },
- {
- "n": "InjectInputSyncOpts",
- "s": "InjectInputSyncOpts(source, channel, text string, opts InjectOptions) string",
- "d": "",
- "k": "method",
- "r": "IOInjector",
- "p": "channels",
- "b": false,
- "f": "plugin.go",
- "l": 240,
+ "l": 796,
"g": [
"注入输入",
"投喂输入",
@@ -893,7 +888,23 @@
"p": "channels",
"b": false,
"f": "plugin.go",
- "l": 736,
+ "l": 835,
+ "g": [
+ "注入输入",
+ "投喂输入",
+ "模拟用户输入"
+ ]
+ },
+ {
+ "n": "InjectInputSyncOpts",
+ "s": "InjectInputSyncOpts(source, channel, text string, opts InjectOptions) string",
+ "d": "",
+ "k": "method",
+ "r": "IOInjector",
+ "p": "channels",
+ "b": false,
+ "f": "plugin.go",
+ "l": 339,
"g": [
"注入输入",
"投喂输入",
@@ -909,7 +920,7 @@
"p": "channels",
"b": false,
"f": "plugin.go",
- "l": 232,
+ "l": 331,
"g": [
"中断注入",
"插话",
@@ -929,7 +940,7 @@
"p": "channels",
"b": false,
"f": "plugin.go",
- "l": 769,
+ "l": 868,
"g": [
"中断注入",
"插话",
@@ -949,7 +960,7 @@
"p": "channels",
"b": false,
"f": "plugin.go",
- "l": 243,
+ "l": 342,
"g": [
"中断注入",
"插话",
@@ -969,7 +980,7 @@
"p": "channels",
"b": false,
"f": "plugin.go",
- "l": 761,
+ "l": 860,
"g": [
"中断注入",
"插话",
@@ -989,7 +1000,7 @@
"p": "channels",
"b": false,
"f": "plugin.go",
- "l": 221,
+ "l": 320,
"g": [
"中断注入",
"插话",
@@ -1006,7 +1017,7 @@
"p": "channels",
"b": false,
"f": "plugin.go",
- "l": 678,
+ "l": 777,
"g": [
"中断注入",
"插话",
@@ -1023,7 +1034,7 @@
"p": "channels",
"b": false,
"f": "plugin.go",
- "l": 239,
+ "l": 338,
"g": [
"中断注入",
"插话",
@@ -1040,7 +1051,7 @@
"p": "channels",
"b": false,
"f": "plugin.go",
- "l": 729,
+ "l": 828,
"g": [
"中断注入",
"插话",
@@ -1057,30 +1068,13 @@
"p": "channels",
"b": false,
"f": "plugin.go",
- "l": 98,
+ "l": 132,
"g": [
"注入选项",
"是否记入记忆",
"是否裁剪上下文"
]
},
- {
- "n": "InjectText",
- "s": "InjectText(source, channel, text string)",
- "d": "",
- "k": "method",
- "r": "IOInjector",
- "p": "channels",
- "b": false,
- "f": "plugin.go",
- "l": 222,
- "g": [
- "注入文本",
- "注入消息",
- "投喂输入",
- "灌入内容"
- ]
- },
{
"n": "InjectText",
"s": "func (s *PluginSDK) InjectText(source, channel, text string)",
@@ -1090,7 +1084,24 @@
"p": "channels",
"b": false,
"f": "plugin.go",
- "l": 684,
+ "l": 783,
+ "g": [
+ "注入文本",
+ "注入消息",
+ "投喂输入",
+ "灌入内容"
+ ]
+ },
+ {
+ "n": "InjectText",
+ "s": "InjectText(source, channel, text string)",
+ "d": "",
+ "k": "method",
+ "r": "IOInjector",
+ "p": "channels",
+ "b": false,
+ "f": "plugin.go",
+ "l": 321,
"g": [
"注入文本",
"注入消息",
@@ -1107,7 +1118,7 @@
"p": "channels",
"b": false,
"f": "plugin.go",
- "l": 223,
+ "l": 322,
"g": [
"注入文本",
"注入消息",
@@ -1126,7 +1137,7 @@
"p": "channels",
"b": false,
"f": "plugin.go",
- "l": 690,
+ "l": 789,
"g": [
"注入文本",
"注入消息",
@@ -1138,14 +1149,14 @@
},
{
"n": "InjectTextOpts",
- "s": "func (s *PluginSDK) InjectTextOpts(source, channel, text string, opts InjectOptions)",
- "d": "InjectTextOpts 注入文本到 agent,并在这一次注入上声明记忆与裁剪行为。",
+ "s": "InjectTextOpts(source, channel, text string, opts InjectOptions)",
+ "d": "以下 Opts 变体让调用点在**这一次注入**上声明记忆与裁剪行为。",
"k": "method",
- "r": "PluginSDK",
+ "r": "IOInjector",
"p": "channels",
"b": false,
"f": "plugin.go",
- "l": 719,
+ "l": 337,
"g": [
"注入文本",
"注入消息",
@@ -1155,14 +1166,14 @@
},
{
"n": "InjectTextOpts",
- "s": "InjectTextOpts(source, channel, text string, opts InjectOptions)",
- "d": "以下 Opts 变体让调用点在**这一次注入**上声明记忆与裁剪行为。",
+ "s": "func (s *PluginSDK) InjectTextOpts(source, channel, text string, opts InjectOptions)",
+ "d": "InjectTextOpts 注入文本到 agent,并在这一次注入上声明记忆与裁剪行为。",
"k": "method",
- "r": "IOInjector",
+ "r": "PluginSDK",
"p": "channels",
"b": false,
"f": "plugin.go",
- "l": 238,
+ "l": 818,
"g": [
"注入文本",
"注入消息",
@@ -1179,7 +1190,7 @@
"p": "bridge",
"b": false,
"f": "plugin.go",
- "l": 314,
+ "l": 413,
"g": [
"通道",
"频道"
@@ -1240,7 +1251,7 @@
"p": "lifecycle",
"b": false,
"f": "plugin.go",
- "l": 290,
+ "l": 389,
"g": [
"查插件是否被禁用"
]
@@ -1254,7 +1265,7 @@
"p": "stages",
"b": false,
"f": "plugin.go",
- "l": 172
+ "l": 213
},
{
"n": "Knowledge",
@@ -1265,7 +1276,7 @@
"p": "memory",
"b": false,
"f": "plugin.go",
- "l": 430,
+ "l": 529,
"g": [
"知识库",
"知识"
@@ -1273,7 +1284,7 @@
},
{
"n": "Knowledge",
- "s": "type Knowledge struct { Name string `json:\"name\"` Content string `json:\"content\"` }",
+ "s": "type Knowledge struct { Name string `json:\"name\"` // Category 是该条目的父分类路径(如 \"tech/go\"),根下条目为空。 // // 为何加这个字段:对�� …",
"d": "Knowledge represents a knowledge entry.",
"k": "type",
"r": "",
@@ -1306,7 +1317,7 @@
"p": "llm",
"b": false,
"f": "plugin.go",
- "l": 437
+ "l": 536
},
{
"n": "LLMAPI",
@@ -1370,7 +1381,7 @@
"p": "lifecycle",
"b": false,
"f": "plugin.go",
- "l": 288,
+ "l": 387,
"g": [
"已加载插件列表"
]
@@ -1426,7 +1437,7 @@
"p": "stages",
"b": false,
"f": "plugin.go",
- "l": 170
+ "l": 211
},
{
"n": "MediaAttachment",
@@ -1454,7 +1465,7 @@
"p": "misc",
"b": false,
"f": "plugin.go",
- "l": 179
+ "l": 220
},
{
"n": "Memory",
@@ -1465,7 +1476,7 @@
"p": "memory",
"b": false,
"f": "plugin.go",
- "l": 409,
+ "l": 508,
"g": [
"记忆",
"内存"
@@ -1530,7 +1541,7 @@
"p": "bridge",
"b": false,
"f": "plugin.go",
- "l": 317,
+ "l": 416,
"g": [
"通道",
"频道"
@@ -1545,7 +1556,7 @@
"p": "bridge",
"b": false,
"f": "plugin.go",
- "l": 324,
+ "l": 423,
"g": [
"通道",
"频道"
@@ -1585,7 +1596,7 @@
"p": "lifecycle",
"b": false,
"f": "plugin.go",
- "l": 658,
+ "l": 757,
"g": [
"插件管理",
"重载插件",
@@ -1613,7 +1624,7 @@
"p": "lifecycle",
"b": false,
"f": "plugin.go",
- "l": 402,
+ "l": 501,
"g": [
"插件名"
]
@@ -1627,7 +1638,7 @@
"p": "misc",
"b": false,
"f": "plugin.go",
- "l": 337
+ "l": 436
},
{
"n": "Plugins",
@@ -1652,7 +1663,7 @@
"p": "channels",
"b": false,
"f": "plugin.go",
- "l": 123,
+ "l": 161,
"g": [
"优先级",
"中断级别"
@@ -1667,7 +1678,7 @@
"p": "channels",
"b": false,
"f": "plugin.go",
- "l": 124,
+ "l": 162,
"g": [
"优先级",
"中断级别"
@@ -1682,7 +1693,7 @@
"p": "channels",
"b": false,
"f": "plugin.go",
- "l": 125,
+ "l": 163,
"g": [
"优先级",
"中断级别"
@@ -1697,7 +1708,7 @@
"p": "channels",
"b": true,
"f": "plugin.go",
- "l": 127,
+ "l": 165,
"g": [
"优先级",
"中断级别"
@@ -1786,7 +1797,7 @@
"p": "stages",
"b": false,
"f": "plugin.go",
- "l": 168
+ "l": 209
},
{
"n": "RUnlock",
@@ -1797,7 +1808,7 @@
"p": "stages",
"b": false,
"f": "plugin.go",
- "l": 169
+ "l": 210
},
{
"n": "Recall",
@@ -1877,7 +1888,7 @@
"p": "channels",
"b": false,
"f": "plugin.go",
- "l": 566,
+ "l": 665,
"g": [
"注册输入通道",
"收消息",
@@ -1896,7 +1907,7 @@
"p": "lifecycle",
"b": false,
"f": "plugin.go",
- "l": 831,
+ "l": 930,
"g": [
"卸载回调",
"删除清理",
@@ -1913,7 +1924,7 @@
"p": "channels",
"b": false,
"f": "plugin.go",
- "l": 533,
+ "l": 632,
"g": [
"注册输出通道",
"发消息",
@@ -1932,7 +1943,7 @@
"p": "lifecycle",
"b": false,
"f": "plugin.go",
- "l": 507,
+ "l": 606,
"g": [
"注册插件接口",
"暴露接口"
@@ -1958,7 +1969,7 @@
"p": "stages",
"b": false,
"f": "plugin.go",
- "l": 472,
+ "l": 571,
"g": [
"注册阶段",
"阶段钩子",
@@ -1978,7 +1989,7 @@
"p": "lifecycle",
"b": false,
"f": "plugin.go",
- "l": 806,
+ "l": 905,
"g": [
"停止回调",
"关闭清理",
@@ -1994,7 +2005,7 @@
"p": "tools",
"b": false,
"f": "plugin.go",
- "l": 458,
+ "l": 557,
"g": [
"注册工具",
"添加工具",
@@ -2031,7 +2042,7 @@
"p": "lifecycle",
"b": false,
"f": "plugin.go",
- "l": 286,
+ "l": 385,
"g": [
"重载单个插件"
]
@@ -2059,7 +2070,7 @@
"p": "lifecycle",
"b": false,
"f": "plugin.go",
- "l": 842
+ "l": 941
},
{
"n": "RunStopHandlers",
@@ -2070,7 +2081,7 @@
"p": "lifecycle",
"b": false,
"f": "plugin.go",
- "l": 817
+ "l": 916
},
{
"n": "SDKVersion",
@@ -2086,6 +2097,34 @@
"SDK 版本"
]
},
+ {
+ "n": "ScenePolicyAuto",
+ "s": "const ScenePolicyAuto",
+ "d": "场面策略:决定一次输入是否参与**场面识别**(场景式记忆)。",
+ "k": "const",
+ "r": "",
+ "p": "misc",
+ "b": false,
+ "f": "plugin.go",
+ "l": 98,
+ "g": [
+ "策略"
+ ]
+ },
+ {
+ "n": "ScenePolicyNone",
+ "s": "const ScenePolicyNone",
+ "d": "场面策略:决定一次输入是否参与**场面识别**(场景式记忆)。",
+ "k": "const",
+ "r": "",
+ "p": "misc",
+ "b": false,
+ "f": "plugin.go",
+ "l": 99,
+ "g": [
+ "策略"
+ ]
+ },
{
"n": "Search",
"s": "Search(query string, topK int) ([]*Knowledge, error)",
@@ -2125,7 +2164,7 @@
"p": "lifecycle",
"b": false,
"f": "plugin.go",
- "l": 789,
+ "l": 888,
"g": [
"设置",
"装配",
@@ -2161,7 +2200,7 @@
"p": "bridge",
"b": false,
"f": "plugin.go",
- "l": 619,
+ "l": 718,
"g": [
"设置",
"装配",
@@ -2179,7 +2218,7 @@
"p": "bridge",
"b": true,
"f": "plugin.go",
- "l": 643,
+ "l": 742,
"g": [
"设置",
"装配",
@@ -2196,7 +2235,7 @@
"p": "bridge",
"b": false,
"f": "plugin.go",
- "l": 600,
+ "l": 699,
"g": [
"设置",
"装配"
@@ -2211,7 +2250,7 @@
"p": "bridge",
"b": false,
"f": "plugin.go",
- "l": 593,
+ "l": 692,
"g": [
"设置",
"装配",
@@ -2228,7 +2267,7 @@
"p": "bridge",
"b": false,
"f": "plugin.go",
- "l": 625,
+ "l": 724,
"g": [
"设置",
"装配",
@@ -2245,7 +2284,7 @@
"p": "bridge",
"b": false,
"f": "plugin.go",
- "l": 631,
+ "l": 730,
"g": [
"设置",
"装配"
@@ -2260,7 +2299,7 @@
"p": "bridge",
"b": false,
"f": "plugin.go",
- "l": 607,
+ "l": 706,
"g": [
"设置",
"装配",
@@ -2277,7 +2316,7 @@
"p": "bridge",
"b": false,
"f": "plugin.go",
- "l": 579,
+ "l": 678,
"g": [
"设置",
"装配",
@@ -2294,7 +2333,7 @@
"p": "bridge",
"b": true,
"f": "plugin.go",
- "l": 586,
+ "l": 685,
"g": [
"设置",
"装配",
@@ -2327,7 +2366,7 @@
"p": "bridge",
"b": false,
"f": "plugin.go",
- "l": 650,
+ "l": 749,
"g": [
"设置",
"装配",
@@ -2360,7 +2399,7 @@
"p": "bridge",
"b": false,
"f": "plugin.go",
- "l": 637,
+ "l": 736,
"g": [
"设置",
"装配",
@@ -2394,7 +2433,7 @@
"p": "bridge",
"b": false,
"f": "plugin.go",
- "l": 613,
+ "l": 712,
"g": [
"设置",
"装配",
@@ -2402,25 +2441,6 @@
"内存"
]
},
- {
- "n": "SetToolBlocks",
- "s": "SetToolBlocks(blocks []ContentBlock)",
- "d": "SetToolBlocks 插件工具注入多模态内容块(image_url/audio_url),内核在下一条",
- "k": "method",
- "r": "IOInjector",
- "p": "channels",
- "b": false,
- "f": "plugin.go",
- "l": 229,
- "g": [
- "设置",
- "装配",
- "工具",
- "工具返回图片",
- "多模态返回",
- "让模型看图"
- ]
- },
{
"n": "SetToolBlocks",
"s": "func (s *PluginSDK) SetToolBlocks(blocks []ContentBlock)",
@@ -2430,7 +2450,26 @@
"p": "channels",
"b": false,
"f": "plugin.go",
- "l": 777,
+ "l": 876,
+ "g": [
+ "设置",
+ "装配",
+ "工具",
+ "工具返回图片",
+ "多模态返回",
+ "让模型看图"
+ ]
+ },
+ {
+ "n": "SetToolBlocks",
+ "s": "SetToolBlocks(blocks []ContentBlock)",
+ "d": "SetToolBlocks 插件工具注入多模态内容块(image_url/audio_url),内核在下一条",
+ "k": "method",
+ "r": "IOInjector",
+ "p": "channels",
+ "b": false,
+ "f": "plugin.go",
+ "l": 328,
"g": [
"设置",
"装配",
@@ -2449,7 +2488,7 @@
"p": "settings",
"b": false,
"f": "plugin.go",
- "l": 406,
+ "l": 505,
"g": [
"设置",
"装配"
@@ -2475,7 +2514,7 @@
"p": "memory",
"b": false,
"f": "plugin.go",
- "l": 444,
+ "l": 543,
"g": [
"社交",
"关系",
@@ -2598,7 +2637,7 @@
"p": "stages",
"b": false,
"f": "plugin.go",
- "l": 148,
+ "l": 189,
"g": [
"阶段",
"钩子",
@@ -2676,7 +2715,7 @@
"p": "stages",
"b": false,
"f": "plugin.go",
- "l": 308,
+ "l": 407,
"g": [
"阶段",
"钩子"
@@ -2691,7 +2730,7 @@
"p": "stages",
"b": false,
"f": "plugin.go",
- "l": 294,
+ "l": 393,
"g": [
"阶段",
"钩子",
@@ -2708,7 +2747,7 @@
"p": "constants",
"b": false,
"f": "plugin.go",
- "l": 298,
+ "l": 397,
"g": [
"阶段",
"钩子"
@@ -2723,7 +2762,7 @@
"p": "constants",
"b": false,
"f": "plugin.go",
- "l": 301,
+ "l": 400,
"g": [
"阶段",
"钩子",
@@ -2782,7 +2821,7 @@
"p": "events",
"b": true,
"f": "plugin.go",
- "l": 279
+ "l": 378
},
{
"n": "TextEvent",
@@ -2808,7 +2847,7 @@
"p": "memory",
"b": false,
"f": "plugin.go",
- "l": 416,
+ "l": 515,
"g": [
"记忆",
"内存"
@@ -2834,7 +2873,7 @@
"p": "tools",
"b": false,
"f": "plugin.go",
- "l": 186,
+ "l": 227,
"g": [
"工具",
"工具调用"
@@ -2849,7 +2888,7 @@
"p": "tools",
"b": false,
"f": "plugin.go",
- "l": 203,
+ "l": 279,
"g": [
"工具",
"工具定义",
@@ -2857,6 +2896,20 @@
"参数表"
]
},
+ {
+ "n": "ToolError",
+ "s": "type ToolError struct { // Field 是出错的参数字段名(参数校验失败时填)。 Field string `json:\"field,omitempty\"` // Reason 是机器可读的原因码: …",
+ "d": "ToolError 描述一次工具调用的失败原因。",
+ "k": "type",
+ "r": "",
+ "p": "misc",
+ "b": false,
+ "f": "plugin.go",
+ "l": 252,
+ "g": [
+ "工具"
+ ]
+ },
{
"n": "ToolHandler",
"s": "type ToolHandler func(args map[string]interface{}) (interface{}, error)",
@@ -2882,7 +2935,7 @@
"p": "bridge",
"b": false,
"f": "plugin.go",
- "l": 305,
+ "l": 404,
"g": [
"工具"
]
@@ -2896,7 +2949,7 @@
"p": "tools",
"b": false,
"f": "plugin.go",
- "l": 194,
+ "l": 235,
"g": [
"工具",
"工具结果",
@@ -2926,7 +2979,7 @@
"p": "stages",
"b": false,
"f": "plugin.go",
- "l": 171
+ "l": 212
},
{
"n": "UnregisterOutputChannel",
@@ -2937,7 +2990,7 @@
"p": "misc",
"b": true,
"f": "plugin.go",
- "l": 544,
+ "l": 643,
"g": [
"通道",
"频道",
@@ -2999,6 +3052,20 @@
"校验召回策略"
]
},
+ {
+ "n": "ValidScenePolicy",
+ "s": "func ValidScenePolicy(policy string) bool",
+ "d": "ValidScenePolicy 校验场面策略取值;空串等价于 ScenePolicyAuto。",
+ "k": "func",
+ "r": "",
+ "p": "misc",
+ "b": false,
+ "f": "plugin.go",
+ "l": 103,
+ "g": [
+ "策略"
+ ]
+ },
{
"n": "ValidateProxyDef",
"s": "func ValidateProxyDef(d ProxyDef) string",
diff --git a/docs/examples/index.md b/docs/examples/index.md
index f3d6114..dbe4bab 100644
--- a/docs/examples/index.md
+++ b/docs/examples/index.md
@@ -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`
diff --git a/docs/guide/scene-memory.md b/docs/guide/scene-memory.md
new file mode 100644
index 0000000..da15b4a
--- /dev/null
+++ b/docs/guide/scene-memory.md
@@ -0,0 +1,113 @@
+# 场景记忆(Scene Memory)
+
+> 场景式记忆是内核 v1.3 起的能力。它不新增 API 面,只影响**你的输入被怎样记住与取回**。
+> 与 `NoMemory` / `ContextPolicy` / `RecallPolicy` 并列为第四项声明:`ScenePolicy`。
+
+## 它解决什么问题
+
+三层记忆按**字面相关性**召回:你得说出相近的词,记忆才会被取回来。
+场景记忆补上另一半:按**场合**召回。
+
+同一场合再次出现时,当时挂在这个场合上的约定、偏好、人物关系会自动回来——
+与这次说了什么措辞无关。
+
+```
+你:以后在群里回消息简短点
+ └─ 这条记忆挂到场面「chan:qq + peer:group_xxx」上
+
+一周后,同一个群里有人问「上次说的格式是什么」
+ └─ 场面重现(还没等你提到「格式」),那条约定已经被取回
+```
+
+## 场面是自己长出来的
+
+场景**不需要声明**。每轮交互,内核采集一组可观察信号当这轮���「场面指纹」:
+
+| 特征 | 来源 | 权重 | 说明 |
+|---|---|---|---|
+| `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` —— 常量与校验
diff --git a/docs/llms.txt b/docs/llms.txt
index 21520df..a8e5992 100644
--- a/docs/llms.txt
+++ b/docs/llms.txt
@@ -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 位恒为
diff --git a/mkdocs.yml b/mkdocs.yml
index 49d8529..11c81a4 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -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
diff --git a/remotedevice/CMakeLists.txt b/remotedevice/CMakeLists.txt
deleted file mode 100644
index c7fc578..0000000
--- a/remotedevice/CMakeLists.txt
+++ /dev/null
@@ -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: $")
-message(STATUS " Shared lib: ${BUILD_SHARED_LIBS}")
-message(STATUS " No alloc: ${HA_NO_ALLOC}")
\ No newline at end of file
diff --git a/remotedevice/include/ha_remotedevice.h b/remotedevice/include/ha_remotedevice.h
deleted file mode 100644
index 84e64a7..0000000
--- a/remotedevice/include/ha_remotedevice.h
+++ /dev/null
@@ -1,216 +0,0 @@
-#ifndef HA_REMOTEDEVICE_H
-#define HA_REMOTEDEVICE_H
-
-#include
-#include
-
-#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 */
\ No newline at end of file
diff --git a/remotedevice/src/ha_json.c b/remotedevice/src/ha_json.c
deleted file mode 100644
index 0d3af91..0000000
--- a/remotedevice/src/ha_json.c
+++ /dev/null
@@ -1,369 +0,0 @@
-#include "ha_json.h"
-#include
-#include
-#include
-#include
-
-/* ======================== 解析器 ======================== */
-
-/* 前向声明 */
-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;
-}
\ No newline at end of file
diff --git a/remotedevice/src/ha_json.h b/remotedevice/src/ha_json.h
deleted file mode 100644
index 1560d9e..0000000
--- a/remotedevice/src/ha_json.h
+++ /dev/null
@@ -1,107 +0,0 @@
-#ifndef HA_JSON_H
-#define HA_JSON_H
-
-#include
-#include
-
-#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 */
\ No newline at end of file
diff --git a/remotedevice/src/ha_remotedevice.c b/remotedevice/src/ha_remotedevice.c
deleted file mode 100644
index 4ab127d..0000000
--- a/remotedevice/src/ha_remotedevice.c
+++ /dev/null
@@ -1,628 +0,0 @@
-#include "ha_remotedevice.h"
-#include "ha_json.h"
-#include "ha_ws.h"
-#include
-#include
-#include
-
-#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;
-}
-
diff --git a/remotedevice/src/ha_ws.c b/remotedevice/src/ha_ws.c
deleted file mode 100644
index 266509d..0000000
--- a/remotedevice/src/ha_ws.c
+++ /dev/null
@@ -1,325 +0,0 @@
-#include "ha_ws.h"
-#include
-#include
-#include
-
-/* 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);
-}
\ No newline at end of file
diff --git a/remotedevice/src/ha_ws.h b/remotedevice/src/ha_ws.h
deleted file mode 100644
index e47cfcd..0000000
--- a/remotedevice/src/ha_ws.h
+++ /dev/null
@@ -1,62 +0,0 @@
-#ifndef HA_WS_H
-#define HA_WS_H
-
-#include
-#include
-#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 */
\ No newline at end of file
diff --git a/remotedevice/test/test_ha_remotedevice.c b/remotedevice/test/test_ha_remotedevice.c
deleted file mode 100644
index affd3f9..0000000
--- a/remotedevice/test/test_ha_remotedevice.c
+++ /dev/null
@@ -1,1509 +0,0 @@
-/**
- * ha_remotedevice 全面测试
- *
- * 覆盖:JSON 解析/构建、WS 帧编解码、工具函数、
- * 客户端完整生命周期、命令分发、二进制分块、语音数据、事件上报。
- *
- * 编译 (POSIX/Linux/macOS):
- * gcc -I../include -I../src ../src/*.c test_ha_remotedevice.c -lpthread -o test
- *
- * 编译 (Windows/MinGW):
- * gcc -I../include -I../src ../src/*.c test_ha_remotedevice.c -lpthread -lws2_32 -o test
- */
-
-#include "ha_remotedevice.h"
-#include "ha_json.h"
-#include "ha_ws.h"
-
-#include
-#include
-#include
-#include
-#include
-
-/* 前向声明:ha_client_send_data_chunked 供测试调用 */
-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 defined(_WIN32) || defined(_WIN64)
- #define _CRT_SECURE_NO_WARNINGS
- #include
- #include
- #include
- typedef int socklen_t;
- #define sleep(t) Sleep((t)*1000)
- #define usleep(t) Sleep((t)/1000)
- typedef unsigned thread_func_ret;
- #define THREAD_RETURN return 0
- #define SIGPIPE 13
- static void signal(int sig, void (*func)(int)) { (void)sig; (void)func; }
- static void sock_close(int fd) { closesocket(fd); }
-#else
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- typedef void *thread_func_ret;
- #define THREAD_RETURN return NULL
- static void sock_close(int fd) { close(fd); }
-#endif
-
-/* ==================================================================
- * 简易测试框架
- * ================================================================== */
-static int tests_passed = 0;
-static int tests_failed = 0;
-static int tests_skipped = 0;
-
-#define TEST_BEGIN(name) do { \
- printf(" TEST: %s ... ", name); \
- fflush(stdout); \
- do { (void)0
-
-#define TEST_END() } while(0); \
- printf("PASS\n"); \
- tests_passed++; \
-} while(0)
-
-#define TEST_FAIL(msg) do { \
- printf("FAIL: %s\n", msg); \
- tests_failed++; \
- return; \
-} while(0)
-
-#define TEST_ASSERT(cond, msg) do { \
- if (!(cond)) { TEST_FAIL(msg); } \
-} while(0)
-
-#define TEST_SKIP(reason) do { \
- printf("SKIP: %s\n", reason); \
- tests_skipped++; \
- return; \
-} while(0)
-
-/* ==================================================================
- * 工具:在本地端口启动一个 TCP 服务器(用于 mock 网关)
- * ================================================================== */
-typedef struct {
- int listen_fd;
- int client_fd;
- int port;
-#if defined(_WIN32) || defined(_WIN64)
- uintptr_t thread;
-#else
- pthread_t thread;
-#endif
- volatile int running;
-} mock_server_t;
-
-/* 发送 WS 文本帧(服务器端,无需掩码) */
-static void mock_send_text(int fd, const char *json) {
- int len = (int)strlen(json);
- uint8_t hdr[10];
- int hdr_len = 2;
- hdr[0] = 0x80 | 0x1;
- if (len < 126) {
- hdr[1] = (uint8_t)len;
- } else if (len < 65536) {
- hdr[1] = 126;
- hdr[2] = (uint8_t)(len >> 8);
- hdr[3] = (uint8_t)(len & 0xFF);
- hdr_len = 4;
- } else {
- hdr[1] = 127;
- uint64_t l = (uint64_t)len;
- for (int i = 8; i > 0; i--) {
- hdr[1 + i] = (uint8_t)(l & 0xFF);
- l >>= 8;
- }
- hdr_len = 10;
- }
- send(fd, (const char *)hdr, hdr_len, 0);
- send(fd, json, len, 0);
-}
-
-/* 发送 WS 二进制帧(服务器端,无需掩码) */
-static void mock_send_binary(int fd, const uint8_t *data, int len) {
- uint8_t hdr[10];
- int hdr_len = 2;
- hdr[0] = 0x80 | 0x2;
- if (len < 126) {
- hdr[1] = (uint8_t)len;
- } else if (len < 65536) {
- hdr[1] = 126;
- hdr[2] = (uint8_t)(len >> 8);
- hdr[3] = (uint8_t)(len & 0xFF);
- hdr_len = 4;
- } else {
- hdr[1] = 127;
- uint64_t l = (uint64_t)len;
- for (int i = 8; i > 0; i--) {
- hdr[1 + i] = (uint8_t)(l & 0xFF);
- l >>= 8;
- }
- hdr_len = 10;
- }
- send(fd, (const char *)hdr, hdr_len, 0);
- send(fd, (const char *)data, len, 0);
-}
-
-/* 读取 WS 文本帧(服务器端,解析掩码) */
-static char *mock_read_text(int fd) {
- uint8_t hdr[2];
- if (recv(fd, (char *)hdr, 2, 0) != 2) return NULL;
- int masked = (hdr[1] & 0x80) ? 1 : 0;
- uint64_t len = hdr[1] & 0x7F;
- if (len == 126) {
- uint8_t ext[2];
- if (recv(fd, (char *)ext, 2, 0) != 2) return NULL;
- len = ((uint64_t)ext[0] << 8) | ext[1];
- } else if (len == 127) {
- uint8_t ext[8];
- if (recv(fd, (char *)ext, 8, 0) != 8) return NULL;
- len = 0;
- for (int i = 0; i < 8; i++) len = (len << 8) | ext[i];
- }
- uint8_t mask_key[4] = {0};
- if (masked) {
- if (recv(fd, (char *)mask_key, 4, 0) != 4) return NULL;
- }
- char *buf = (char *)malloc((size_t)len + 1);
- if (!buf) return NULL;
- if (len > 0) {
- if ((int)recv(fd, buf, (int)len, 0) != (int)len) {
- free(buf);
- return NULL;
- }
- if (masked) {
- for (uint64_t i = 0; i < len; i++)
- buf[i] ^= mask_key[i & 3];
- }
- }
- buf[len] = '\0';
- return buf;
-}
-
-/* mock 服务器线程 */
-#if defined(_WIN32) || defined(_WIN64)
-static unsigned __stdcall mock_server_thread(void *arg) {
-#else
-static void *mock_server_thread(void *arg) {
-#endif
- mock_server_t *ms = (mock_server_t *)arg;
- struct sockaddr_in client_addr;
- socklen_t addr_len = sizeof(client_addr);
- int fd = accept(ms->listen_fd, (struct sockaddr *)&client_addr, &addr_len);
- if (fd < 0) THREAD_RETURN;
- ms->client_fd = fd;
-
- /* 读取 WS 升级请求 */
- char buf[4096] = {0};
- int n = 0;
- while (n < (int)sizeof(buf) - 1) {
- int r = (int)recv(fd, buf + n, 1, 0);
- if (r <= 0) break;
- n += r;
- buf[n] = '\0';
- if (n >= 4 && strcmp(buf + n - 4, "\r\n\r\n") == 0) break;
- }
-
- /* 发送 101 响应 */
- const char *resp =
- "HTTP/1.1 101 Switching Protocols\r\n"
- "Upgrade: websocket\r\n"
- "Connection: Upgrade\r\n"
- "Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n"
- "\r\n";
- send(fd, resp, (int)strlen(resp), 0);
-
- /* 协议循环 */
- char *msg;
- while (ms->running) {
- msg = mock_read_text(fd);
- if (!msg) break;
-
- if (strstr(msg, "\"hello\"")) {
- mock_send_text(fd, "{\"op\":\"hello_ack\",\"status\":\"ok\"}");
- } else if (strstr(msg, "\"bind\"")) {
- mock_send_text(fd, "{\"op\":\"bind_ack\",\"status\":\"ok\",\"device_id\":\"test-dev\"}");
- }
- /* cmd_result, event, status, cmd_data_start/end 可以忽略 */
- free(msg);
- }
- sock_close(fd);
- ms->client_fd = -1;
- THREAD_RETURN;
-}
-
-static mock_server_t *mock_server_start(int port) {
- mock_server_t *ms = (mock_server_t *)calloc(1, sizeof(mock_server_t));
-
-#if defined(_WIN32) || defined(_WIN64)
- WSADATA wsa;
- WSAStartup(MAKEWORD(2, 2), &wsa);
-#endif
-
- ms->listen_fd = (int)socket(AF_INET, SOCK_STREAM, 0);
- int opt = 1;
- setsockopt(ms->listen_fd, SOL_SOCKET, SO_REUSEADDR,
- (const char *)&opt, sizeof(opt));
-
- struct sockaddr_in addr;
- memset(&addr, 0, sizeof(addr));
- addr.sin_family = AF_INET;
- addr.sin_addr.s_addr = INADDR_ANY;
- addr.sin_port = htons(port);
- if (bind(ms->listen_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
- free(ms);
- return NULL;
- }
- listen(ms->listen_fd, 1);
- ms->port = port;
- ms->running = 1;
- ms->client_fd = -1;
-
-#if defined(_WIN32) || defined(_WIN64)
- ms->thread = _beginthreadex(NULL, 0, mock_server_thread, ms, 0, NULL);
-#else
- pthread_create(&ms->thread, NULL, mock_server_thread, ms);
-#endif
- usleep(200000); /* 等待服务器就绪 */
- return ms;
-}
-
-static void mock_server_stop(mock_server_t *ms) {
- if (!ms) return;
- ms->running = 0;
- if (ms->client_fd >= 0) sock_close(ms->client_fd);
- sock_close(ms->listen_fd);
-#if defined(_WIN32) || defined(_WIN64)
- WaitForSingleObject((HANDLE)ms->thread, 3000);
- WSACleanup();
-#else
- pthread_join(ms->thread, NULL);
-#endif
- free(ms);
-}
-
-/* ==================================================================
- * 传输层实现(POSIX socket)
- * ================================================================== */
-struct transport_ctx {
- int sock;
-};
-
-static int transport_connect(void *ctx, const char *host, uint16_t port) {
- struct transport_ctx *tc = (struct transport_ctx *)ctx;
- struct hostent *he = gethostbyname(host);
- if (!he) return -1;
- tc->sock = (int)socket(AF_INET, SOCK_STREAM, 0);
- if (tc->sock < 0) return -1;
- struct sockaddr_in addr;
- memset(&addr, 0, sizeof(addr));
- addr.sin_family = AF_INET;
- addr.sin_port = htons(port);
- memcpy(&addr.sin_addr, he->h_addr_list[0], he->h_length);
- if (connect(tc->sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
- sock_close(tc->sock);
- tc->sock = -1;
- return -1;
- }
- /* 设置 100ms 接收超时,使 ha_client_process 不会永久阻塞 */
-#if defined(_WIN32) || defined(_WIN64)
- DWORD timeout = 100;
- setsockopt(tc->sock, SOL_SOCKET, SO_RCVTIMEO, (const char *)&timeout, sizeof(timeout));
-#else
- struct timeval tv = {0, 100000};
- setsockopt(tc->sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
-#endif
- return 0;
-}
-
-static int transport_send(void *ctx, const uint8_t *data, int len) {
- struct transport_ctx *tc = (struct transport_ctx *)ctx;
- int sent = 0;
- while (sent < len) {
- int n = (int)send(tc->sock, (const char *)(data + sent), len - sent, 0);
- if (n <= 0) return -1;
- sent += n;
- }
- return sent;
-}
-
-static int transport_recv(void *ctx, uint8_t *buf, int len) {
- struct transport_ctx *tc = (struct transport_ctx *)ctx;
- return (int)recv(tc->sock, (char *)buf, len, 0);
-}
-
-static void transport_close(void *ctx) {
- struct transport_ctx *tc = (struct transport_ctx *)ctx;
- if (tc->sock >= 0) {
- sock_close(tc->sock);
- tc->sock = -1;
- }
-}
-
-/* ==================================================================
- * mock 服务器:发送命令
- * ================================================================== */
-static void mock_send_cmd(mock_server_t *ms, const char *cmd_type,
- const char *req_id, const char *cmd) {
- char buf[4096];
- char escaped[2048];
- int ei = 0;
- /* 转义 cmd 中的双引号和反斜杠 */
- if (cmd) {
- for (int i = 0; cmd[i] && ei < (int)sizeof(escaped) - 6; i++) {
- if (cmd[i] == '"' || cmd[i] == '\\') {
- escaped[ei++] = '\\';
- if (ei >= (int)sizeof(escaped) - 1) break;
- }
- escaped[ei++] = cmd[i];
- }
- }
- escaped[ei] = '\0';
- int n = snprintf(buf, sizeof(buf),
- "{\"op\":\"cmd\",\"req_id\":\"%s\",\"cmd_type\":\"%s\",\"command\":\"%s\"}",
- req_id ? req_id : "", cmd_type ? cmd_type : "", escaped);
- mock_send_text(ms->client_fd, buf);
-}
-
-/* ==================================================================
- * 测试用例
- * ================================================================== */
-
-/* ---------- 1. JSON 解析器 ---------- */
-static void test_json_parser(void) {
- TEST_BEGIN("JSON parser: parse object");
-
- const char *json = "{\"op\":\"hello\",\"device\":{\"id\":\"test\",\"caps\":[\"a\",\"b\"]}}";
- ha_json_node_t *root = ha_json_parse(json);
- TEST_ASSERT(root != NULL, "parse failed");
- TEST_ASSERT(root->type == HA_JSON_OBJECT, "not an object");
-
- const char *op = ha_json_get_string(root, "op");
- TEST_ASSERT(op != NULL && strcmp(op, "hello") == 0, "op mismatch");
-
- ha_json_node_t *dev = ha_json_get(root, "device");
- TEST_ASSERT(dev != NULL && dev->type == HA_JSON_OBJECT, "device not object");
-
- const char *id = ha_json_get_string(dev, "id");
- TEST_ASSERT(id != NULL && strcmp(id, "test") == 0, "device id mismatch");
-
- ha_json_node_t *caps = ha_json_get(dev, "caps");
- TEST_ASSERT(caps != NULL && caps->type == HA_JSON_ARRAY, "caps not array");
- TEST_ASSERT(ha_json_array_len(caps) == 2, "caps length wrong");
-
- ha_json_node_t *c0 = ha_json_array_get(caps, 0);
- TEST_ASSERT(c0 != NULL && c0->type == HA_JSON_STRING &&
- strcmp(c0->str_val, "a") == 0, "caps[0] mismatch");
-
- ha_json_free(root);
- TEST_END();
-}
-
-static void test_json_int(void) {
- TEST_BEGIN("JSON parser: int fields");
-
- const char *json = "{\"count\":42,\"total\":100500,\"neg\":-7}";
- ha_json_node_t *root = ha_json_parse(json);
- TEST_ASSERT(root != NULL, "parse failed");
-
- TEST_ASSERT(ha_json_get_int(root, "count", -1) == 42, "count mismatch");
- TEST_ASSERT(ha_json_get_int(root, "total", -1) == 100500, "total mismatch");
- TEST_ASSERT(ha_json_get_int(root, "neg", 0) == -7, "neg mismatch");
- TEST_ASSERT(ha_json_get_int(root, "nonexistent", -999) == -999, "default wrong");
-
- ha_json_free(root);
- TEST_END();
-}
-
-static void test_json_builder(void) {
- TEST_BEGIN("JSON builder: object with string + int + bool");
-
- char buf[256];
- ha_json_builder_t jb;
- ha_json_builder_init(&jb, buf, sizeof(buf));
- ha_json_builder_begin_object(&jb);
- ha_json_builder_string(&jb, "op", "hello");
- ha_json_builder_int(&jb, "seq", 1);
- ha_json_builder_bool(&jb, "active", 1);
- ha_json_builder_end_object(&jb);
-
- const char *r = ha_json_builder_str(&jb);
- TEST_ASSERT(r != NULL, "builder returned NULL");
- TEST_ASSERT(strstr(r, "\"op\":\"hello\"") != NULL, "missing op");
- TEST_ASSERT(strstr(r, "\"seq\":1") != NULL, "missing seq");
- TEST_ASSERT(strstr(r, "\"active\":true") != NULL, "missing bool");
-
- TEST_END();
-}
-
-static void test_json_array_builder(void) {
- TEST_BEGIN("JSON builder: array of strings");
-
- char buf[256];
- ha_json_builder_t jb;
- ha_json_builder_init(&jb, buf, sizeof(buf));
- ha_json_builder_begin_object(&jb);
- ha_json_builder_key(&jb, "caps");
- ha_json_builder_begin_array(&jb);
- ha_json_builder_add_string(&jb, "camera");
- ha_json_builder_add_string(&jb, "screen");
- ha_json_builder_end_array(&jb);
- ha_json_builder_end_object(&jb);
-
- const char *r = ha_json_builder_str(&jb);
- TEST_ASSERT(r != NULL, "builder returned NULL");
- TEST_ASSERT(strstr(r, "\"camera\"") != NULL, "missing camera");
- TEST_ASSERT(strstr(r, "\"screen\"") != NULL, "missing screen");
-
- TEST_END();
-}
-
-/* ---------- 2. 工具函数 ---------- */
-static void test_parse_homeagent(void) {
- TEST_BEGIN("ha_cmd_parse_homeagent: basic");
-
- const char *cap, *args;
-
- ha_cmd_parse_homeagent("camerasue 5", &cap, &args);
- TEST_ASSERT(strcmp(cap, "camerasue") == 0, "cap mismatch");
- TEST_ASSERT(strcmp(args, "5") == 0, "args mismatch");
-
- ha_cmd_parse_homeagent("screensee", &cap, &args);
- TEST_ASSERT(strcmp(cap, "screensee") == 0, "cap mismatch (no args)");
- TEST_ASSERT(strcmp(args, "") == 0, "args should be empty");
-
- ha_cmd_parse_homeagent("homeagent-camerasue 3", &cap, &args);
- TEST_ASSERT(strcmp(cap, "camerasue") == 0, "prefix not stripped");
- TEST_ASSERT(strcmp(args, "3") == 0, "args after prefix");
-
- ha_cmd_parse_homeagent("", &cap, &args);
- TEST_ASSERT(strcmp(cap, "") == 0, "empty input");
-
- TEST_END();
-}
-
-static void test_parse_json(void) {
- TEST_BEGIN("ha_cmd_parse_json: action + json");
-
- const char *action, *json_str;
-
- ha_cmd_parse_json("computeruse {\"action\":\"click\",\"x\":100}",
- &action, &json_str);
- TEST_ASSERT(strcmp(action, "computeruse") == 0, "action mismatch");
- TEST_ASSERT(strstr(json_str, "\"action\"") != NULL, "json missing");
-
- ha_cmd_parse_json("screensee", &action, &json_str);
- TEST_ASSERT(strcmp(action, "screensee") == 0, "action no json");
- TEST_ASSERT(strcmp(json_str, "") == 0, "json should be empty");
-
- TEST_END();
-}
-
-static void test_base64(void) {
- TEST_BEGIN("ha_base64_encode: basic");
-
- const uint8_t data[] = "Hello, World!";
- char out[64];
- int n = ha_base64_encode(data, 13, out, sizeof(out));
- TEST_ASSERT(n > 0, "encode returned 0");
- TEST_ASSERT(strcmp(out, "SGVsbG8sIFdvcmxkIQ==") == 0, "base64 mismatch");
-
- const uint8_t jpeg[] = {0xFF, 0xD8, 0xFF};
- n = ha_base64_encode(jpeg, 3, out, sizeof(out));
- TEST_ASSERT(n > 0, "short encode failed");
- TEST_ASSERT(strcmp(out, "/9j/") == 0, "jpeg magic mismatch");
-
- /* 缓冲区不足 */
- n = ha_base64_encode(data, 13, out, 5);
- TEST_ASSERT(n > 5, "should return needed size");
-
- TEST_END();
-}
-
-/* ---------- 3. WS 帧编解码 ---------- */
-static void test_ws_frame(void) {
-#if defined(_WIN32) || defined(_WIN64)
- printf(" TEST: WS frame: encode/decode with mask ... SKIP: socketpair not available on Windows\n");
- tests_skipped++;
- return;
-#else
- TEST_BEGIN("WS frame: encode/decode with mask");
-
- /* 使用 TCP 本地连接测试 */
- int sv[2];
- TEST_ASSERT(socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == 0,
- "socketpair failed");
-
- const char *test_payload = "hello world";
- int test_len = (int)strlen(test_payload);
-
- /* 发送端:手动构造 WS 帧 */
- uint8_t frame[1024];
- int off = 0;
- frame[off++] = 0x80 | 0x1;
- frame[off++] = 0x80 | (uint8_t)test_len;
- uint8_t mask[4] = {0x01, 0x02, 0x03, 0x04};
- memcpy(frame + off, mask, 4); off += 4;
- for (int i = 0; i < test_len; i++)
- frame[off++] = test_payload[i] ^ mask[i & 3];
- send(sv[0], (const char *)frame, off, 0);
-
- /* 接收端 */
- ha_transport_t transport = {
- .send = transport_send, .recv = transport_recv,
- .close = transport_close,
- };
- struct transport_ctx tctx_reader = { .sock = sv[1] };
- transport.ctx = &tctx_reader;
-
- ha_ws_t ws;
- memset(&ws, 0, sizeof(ws));
- ws.transport = &transport;
- ws.connected = 1;
-
- const uint8_t *payload;
- int len;
- int opcode = ha_ws_read_frame(&ws, &payload, &len);
- TEST_ASSERT(opcode == 0x1, "expected text frame");
- TEST_ASSERT(len == test_len, "length mismatch");
- TEST_ASSERT(memcmp(payload, test_payload, len) == 0, "payload mismatch");
-
- sock_close(sv[0]);
- sock_close(sv[1]);
- TEST_END();
-#endif
-}
-
-/* ---------- 4. ha_version ---------- */
-static void test_version(void) {
- TEST_BEGIN("ha_version: returns non-empty");
-
- const char *v = ha_version();
- TEST_ASSERT(v != NULL && v[0] != '\0', "version empty");
- printf("(v=%s) ", v);
- TEST_END();
-}
-
-/* ---------- 5. 完整客户端生命周期 ---------- */
-static volatile int lifecycle_state = 0;
-
-static void test_lifecycle_on_state(int connected, void *userdata) {
- (void)userdata;
- lifecycle_state = connected ? 1 : 0;
-}
-
-static ha_status_t test_handler_camerasue(const char *req_id, const char *args,
- ha_cmd_result_t *result, void *userdata) {
- (void)req_id; (void)userdata;
- printf(" [handler] camerasue called, args=%s\n", args ? args : "");
- result->status = 0;
- result->output = "data:image/jpeg;base64,test123";
- return HA_OK;
-}
-
-static ha_status_t test_handler_shell(const char *req_id, const char *args,
- ha_cmd_result_t *result, void *userdata) {
- (void)req_id; (void)userdata;
- printf(" [handler] shell called: %s\n", args ? args : "");
- result->status = 0;
- result->output = "shell output here";
- return HA_OK;
-}
-
-static void test_client_lifecycle(void) {
- TEST_BEGIN("Client lifecycle: connect -> hello -> bind -> ready");
-
- mock_server_t *ms = mock_server_start(19890);
- TEST_ASSERT(ms != NULL, "mock server start failed");
-
- struct transport_ctx tctx;
- tctx.sock = -1;
- ha_transport_t transport = {
- .connect = transport_connect,
- .send = transport_send,
- .recv = transport_recv,
- .close = transport_close,
- .ctx = &tctx,
- };
-
- const char *caps[] = {"camera", "status", NULL};
- ha_cmd_handler_def_t handlers[] = {
- {.command = "shell", .handler = test_handler_shell},
- {.command = "camerasue", .handler = test_handler_camerasue},
- {.command = NULL},
- };
-
- ha_config_t config = {
- .transport = transport,
- .server = "127.0.0.1:19890",
- .token = "test-token",
- .device = {
- .device_id = "test-dev",
- .name = "Test Device",
- .kind = "camera",
- .caps = caps,
- },
- .handlers = handlers,
- .on_state = test_lifecycle_on_state,
- .ping_interval = 30,
- };
-
- lifecycle_state = 0;
- ha_client_t *client = ha_client_new(&config);
- TEST_ASSERT(client != NULL, "client new failed");
-
- ha_status_t st = ha_client_start(client);
- TEST_ASSERT(st == HA_OK, "client start failed");
- usleep(200000);
- TEST_ASSERT(lifecycle_state == 1, "should be connected");
-
- /* 发送命令验证分发 */
- mock_send_cmd(ms, "homeagent", "req-1", "camerasue 3");
- usleep(200000);
- ha_client_process(client);
-
- mock_send_cmd(ms, "shell", "req-2", "ls -la");
- usleep(200000);
- ha_client_process(client);
-
- /* 未注册的命令 */
- mock_send_cmd(ms, "homeagent", "req-3", "unknowncmd");
- usleep(100000);
- ha_client_process(client);
-
- ha_client_stop(client);
- ha_client_destroy(client);
- mock_server_stop(ms);
-
- TEST_END();
-}
-
-/* ---------- 6. 命令分发 ---------- */
-static volatile int cmd_camerasue_fired = 0;
-static volatile int cmd_shell_fired = 0;
-static char cmd_req_id[128] = "";
-
-static ha_status_t test_handler_camerasue2(const char *req_id, const char *args,
- ha_cmd_result_t *result, void *userdata) {
- (void)userdata;
- cmd_camerasue_fired = 1;
- strncpy(cmd_req_id, req_id, sizeof(cmd_req_id) - 1);
- if (strcmp(args, "5") != 0) {
- printf(" [handler] args mismatch: expected '5', got '%s'\n", args);
- result->status = 1;
- result->error = "args mismatch";
- return HA_OK;
- }
- result->status = 0;
- result->output = "snapshot taken";
- return HA_OK;
-}
-
-static ha_status_t test_handler_shell2(const char *req_id, const char *args,
- ha_cmd_result_t *result, void *userdata) {
- (void)userdata;
- cmd_shell_fired = 1;
- if (strstr(args, "ls") == NULL) {
- printf(" [handler] args should contain 'ls', got '%s'\n", args);
- result->status = 1;
- result->error = "args mismatch";
- return HA_OK;
- }
- result->status = 0;
- result->output = "file1.txt\nfile2.txt";
- return HA_OK;
-}
-
-static void test_command_dispatch(void) {
- TEST_BEGIN("Command dispatch: homeagent + shell");
-
- mock_server_t *ms = mock_server_start(19891);
- TEST_ASSERT(ms != NULL, "mock server start failed");
-
- struct transport_ctx tctx;
- tctx.sock = -1;
- ha_transport_t transport = {
- .connect = transport_connect,
- .send = transport_send,
- .recv = transport_recv,
- .close = transport_close,
- .ctx = &tctx,
- };
-
- const char *caps[] = {"camera", "cmd", NULL};
- ha_cmd_handler_def_t handlers[] = {
- {.command = "shell", .handler = test_handler_shell2},
- {.command = "camerasue", .handler = test_handler_camerasue2},
- {.command = NULL},
- };
-
- ha_config_t config = {
- .transport = transport,
- .server = "127.0.0.1:19891",
- .token = "test-token",
- .device = {
- .device_id = "cmd-dev",
- .name = "Cmd Test",
- .kind = "camera",
- .caps = caps,
- },
- .handlers = handlers,
- .ping_interval = 30,
- };
-
- cmd_camerasue_fired = 0;
- cmd_shell_fired = 0;
- memset(cmd_req_id, 0, sizeof(cmd_req_id));
-
- ha_client_t *client = ha_client_new(&config);
- ha_status_t st = ha_client_start(client);
- TEST_ASSERT(st == HA_OK, "client start failed");
- usleep(200000);
-
- /* 发送 homeagent 命令 */
- mock_send_cmd(ms, "homeagent", "req-cam", "camerasue 5");
- usleep(200000);
- ha_client_process(client);
- TEST_ASSERT(cmd_camerasue_fired == 1, "camerasue handler not called");
- TEST_ASSERT(strcmp(cmd_req_id, "req-cam") == 0, "req_id mismatch");
-
- /* 发送 shell 命令 */
- mock_send_cmd(ms, "shell", "req-sh", "ls -la /tmp");
- usleep(200000);
- ha_client_process(client);
- TEST_ASSERT(cmd_shell_fired == 1, "shell handler not called");
-
- ha_client_stop(client);
- ha_client_destroy(client);
- mock_server_stop(ms);
-
- TEST_END();
-}
-
-/* ---------- 7. 事件上报 ---------- */
-static void test_event_report(void) {
- TEST_BEGIN("Event report: client sends event to server");
-
- mock_server_t *ms = mock_server_start(19892);
- TEST_ASSERT(ms != NULL, "mock server start failed");
-
- struct transport_ctx tctx;
- tctx.sock = -1;
- ha_transport_t transport = {
- .connect = transport_connect,
- .send = transport_send,
- .recv = transport_recv,
- .close = transport_close,
- .ctx = &tctx,
- };
-
- ha_config_t config = {
- .transport = transport,
- .server = "127.0.0.1:19892",
- .token = "test-token",
- .device = {
- .device_id = "evt-dev",
- .name = "Event Test",
- .kind = "camera",
- .caps = (const char *[]){"camera", NULL},
- },
- .handlers = NULL,
- .ping_interval = 30,
- };
-
- ha_client_t *client = ha_client_new(&config);
- ha_status_t st = ha_client_start(client);
- TEST_ASSERT(st == HA_OK, "client start failed");
- usleep(200000);
-
- ha_client_send_event(client, "motion_detected", "{\"zone\":\"front_door\"}");
- usleep(100000);
- ha_client_process(client);
-
- ha_client_stop(client);
- ha_client_destroy(client);
- mock_server_stop(ms);
-
- TEST_END();
-}
-
-/* ---------- 8. 状态上报 ---------- */
-static void test_status_report(void) {
- TEST_BEGIN("Status report: client sends status to server");
-
- mock_server_t *ms = mock_server_start(19893);
- TEST_ASSERT(ms != NULL, "mock server start failed");
-
- struct transport_ctx tctx;
- tctx.sock = -1;
- ha_transport_t transport = {
- .connect = transport_connect,
- .send = transport_send,
- .recv = transport_recv,
- .close = transport_close,
- .ctx = &tctx,
- };
-
- ha_config_t config = {
- .transport = transport,
- .server = "127.0.0.1:19893",
- .token = "test-token",
- .device = {
- .device_id = "st-dev",
- .name = "Status Test",
- .kind = "camera",
- .caps = (const char *[]){"camera", NULL},
- },
- .handlers = NULL,
- .ping_interval = 30,
- };
-
- ha_client_t *client = ha_client_new(&config);
- ha_status_t st = ha_client_start(client);
- TEST_ASSERT(st == HA_OK, "client start failed");
- usleep(200000);
-
- ha_client_send_status(client, "offline");
- usleep(100000);
- ha_client_process(client);
-
- ha_client_stop(client);
- ha_client_destroy(client);
- mock_server_stop(ms);
-
- TEST_END();
-}
-
-/* ---------- 9. 二进制分块回传 ---------- */
-static void test_binary_chunked(void) {
- TEST_BEGIN("Binary chunked transfer: send video data to server");
-
- mock_server_t *ms = mock_server_start(19894);
- TEST_ASSERT(ms != NULL, "mock server start failed");
-
- struct transport_ctx tctx;
- tctx.sock = -1;
- ha_transport_t transport = {
- .connect = transport_connect,
- .send = transport_send,
- .recv = transport_recv,
- .close = transport_close,
- .ctx = &tctx,
- };
-
- ha_cmd_handler_def_t handlers[] = {
- {.command = "camerasue", .handler = test_handler_camerasue},
- {.command = NULL},
- };
-
- ha_config_t config = {
- .transport = transport,
- .server = "127.0.0.1:19894",
- .token = "test-token",
- .device = {
- .device_id = "bin-dev",
- .name = "Binary Test",
- .kind = "camera",
- .caps = (const char *[]){"camera", NULL},
- },
- .handlers = handlers,
- .ping_interval = 30,
- };
-
- ha_client_t *client = ha_client_new(&config);
- ha_status_t st = ha_client_start(client);
- TEST_ASSERT(st == HA_OK, "client start failed");
- usleep(200000);
-
- /* 模拟录像数据 */
- uint8_t video_data[20000];
- for (int i = 0; i < 20000; i++) video_data[i] = (uint8_t)(i % 251);
-
- ha_client_send_data_chunked(client, "req-video", "camera_video",
- "video/mp4", video_data, 20000);
- usleep(500000);
- ha_client_process(client);
-
- ha_client_stop(client);
- ha_client_destroy(client);
- mock_server_stop(ms);
-
- TEST_END();
-}
-
-/* ---------- 10. 未注册命令处理 ---------- */
-static void test_unsupported_command(void) {
- TEST_BEGIN("Unsupported command: returns error gracefully");
-
- mock_server_t *ms = mock_server_start(19895);
- TEST_ASSERT(ms != NULL, "mock server start failed");
-
- struct transport_ctx tctx;
- tctx.sock = -1;
- ha_transport_t transport = {
- .connect = transport_connect,
- .send = transport_send,
- .recv = transport_recv,
- .close = transport_close,
- .ctx = &tctx,
- };
-
- ha_cmd_handler_def_t handlers[] = {
- {.command = "camerasue", .handler = test_handler_camerasue},
- {.command = NULL},
- };
-
- ha_config_t config = {
- .transport = transport,
- .server = "127.0.0.1:19895",
- .token = "test-token",
- .device = {
- .device_id = "unsup-dev",
- .name = "Unsupported Test",
- .kind = "camera",
- .caps = (const char *[]){"camera", NULL},
- },
- .handlers = handlers,
- .ping_interval = 30,
- };
-
- ha_client_t *client = ha_client_new(&config);
- ha_status_t st = ha_client_start(client);
- TEST_ASSERT(st == HA_OK, "client start failed");
- usleep(200000);
-
- /* 发送未注册命令 */
- mock_send_cmd(ms, "homeagent", "req-unsup", "screensee");
- usleep(100000);
- ha_client_process(client);
-
- /* 发送已注册命令 */
- mock_send_cmd(ms, "homeagent", "req-ok", "camerasue");
- usleep(100000);
- ha_client_process(client);
-
- ha_client_stop(client);
- ha_client_destroy(client);
- mock_server_stop(ms);
-
- TEST_END();
-}
-
-/* ---------- 11. 语音数据接收 ---------- */
-static volatile int speech_received = 0;
-static uint8_t speech_data[4096];
-static int speech_len = 0;
-
-static void test_on_binary(const char *req_id, const char *kind,
- const char *mime, const uint8_t *data,
- int len, void *userdata) {
- (void)req_id; (void)kind; (void)mime; (void)userdata;
- speech_received = 1;
- speech_len = len < (int)sizeof(speech_data) ? len : (int)sizeof(speech_data);
- memcpy(speech_data, data, (size_t)speech_len);
-}
-
-static void test_speech_receive(void) {
- TEST_BEGIN("Speech data receive: cmd_speech_start -> binary -> end");
-
- mock_server_t *ms = mock_server_start(19896);
- TEST_ASSERT(ms != NULL, "mock server start failed");
-
- struct transport_ctx tctx;
- tctx.sock = -1;
- ha_transport_t transport = {
- .connect = transport_connect,
- .send = transport_send,
- .recv = transport_recv,
- .close = transport_close,
- .ctx = &tctx,
- };
-
- ha_config_t config = {
- .transport = transport,
- .server = "127.0.0.1:19896",
- .token = "test-token",
- .device = {
- .device_id = "speech-dev",
- .name = "Speech Test",
- .kind = "speaker",
- .caps = (const char *[]){"speaker", NULL},
- },
- .handlers = NULL,
- .on_binary = test_on_binary,
- .ping_interval = 30,
- };
-
- speech_received = 0;
- speech_len = 0;
-
- ha_client_t *client = ha_client_new(&config);
- ha_status_t st = ha_client_start(client);
- TEST_ASSERT(st == HA_OK, "client start failed");
- usleep(200000);
-
- /* mock 服务器发送语音数据 */
- const char *audio_data = "RIFF....fake-wav-data....";
- mock_send_text(ms->client_fd,
- "{\"op\":\"cmd_speech_start\",\"req_id\":\"req-speech\","
- "\"kind\":\"speech\",\"mime\":\"audio/wav\",\"total\":25}");
- usleep(50000);
- mock_send_binary(ms->client_fd, (const uint8_t *)audio_data, 25);
- usleep(50000);
- mock_send_text(ms->client_fd,
- "{\"op\":\"cmd_speech_end\",\"req_id\":\"req-speech\"}");
- usleep(200000);
-
- /* 处理帧 */
- for (int i = 0; i < 50; i++) {
- ha_client_process(client);
- if (speech_received) break;
- usleep(20000);
- }
-
- TEST_ASSERT(speech_received == 1, "speech handler not called");
- TEST_ASSERT(speech_len == 25, "speech length mismatch");
- TEST_ASSERT(memcmp(speech_data, audio_data, 25) == 0, "speech data mismatch");
-
- ha_client_stop(client);
- ha_client_destroy(client);
- mock_server_stop(ms);
-
- TEST_END();
-}
-
-/* ---------- 12. 二进制分块 via handler ---------- */
-static ha_status_t test_handler_video(const char *req_id, const char *args,
- ha_cmd_result_t *result, void *userdata) {
- (void)req_id; (void)userdata;
- printf(" [handler] video record %s\n", args ? args : "");
-
- static uint8_t video[5000];
- for (int i = 0; i < 5000; i++) video[i] = (uint8_t)(i & 0xFF);
-
- result->status = 0;
- result->has_binary = 1;
- result->binary_data = video;
- result->binary_len = 5000;
- result->binary_mime = "video/mp4";
- return HA_OK;
-}
-
-static void test_binary_via_handler(void) {
- TEST_BEGIN("Binary via handler: handler sets has_binary, SDK auto-chunks");
-
- mock_server_t *ms = mock_server_start(19897);
- TEST_ASSERT(ms != NULL, "mock server start failed");
-
- struct transport_ctx tctx;
- tctx.sock = -1;
- ha_transport_t transport = {
- .connect = transport_connect,
- .send = transport_send,
- .recv = transport_recv,
- .close = transport_close,
- .ctx = &tctx,
- };
-
- ha_cmd_handler_def_t handlers[] = {
- {.command = "camerasue", .handler = test_handler_video},
- {.command = NULL},
- };
-
- ha_config_t config = {
- .transport = transport,
- .server = "127.0.0.1:19897",
- .token = "test-token",
- .device = {
- .device_id = "bin2-dev",
- .name = "Binary Via Handler",
- .kind = "camera",
- .caps = (const char *[]){"camera", NULL},
- },
- .handlers = handlers,
- .ping_interval = 30,
- };
-
- ha_client_t *client = ha_client_new(&config);
- ha_status_t st = ha_client_start(client);
- TEST_ASSERT(st == HA_OK, "client start failed");
- usleep(200000);
-
- mock_send_cmd(ms, "homeagent", "req-video2", "camerasue 10");
- usleep(500000);
- ha_client_process(client);
- usleep(100000);
- ha_client_process(client);
-
- ha_client_stop(client);
- ha_client_destroy(client);
- mock_server_stop(ms);
-
- TEST_END();
-}
-
-/* ---------- 13. computeruse:结构化 JSON 命令分发 ---------- */
-static volatile int computeruse_fired = 0;
-static char computeruse_action[64] = "";
-static int computeruse_x = 0;
-static int computeruse_y = 0;
-
-static ha_status_t test_handler_computeruse(const char *req_id, const char *args,
- ha_cmd_result_t *result, void *userdata) {
- (void)req_id; (void)userdata;
- computeruse_fired = 1;
- /* 解析 JSON 参数 */
- if (args && args[0] == '{') {
- ha_json_node_t *root = ha_json_parse(args);
- if (root) {
- const char *act = ha_json_get_string(root, "action");
- if (act) strncpy(computeruse_action, act, sizeof(computeruse_action) - 1);
- computeruse_x = ha_json_get_int(root, "x", 0);
- computeruse_y = ha_json_get_int(root, "y", 0);
- ha_json_free(root);
- }
- }
- result->status = 0;
- result->output = "clicked at (100,200)";
- return HA_OK;
-}
-
-static void test_computeruse_dispatch(void) {
- TEST_BEGIN("Computeruse: structured JSON command dispatch");
-
- mock_server_t *ms = mock_server_start(19898);
- TEST_ASSERT(ms != NULL, "mock server start failed");
-
- struct transport_ctx tctx;
- tctx.sock = -1;
- ha_transport_t transport = {
- .connect = transport_connect,
- .send = transport_send,
- .recv = transport_recv,
- .close = transport_close,
- .ctx = &tctx,
- };
-
- const char *caps[] = {"computeruse", "cmd", NULL};
- ha_cmd_handler_def_t handlers[] = {
- {.command = "computeruse", .handler = test_handler_computeruse},
- {.command = NULL},
- };
-
- ha_config_t config = {
- .transport = transport,
- .server = "127.0.0.1:19898",
- .token = "test-token",
- .device = {
- .device_id = "cu-dev",
- .name = "ComputerUse Test",
- .kind = "computer",
- .caps = caps,
- },
- .handlers = handlers,
- .ping_interval = 30,
- };
-
- computeruse_fired = 0;
- memset(computeruse_action, 0, sizeof(computeruse_action));
- computeruse_x = 0;
- computeruse_y = 0;
-
- ha_client_t *client = ha_client_new(&config);
- ha_status_t st = ha_client_start(client);
- TEST_ASSERT(st == HA_OK, "client start failed");
- usleep(200000);
-
- /* 发送 computeruse 命令(带结构化 JSON 参数) */
- mock_send_cmd(ms, "homeagent", "req-cu",
- "computeruse {\"action\":\"click\",\"x\":100,\"y\":200}");
- usleep(200000);
- ha_client_process(client);
- TEST_ASSERT(computeruse_fired == 1, "computeruse handler not called");
- TEST_ASSERT(strcmp(computeruse_action, "click") == 0,
- "action should be 'click'");
- TEST_ASSERT(computeruse_x == 100, "x should be 100");
- TEST_ASSERT(computeruse_y == 200, "y should be 200");
-
- ha_client_stop(client);
- ha_client_destroy(client);
- mock_server_stop(ms);
-
- TEST_END();
-}
-
-/* ---------- 14. clipboardsee/clipboardsue 命令分发 ---------- */
-static volatile int clipboardsee_fired = 0;
-static volatile int clipboardsue_fired = 0;
-static char clipboardsue_text[256] = "";
-
-static ha_status_t test_handler_clipboardsee(const char *req_id, const char *args,
- ha_cmd_result_t *result, void *userdata) {
- (void)req_id; (void)args; (void)userdata;
- clipboardsee_fired = 1;
- result->status = 0;
- result->output = "clipboard content here";
- return HA_OK;
-}
-
-static ha_status_t test_handler_clipboardsue(const char *req_id, const char *args,
- ha_cmd_result_t *result, void *userdata) {
- (void)req_id; (void)userdata;
- clipboardsue_fired = 1;
- if (args) strncpy(clipboardsue_text, args, sizeof(clipboardsue_text) - 1);
- result->status = 0;
- result->output = "clipboard set";
- return HA_OK;
-}
-
-static void test_clipboard_dispatch(void) {
- TEST_BEGIN("Clipboard: clipboardsee + clipboardsue dispatch");
-
- mock_server_t *ms = mock_server_start(19899);
- TEST_ASSERT(ms != NULL, "mock server start failed");
-
- struct transport_ctx tctx;
- tctx.sock = -1;
- ha_transport_t transport = {
- .connect = transport_connect,
- .send = transport_send,
- .recv = transport_recv,
- .close = transport_close,
- .ctx = &tctx,
- };
-
- const char *caps[] = {"clipboard", "cmd", NULL};
- ha_cmd_handler_def_t handlers[] = {
- {.command = "clipboardsee", .handler = test_handler_clipboardsee},
- {.command = "clipboardsue", .handler = test_handler_clipboardsue},
- {.command = NULL},
- };
-
- ha_config_t config = {
- .transport = transport,
- .server = "127.0.0.1:19899",
- .token = "test-token",
- .device = {
- .device_id = "clip-dev",
- .name = "Clipboard Test",
- .kind = "computer",
- .caps = caps,
- },
- .handlers = handlers,
- .ping_interval = 30,
- };
-
- clipboardsee_fired = 0;
- clipboardsue_fired = 0;
- memset(clipboardsue_text, 0, sizeof(clipboardsue_text));
-
- ha_client_t *client = ha_client_new(&config);
- ha_status_t st = ha_client_start(client);
- TEST_ASSERT(st == HA_OK, "client start failed");
- usleep(200000);
-
- /* 发送 clipboardsee 命令 */
- mock_send_cmd(ms, "homeagent", "req-cs", "clipboardsee");
- usleep(200000);
- ha_client_process(client);
- TEST_ASSERT(clipboardsee_fired == 1, "clipboardsee handler not called");
-
- /* 发送 clipboardsue 命令 */
- mock_send_cmd(ms, "homeagent", "req-cw", "clipboardsue Hello World");
- usleep(200000);
- ha_client_process(client);
- TEST_ASSERT(clipboardsue_fired == 1, "clipboardsue handler not called");
- TEST_ASSERT(strcmp(clipboardsue_text, "Hello World") == 0,
- "clipboardsue text mismatch");
-
- ha_client_stop(client);
- ha_client_destroy(client);
- mock_server_stop(ms);
-
- TEST_END();
-}
-
-/* ---------- 15. screensee:带 data URL 的结果回传 ---------- */
-static ha_status_t test_handler_screensee(const char *req_id, const char *args,
- ha_cmd_result_t *result, void *userdata) {
- (void)req_id; (void)args; (void)userdata;
- result->status = 0;
- /* 返回 data URL 格式的 base64 图像数据 */
- result->output = "data:image/jpeg;base64,/9j/4AAQSkZJRg==";
- return HA_OK;
-}
-
-static void test_screensee_data_url(void) {
- TEST_BEGIN("Screensee: handler returns data URL result");
-
- mock_server_t *ms = mock_server_start(19900);
- TEST_ASSERT(ms != NULL, "mock server start failed");
-
- struct transport_ctx tctx;
- tctx.sock = -1;
- ha_transport_t transport = {
- .connect = transport_connect,
- .send = transport_send,
- .recv = transport_recv,
- .close = transport_close,
- .ctx = &tctx,
- };
-
- const char *caps[] = {"screen", "cmd", NULL};
- ha_cmd_handler_def_t handlers[] = {
- {.command = "screensee", .handler = test_handler_screensee},
- {.command = NULL},
- };
-
- ha_config_t config = {
- .transport = transport,
- .server = "127.0.0.1:19900",
- .token = "test-token",
- .device = {
- .device_id = "see-dev",
- .name = "Screensee Test",
- .kind = "computer",
- .caps = caps,
- },
- .handlers = handlers,
- .ping_interval = 30,
- };
-
- ha_client_t *client = ha_client_new(&config);
- ha_status_t st = ha_client_start(client);
- TEST_ASSERT(st == HA_OK, "client start failed");
- usleep(200000);
-
- /* 发送 screensee 命令 */
- mock_send_cmd(ms, "homeagent", "req-see", "screensee");
- usleep(200000);
- ha_client_process(client);
-
- /* handler 已被调用,且返回了 data URL(无法直接验证回执内容,但 handler 已执行) */
- ha_client_stop(client);
- ha_client_destroy(client);
- mock_server_stop(ms);
-
- TEST_END();
-}
-
-/* ---------- 16. info_json 设备信息 ---------- */
-static void test_info_json(void) {
- TEST_BEGIN("Device info_json: included in hello message");
-
- mock_server_t *ms = mock_server_start(19901);
- TEST_ASSERT(ms != NULL, "mock server start failed");
-
- struct transport_ctx tctx;
- tctx.sock = -1;
- ha_transport_t transport = {
- .connect = transport_connect,
- .send = transport_send,
- .recv = transport_recv,
- .close = transport_close,
- .ctx = &tctx,
- };
-
- const char *caps[] = {"camera", NULL};
- ha_cmd_handler_def_t handlers[] = {
- {.command = "camerasue", .handler = test_handler_camerasue},
- {.command = NULL},
- };
-
- ha_config_t config = {
- .transport = transport,
- .server = "127.0.0.1:19901",
- .token = "test-token",
- .device = {
- .device_id = "info-dev",
- .name = "Info Test",
- .kind = "camera",
- .caps = caps,
- .info_json = "{\"chip\":\"ESP32-S3\",\"psram\":8}",
- },
- .handlers = handlers,
- .ping_interval = 30,
- };
-
- ha_client_t *client = ha_client_new(&config);
- ha_status_t st = ha_client_start(client);
- TEST_ASSERT(st == HA_OK, "client start failed");
- usleep(200000);
-
- /* 连接成功,info_json 已通过 hello 消息发送 */
- ha_client_stop(client);
- ha_client_destroy(client);
- mock_server_stop(ms);
-
- TEST_END();
-}
-
-/* ==================================================================
- * 主函数
- * ================================================================== */
-int main(void) {
-#if !defined(_WIN32) && !defined(_WIN64)
- signal(SIGPIPE, SIG_IGN);
-#endif
-
- printf("========================================\n");
- printf(" ha_remotedevice 全面测试\n");
- printf("========================================\n\n");
-
- /* ---- JSON 解析/构建 ---- */
- printf("[JSON]\n");
- test_json_parser();
- test_json_int();
- test_json_builder();
- test_json_array_builder();
-
- /* ---- 工具函数 ---- */
- printf("\n[Utilities]\n");
- test_parse_homeagent();
- test_parse_json();
- test_base64();
-
- /* ---- WS 协议 ---- */
- printf("\n[WebSocket]\n");
- test_ws_frame();
-
- /* ---- SDK 核心 ---- */
- printf("\n[SDK Core]\n");
- test_version();
- test_client_lifecycle();
- test_command_dispatch();
-
- /* ---- 协议功能 ---- */
- printf("\n[Protocol]\n");
- test_event_report();
- test_status_report();
- test_binary_chunked();
- test_unsupported_command();
- test_speech_receive();
- test_binary_via_handler();
-
- /* ---- 扩展命令 ---- */
- printf("\n[Extended Commands]\n");
- test_computeruse_dispatch();
- test_clipboard_dispatch();
- test_screensee_data_url();
- test_info_json();
-
- /* ---- 汇总 ---- */
- printf("\n========================================\n");
- printf(" 结果: %d passed, %d failed, %d skipped\n",
- tests_passed, tests_failed, tests_skipped);
- printf("========================================\n");
-
- return tests_failed > 0 ? 1 : 0;
-}
\ No newline at end of file
diff --git a/sdk/knowledge.go b/sdk/knowledge.go
index 4c9d5d7..aaf9f11 100644
--- a/sdk/knowledge.go
+++ b/sdk/knowledge.go
@@ -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"`
}
diff --git a/sdk/plugin.go b/sdk/plugin.go
index 6852696..544df5f 100644
--- a/sdk/plugin.go
+++ b/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.