# 其他类型 剩余的类型与方法:`PluginSDK` 本体的访问器、`StageContext` 的并发控制,以及多模态辅助类型。没有归入上面任何一个主题,但可能仍会用到。 ## `DocMemoryAPI` DocMemoryAPI provides access to the document vector store. | 方法 | 说明 | |---|---| | [`Insert`](#docmemoryapiinsert) | | | [`InsertWithMedia`](#docmemoryapiinsertwithmedia) | InsertWithMedia 写入文档并关联媒体。attachments 里带 Data 的会落进 | | [`Query`](#docmemoryapiquery) | | | [`Remove`](#docmemoryapiremove) | | | [`Stats`](#docmemoryapistats) | | ### `DocMemoryAPI.Insert` ```go Insert(doc *Doc) error ``` `memory.go:76` ### `DocMemoryAPI.InsertWithMedia` ```go InsertWithMedia(doc *Doc, attachments []MediaAttachment) error ``` InsertWithMedia 写入文档并关联媒体。attachments 里带 Data 的会落进 内容寻址存储(相同字节只存一份),只带 Digest 的直接引用已有内容。 媒体成为文档直接持有的一等记忆块:文档向量会融合它们的原生向量, 因此图片按自己的向量被召回,不依赖任何生成的描述文本。 `memory.go:81` ### `DocMemoryAPI.Query` ```go Query(text string, topK int) []*Doc ``` `memory.go:75` ### `DocMemoryAPI.Remove` ```go Remove(id string) ``` `memory.go:82` ### `DocMemoryAPI.Stats` ```go Stats() map[string]interface{} ``` `memory.go:83` ## `EventSubscriber` EventSubscriber allows plugins to subscribe to kernel events. This is a restricted interface: plugins can subscribe but the kernel controls which events are delivered. | 方法 | 说明 | |---|---| | [`Subscribe`](#eventsubscribersubscribe) | | ### `EventSubscriber.Subscribe` !!! warning "仅内核内置插件可用" ```go Subscribe(eventType EventType, handler EventHandler) func() ``` `plugin.go:378` ## `IOInjector` IOInjector provides methods for injecting input and interrupts into the agent pipeline. All methods accept (source, channel) where channel is the target output channel for routing the agent's response. | 方法 | 说明 | |---|---| | [`InjectInputMedia`](#ioinjectorinjectinputmedia) | | | [`InjectInputMediaOpts`](#ioinjectorinjectinputmediaopts) | | | [`InjectInputMediaSync`](#ioinjectorinjectinputmediasync) | | | [`InjectInputMediaSyncOpts`](#ioinjectorinjectinputmediasyncopts) | | | [`InjectInputSync`](#ioinjectorinjectinputsync) | InjectInputSync 注入输入事件并同步等待 agent 回复,返回回复文本(无回复时返回空串)。 | | [`InjectInputSyncOpts`](#ioinjectorinjectinputsyncopts) | | | [`InjectInterruptMedia`](#ioinjectorinjectinterruptmedia) | | | [`InjectInterruptMediaOpts`](#ioinjectorinjectinterruptmediaopts) | | | [`InjectInterruptText`](#ioinjectorinjectinterrupttext) | | | [`InjectInterruptTextOpts`](#ioinjectorinjectinterrupttextopts) | | | [`InjectText`](#ioinjectorinjecttext) | | | [`InjectTextNoMemory`](#ioinjectorinjecttextnomemory) | | | [`InjectTextOpts`](#ioinjectorinjecttextopts) | 以下 Opts 变体让调用点在**这一次注入**上声明记忆与裁剪行为。 | | [`SetToolBlocks`](#ioinjectorsettoolblocks) | SetToolBlocks 插件工具注入多模态内容块(image_url/audio_url),内核在下一条 | ### `IOInjector.InjectInputMedia` ```go InjectInputMedia(source, channel, text string, blocks []ContentBlock) ``` `plugin.go:329` ### `IOInjector.InjectInputMediaOpts` ```go InjectInputMediaOpts(source, channel, text string, blocks []ContentBlock, opts InjectOptions) ``` `plugin.go:340` ### `IOInjector.InjectInputMediaSync` ```go InjectInputMediaSync(source, channel, text string, blocks []ContentBlock) string ``` `plugin.go:330` ### `IOInjector.InjectInputMediaSyncOpts` ```go InjectInputMediaSyncOpts(source, channel, text string, blocks []ContentBlock, opts InjectOptions) string ``` `plugin.go:341` ### `IOInjector.InjectInputSync` ```go InjectInputSync(source, channel, text string) string ``` InjectInputSync 注入输入事件并同步等待 agent 回复,返回回复文本(无回复时返回空串)。 用于通道消息的完整闭环:收到入站 → agent 处理 → 回复取回 → 送回通道。 **示例插件里的真实用法** | 插件 | 位置 | 代码 | |---|---|---| | [`a2a`](../examples/index.md#a2a) | `example/a2a/plugin.go:345` | `reply := p.sdk.InjectInputSync(p.name, p.name,` | | [`acp`](../examples/index.md#acp) | `example/acp/plugin.go:237` | `reply = p.sdk.InjectInputSync(p.name, p.name,` | `plugin.go:325` ### `IOInjector.InjectInputSyncOpts` ```go InjectInputSyncOpts(source, channel, text string, opts InjectOptions) string ``` `plugin.go:339` ### `IOInjector.InjectInterruptMedia` ```go InjectInterruptMedia(source, channel, text string, blocks []ContentBlock) ``` `plugin.go:331` ### `IOInjector.InjectInterruptMediaOpts` ```go InjectInterruptMediaOpts(source, channel, text string, blocks []ContentBlock, opts InjectOptions) ``` `plugin.go:342` ### `IOInjector.InjectInterruptText` ```go InjectInterruptText(source, channel, text string) ``` `plugin.go:320` ### `IOInjector.InjectInterruptTextOpts` ```go InjectInterruptTextOpts(source, channel, text string, opts InjectOptions) ``` **示例插件里的真实用法** | 插件 | 位置 | 代码 | |---|---|---| | [`browser`](../examples/index.md#browser) | `example/browser/plugin.go:1319` | `p.sdk.InjectInterruptTextOpts(p.name, p.name,` | | [`calendar`](../examples/index.md#calendar) | `example/calendar/plugin.go:527` | `p.sdk.InjectInterruptTextOpts("calendar", "calendar", msg, sdk.InjectOptions{NoMemory: true})` | | [`memo`](../examples/index.md#memo) | `example/memo/plugin.go:299` | `p.sdk.InjectInterruptTextOpts(p.name, p.name,` | | [`qq`](../examples/index.md#qq) | `example/qq/plugin.go:1493` | `p.sdk.InjectInterruptTextOpts(p.name, p.name, text, sdk.InjectOptions{` | `plugin.go:338` ### `IOInjector.InjectText` ```go InjectText(source, channel, text string) ``` `plugin.go:321` ### `IOInjector.InjectTextNoMemory` ```go InjectTextNoMemory(source, channel, text string) ``` **示例插件里的真实用法** | 插件 | 位置 | 代码 | |---|---|---| | [`browser`](../examples/index.md#browser) | `example/browser/plugin.go:1114` | `p.sdk.InjectTextNoMemory(p.name, p.name, fmt.Sprintf("[浏览器 %s 已导航到 %s]", id, rawURL))` | `plugin.go:322` ### `IOInjector.InjectTextOpts` ```go InjectTextOpts(source, channel, text string, opts InjectOptions) ``` 以下 Opts 变体让调用点在**这一次注入**上声明记忆与裁剪行为。 上面那些不带 opts 的方法等价于传零值 InjectOptions(记入记忆 + 不裁剪), 保留它们是为了不破坏已有插件;新代码应当用 Opts 变体把意图写清楚。 `plugin.go:337` ### `IOInjector.SetToolBlocks` ```go SetToolBlocks(blocks []ContentBlock) ``` SetToolBlocks 插件工具注入多模态内容块(image_url/audio_url),内核在下一条 tool message 的 content 数组里带上这些块,让模型在后续轮次看到图/听到音频。 `plugin.go:328` ## `KnowledgeAPI` KnowledgeAPI provides access to the knowledge store. | 方法 | 说明 | |---|---| | [`Add`](#knowledgeapiadd) | | | [`List`](#knowledgeapilist) | | | [`Search`](#knowledgeapisearch) | | ### `KnowledgeAPI.Add` ```go Add(name, content string) error ``` `knowledge.go:6` ### `KnowledgeAPI.List` ```go List() ([]string, error) ``` `knowledge.go:7` ### `KnowledgeAPI.Search` ```go Search(query string, topK int) ([]*Knowledge, error) ``` `knowledge.go:5` ## `LLMAPI` LLMAPI provides access to the LLM provider manager. | 方法 | 说明 | |---|---| | [`CurrentSource`](#llmapicurrentsource) | | | [`ListSources`](#llmapilistsources) | | | [`SetSource`](#llmapisetsource) | | ### `LLMAPI.CurrentSource` ```go CurrentSource() string ``` `llm.go:7` ### `LLMAPI.ListSources` ```go ListSources() []string ``` `llm.go:5` ### `LLMAPI.SetSource` ```go SetSource(name string) error ``` `llm.go:6` ## `MemoryAPI` MemoryAPI provides access to the graph memory (entity-relation store). | 方法 | 说明 | |---|---| | [`Commit`](#memoryapicommit) | | | [`Introspect`](#memoryapiintrospect) | | | [`MergeEntities`](#memoryapimergeentities) | | | [`Purge`](#memoryapipurge) | | | [`Recall`](#memoryapirecall) | | ### `MemoryAPI.Commit` ```go Commit(triples []Triple) error ``` `memory.go:6` ### `MemoryAPI.Introspect` ```go Introspect() (map[string]interface{}, error) ``` `memory.go:7` ### `MemoryAPI.MergeEntities` ```go MergeEntities(source, target string) (int, error) ``` `memory.go:8` ### `MemoryAPI.Purge` ```go Purge(criteria map[string]string, mode string) (int, error) ``` `memory.go:9` ### `MemoryAPI.Recall` ```go Recall(query []string, depth int) ([]Entity, []Relation, error) ``` `memory.go:5` ## `Plugin` Plugin is the interface every plugin must implement. | 方法 | 说明 | |---|---| | [`Name`](#pluginname) | | | [`Start`](#pluginstart) | | | [`Stop`](#pluginstop) | | ### `Plugin.Name` ```go Name() string ``` `plugin.go:14` ### `Plugin.Start` ```go Start(sdk *PluginSDK) error ``` `plugin.go:15` ### `Plugin.Stop` ```go Stop() error ``` `plugin.go:16` ## `PluginMgrAPI` PluginMgrAPI 提供插件管理能力(外部插件可调用)。 由 bridge 注入 dispatch 实现,走 C ABI CORE_PLUGIN_RELOAD_ONE 等。 | 方法 | 说明 | |---|---| | [`IsPluginDisabled`](#pluginmgrapiisplugindisabled) | IsPluginDisabled 查询插件是否被禁用。 | | [`ListLoadedPlugins`](#pluginmgrapilistloadedplugins) | ListLoadedPlugins 列出已加载插件。 | | [`ReloadOne`](#pluginmgrapireloadone) | ReloadOne 重载单个插件(停止后重新加载)。 | ### `PluginMgrAPI.IsPluginDisabled` ```go IsPluginDisabled(name string) bool ``` IsPluginDisabled 查询插件是否被禁用。 `plugin.go:389` ### `PluginMgrAPI.ListLoadedPlugins` ```go ListLoadedPlugins() []string ``` ListLoadedPlugins 列出已加载插件。 `plugin.go:387` ### `PluginMgrAPI.ReloadOne` ```go ReloadOne(name string) error ``` ReloadOne 重载单个插件(停止后重新加载)。 `plugin.go:385` ## `SettingsAPI` | 方法 | 说明 | |---|---| | [`DataDir`](#settingsapidatadir) | DataDir returns the plugin-specific data directory (guaranteed to exist): | | [`Defs`](#settingsapidefs) | Defs returns config definitions matching the prefix. | | [`Dump`](#settingsapidump) | Dump returns all config values. | | [`Get`](#settingsapiget) | Get reads the plugin's own config value (config_ table). | | [`GetCore`](#settingsapigetcore) | GetCore reads the core config table. | | [`GetPlugin`](#settingsapigetplugin) | GetPlugin reads another plugin's config table. | | [`List`](#settingsapilist) | List returns all keys matching the given prefix. | | [`ListCore`](#settingsapilistcore) | ListCore lists core config keys matching the prefix. | | [`ListPlugin`](#settingsapilistplugin) | ListPlugin lists another plugin's config keys matching the prefix. | | [`Plugins`](#settingsapiplugins) | Plugins returns a list of all plugin config namespaces. | | [`RegisterDef`](#settingsapiregisterdef) | RegisterDef registers a config definition for UI display. | | [`Set`](#settingsapiset) | Set writes a config value to the plugin's own config table. | | [`SetCore`](#settingsapisetcore) | SetCore writes to the core config table. | | [`SetPlugin`](#settingsapisetplugin) | SetPlugin writes to another plugin's config table. | ### `SettingsAPI.DataDir` ```go DataDir() string ``` DataDir returns the plugin-specific data directory (guaranteed to exist): /plugin_data/. Plugins should persist any runtime files (generated images, caches, downloads) here. `settings.go:25` ### `SettingsAPI.Defs` ```go Defs(prefix string) []*ConfigDef ``` Defs returns config definitions matching the prefix. `settings.go:40` ### `SettingsAPI.Dump` ```go Dump() map[string]interface{} ``` Dump returns all config values. `settings.go:43` ### `SettingsAPI.Get` ```go Get(key string) (interface{}, error) ``` Get reads the plugin's own config value (config_ table). `settings.go:5` ### `SettingsAPI.GetCore` ```go GetCore(key string) (interface{}, error) ``` GetCore reads the core config table. `settings.go:14` ### `SettingsAPI.GetPlugin` ```go GetPlugin(plugin, key string) (interface{}, error) ``` GetPlugin reads another plugin's config table. `settings.go:28` ### `SettingsAPI.List` ```go List(prefix string) ([]string, error) ``` List returns all keys matching the given prefix. `settings.go:11` ### `SettingsAPI.ListCore` ```go ListCore(prefix string) ([]string, error) ``` ListCore lists core config keys matching the prefix. `settings.go:20` ### `SettingsAPI.ListPlugin` ```go ListPlugin(plugin, prefix string) ([]string, error) ``` ListPlugin lists another plugin's config keys matching the prefix. `settings.go:34` ### `SettingsAPI.Plugins` ```go Plugins() []string ``` Plugins returns a list of all plugin config namespaces. `settings.go:46` ### `SettingsAPI.RegisterDef` ```go RegisterDef(def ConfigDef) ``` RegisterDef registers a config definition for UI display. `settings.go:37` ### `SettingsAPI.Set` ```go Set(key string, value interface{}) error ``` Set writes a config value to the plugin's own config table. `settings.go:8` ### `SettingsAPI.SetCore` ```go SetCore(key string, value interface{}) error ``` SetCore writes to the core config table. `settings.go:17` ### `SettingsAPI.SetPlugin` ```go SetPlugin(plugin, key string, value interface{}) error ``` SetPlugin writes to another plugin's config table. `settings.go:31` ## `SocialAPI` SocialAPI provides read-only access to the social graph (person profiles and relationships). External plugins can query person traits and social networks but cannot modify them. | 方法 | 说明 | |---|---| | [`GetNetwork`](#socialapigetnetwork) | | | [`GetPerson`](#socialapigetperson) | | | [`GetRelations`](#socialapigetrelations) | | | [`GetTrait`](#socialapigettrait) | | | [`ListPersons`](#socialapilistpersons) | | ### `SocialAPI.GetNetwork` ```go GetNetwork(name string, depth int) ([]*PersonProfile, error) ``` `memory.go:104` ### `SocialAPI.GetPerson` ```go GetPerson(name string) (*PersonProfile, error) ``` `memory.go:101` ### `SocialAPI.GetRelations` ```go GetRelations(name string) ([]SocialRelation, error) ``` `memory.go:103` ### `SocialAPI.GetTrait` ```go GetTrait(name, trait string) (string, bool) ``` `memory.go:102` ### `SocialAPI.ListPersons` ```go ListPersons() ([]string, error) ``` `memory.go:105` ## `TextMemoryAPI` TextMemoryAPI provides access to chronological text event storage. | 方法 | 说明 | |---|---| | [`Append`](#textmemoryapiappend) | | ### `TextMemoryAPI.Append` ```go Append(evt TextEvent) error ``` `memory.go:44` ### `AudioURL` ```go type AudioURL struct { URL string `json:"url"` } ``` `plugin.go:966` ### `EffectiveProxyAuth` ```go func EffectiveProxyAuth(auth string) string ``` EffectiveProxyAuth 返回生效的鉴权模式(空串归一化为 ProxyAuthHomeAgent)。 `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:961` ### `MemItem` ```go type MemItem struct { Role string `json:"role"` Content string `json:"content"` Score float64 `json:"score"` } ``` MemItem represents a memory item in stage context. `plugin.go:220` ### `NormalizeProxyHost` ```go func NormalizeProxyHost(pluginName string) string ``` NormalizeProxyHost 由插件名派生默认 Host 标签。 下划线转连字符:插件名允许下划线(huawei_smarthome),但 DNS label 不允许, 直接用会导致该子域名无法解析——这里统一转换,避免每个插件各自碰运气。 `proxy.go:202` ### `PluginSDK` ```go type PluginSDK struct { name string regTool ToolRegistrar regStage StageRegistrar regAPI APIRegistrar regOutput OutputChannelRegistrar … ``` PluginSDK is the main API surface provided to plugins at runtime. It wraps tool registration, settings, memory, knowledge, LLM, and IO injection. `plugin.go:436` ### `ProxyAuthHomeAgent` ```go const ProxyAuthHomeAgent ``` ProxyAuthHomeAgent 表示由 HomeAgent 统一保护:浏览器走门户会话 (homeagent_session cookie),非浏览器客户端走 X-API-Key。 两者都没有时返回 401,而不是把请求透传给上游。 `proxy.go:149` ### `ProxyAuthNone` ```go const ProxyAuthNone ``` ProxyAuthNone 表示不经 HomeAgent 鉴权,直接把请求转发给上游。 适用场景:上游自己有鉴权且调用方不是浏览器(设备/嵌入式客户端), 或上游是刻意公开的服务。选用它意味着**信任上游自身的鉴权**, 且该服务在网络层可达范围内对所有人开放。 `proxy.go:156` ### `ProxyDef` ```go type ProxyDef struct { // Name 是同一插件内多条声明的唯一标识(如 "ui"、"api")。 // 运行期由 RegisterProxy 的第一个参数填入;声明式由 … ``` 反向代理声明:插件告诉 HomeAgent「我起了个 HTTP 服务,请把它反代出去」。 为什么需要:插件自带 Web UI / HTTP API 时,监听地址在插件自己的配置里 (如 127.0.0.1:12100),外部无从得知;而 webui 的对外端口通常只有一个 (默认 :8080,且常经 frp 单端口隧道穿透)。没有声明机制时,用户只能 「知道端口 + 自己配转发」,插件换端口就失效。 设计取舍——**声明式而非注册式**:声明写在 plugin.json 里,由 HomeAgent 在加载插件时读取聚合,而不是让插件在运行期调 API 注册。理由: 1. 静态可发现:未启动/已崩溃的插件,其服务声明依然可见(可给出准确报错 「插件 X 声明了 ui 但目标 127.0.0.1:12100 不可达」,而不是静默 404); 2. 可版本化:声明随插件包一起分发、可 diff、可审计; 3. 旧内核无害:manifest 解析忽略未知字段,未支持该能力的 HomeAgent 读旧 插件、或旧 HomeAgent 读新插件都不会报错。 与 ToolDef / ChannelDef / ConfigDef 同族:SDK 定义声明契约,内核实现行为。 声明方式与其它能力一致 —— 在 Start() 里调 RegisterProxy(name, def), 或写进 plugin.json 的 proxies 字段(外部插件两种都支持)。 安全性:**不声明 = 不被反代**。声明本身就是能力声明,因此不需要在 capabilities 里另外开一个开关——最小权限默认生效。 # 单一入口原则(强制要求) **一个声明 = 一个入口**。被反代的插件必须让它的全部资源与接口都能从 该入口的一个基准路径出发访问到,不得依赖「入口之外的根路径」。 为什么强制:反代有两种挂载形态,而它们对「根路径」的处理截然不同—— Host 形态(host):插件独占 <标签>.<基域名>,根路径就是插件的根。 根绝对路径(fetch('/api/x'))**天然正确**。 Path 形态(path):插件挂在门户自身 host 的某个前缀下,根路径属于**门户**。 此时插件里的 fetch('/api/x') 会打到门户自己的 /api/x —— 静默错路由,页面能开但功能全坏。 于是「同一个插件必须同时支持两种形态」这条要求,等价于: **插件内部一律使用相对路径**(或基于 /location 推导的路径), 绝不硬编码以 / 开头的绝对路径。 这样同一份前端在两种形态下都正确,插件作者也不必知道自己被挂在哪。 反代层据此可以:外部子域可用时给 Host 形态,子域不可用(证书/放行限制) 时给 Path 形态,**无需插件配合改动**。 自检(插件作者在本地就该做):把页面挂到 <门户>/<任意前缀>/ 下访问, 所有请求都必须仍然打到插件自己。 本项目实测案例:某插件前端写死 fetch('/api/status'),配在 /p/huawei/ 下会打到门户的 /api/status(404 或返回门户数据); 改成相对路径后两种形态同时可用。 ProxyDef 是一个服务的**反代声明体**。 与 ToolDef 同构:Name 同时出现在字段与 RegisterProxy 的第一个参数里 (ToolDef 也是这么做的 —— 字段供 plugin.json 序列化,参数供运行期调用)。 Name 只用于展示、日志与冲突提示,**不参与路由**(路由键是 Host 与 Path)。 `proxy.go:64` ### `ProxyRegistrar` ```go type ProxyRegistrar func(name string, def ProxyDef) ``` ProxyRegistrar 由内核注入(与 ToolRegistrar / InputChannelRegistrar 同族)。 插件不直接调它,用 RegisterProxy。 为什么需要运行期注册(明明有 plugin.json 自动发现):**内置插件** (编译进内核、没有独立插件目录与 plugin.json,如 remotedevice)扫不到; 而它们恰恰最需要被反代出去(设备网关就是内置的)。两种来源互补: - 外部插件 → plugin.json 的 proxies(静态,未启动也可见) - 内置插件 → RegisterProxy(运行期,随 Start 注册) `proxy.go:306` ### `PluginSDK.RegisterProxy` ```go func (s *PluginSDK) RegisterProxy(name string, def ProxyDef) ``` RegisterProxy 声明一个需要 HomeAgent 反代出去的服务。 与 RegisterTool / RegisterInputChannel / RegisterOutputChannel 同一风格: 显式给名字 + 声明体。名字用于展示、日志与冲突提示(不参与路由 —— 路由键是 def.Host / def.Path)。 用法(通常在 Start 里调用): s.RegisterProxy("ui", sdk.ProxyDef{ Host: "myapp", Target: "127.0.0.1:12100", }) 声明立即生效(反代表在下一次请求时重建)。**不做去重**:同一 Host/Path 被两条声明占用时由反代层判定冲突并明确报错,而不是在这里静默吞掉 —— 插件作者需要看见冲突。 与 plugin.json 的 proxies 字段等价:写哪个都行,两者会合并(同名以本调用为准)。 `proxy.go:335` ### `SDKVersion` ```go var SDKVersion ``` 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 func (s *PluginSDK) SetProxyRegistrar(r ProxyRegistrar) ``` SetProxyRegistrar 由内核注入。插件不直接调它(与 SetInputChannelRegistrar 同族)。 `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 "仅内核内置插件可用" 外部插件的桥接模板只注入 SetOutputChannelRegistrar(proc_main.go.tmpl:693-705),**未**注入 SetOutputChannelUnregistrar(grep Unregistrar 在 templates/ 下无命中)。因此外部插件的 regOutputUnreg 为 nil,UnregisterOutputChannel 会命中 `if reg != nil` 的 else 分支**直接返回 nil**(sdk/plugin.go:539-549)——即**静默无效**:不报错、通道也没注销。内置插件由 internal/sdk/plugin.go:330 注入 cfg.RegOutputUnreg,真正生效。外部插件要让通道下线,只能重载插件。 ```go func (s *PluginSDK) UnregisterOutputChannel(name string) error ``` UnregisterOutputChannel 注销一个输出通道(动态通道随资源生灭时必须调用)。 `plugin.go:643` ### `ValidProxyAuth` ```go func ValidProxyAuth(auth string) bool ``` ValidProxyAuth 校验 Auth 取值;空串合法(等价 ProxyAuthHomeAgent)。 `proxy.go:160` ### `ValidProxyHostLabel` ```go func ValidProxyHostLabel(label string) bool ``` ValidProxyHostLabel 校验子域名标签是否合法(DNS label 规则)。 独立成导出函数:插件作者在写声明时、HomeAgent 在加载时、工具链在打包时 都要用同一套规则判定,避免三处各写一份而互相不一致。 `proxy.go:180` ### `ValidScenePolicy` ```go func ValidScenePolicy(policy string) bool ``` ValidScenePolicy 校验场面策略取值;空串等价于 ScenePolicyAuto。 `plugin.go:103` ### `ValidateProxyDef` ```go func ValidateProxyDef(d ProxyDef) string ``` ValidateProxyDef 校验一条反代声明,返回人类可读的错误说明(合法时为空)。 为什么要在 SDK 里做校验:HomeAgent 加载插件时必须能明确拒绝坏声明并说明 原因(而不是静默忽略导致用户以为配好了);插件作者也需要在本地就能查出 拼错的 Target/Host。同一套规则两端共用。 `proxy.go:229`