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.