跳转至

其他类型

剩余的类型与方法:PluginSDK 本体的访问器、StageContext 的并发控制,以及多模态辅助类型。没有归入上面任何一个主题,但可能仍会用到。

DocMemoryAPI

DocMemoryAPI provides access to the document vector store.

方法 说明
Insert
InsertWithMedia InsertWithMedia 写入文档并关联媒体。attachments 里带 Data 的会落进
Query
Remove
Stats

DocMemoryAPI.Insert

Insert(doc *Doc) error

memory.go:76

DocMemoryAPI.InsertWithMedia

InsertWithMedia(doc *Doc, attachments []MediaAttachment) error

InsertWithMedia 写入文档并关联媒体。attachments 里带 Data 的会落进 内容寻址存储(相同字节只存一份),只带 Digest 的直接引用已有内容。 媒体成为文档直接持有的一等记忆块:文档向量会融合它们的原生向量, 因此图片按自己的向量被召回,不依赖任何生成的描述文本。

memory.go:81

DocMemoryAPI.Query

Query(text string, topK int) []*Doc

memory.go:75

DocMemoryAPI.Remove

Remove(id string)

memory.go:82

DocMemoryAPI.Stats

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

EventSubscriber.Subscribe

仅内核内置插件可用

Subscribe(eventType EventType, handler EventHandler) func()

plugin.go:526

EventsAPI

EventsAPI 是插件可见的事件总线。

它不是 *events.Bus 的投影:这是一个有边界、有审查的接口。

方法 说明
AllowedPublish AllowedPublish 返回当前可发布的事件类型(白名单 + 自己的命名空间前缀)。
AllowedSubscribe AllowedSubscribe 返回当前可订阅的事件类型。
Publish Publish 发布一个事件。
Subscribe Subscribe 订阅一个白名单内的事件(只读面)。

EventsAPI.AllowedPublish

AllowedPublish() []string

AllowedPublish 返回当前可发布的事件类型(白名单 + 自己的命名空间前缀)。

events.go:43

EventsAPI.AllowedSubscribe

AllowedSubscribe() []string

AllowedSubscribe 返回当前可订阅的事件类型。

events.go:46

EventsAPI.Publish

Publish(eventType string, payload map[string]interface{}) error

Publish 发布一个事件。

eventType 必须在白名单内(见 AllowedPublish),或形如 "plugin.<本插件名>.<后缀>"。载荷大小与键数有上限。

events.go:34

EventsAPI.Subscribe

Subscribe(eventType string, handler func(payload map[string]interface{})) (cancel func(), err error)

Subscribe 订阅一个白名单内的事件(只读面)。

返回的 cancel 在插件 Stop 时调用可解除订阅;宿主卸载插件时也会自动解除。 handler 的 panic 会被总线隔离,不影响其他订阅者。

events.go:40

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
InjectInputMediaOpts
InjectInputMediaSync
InjectInputMediaSyncOpts
InjectInputSync InjectInputSync 注入输入事件并同步等待 agent 回复,返回回复文本(无回复时返回空串)。
InjectInputSyncOpts
InjectInterruptMedia
InjectInterruptMediaOpts
InjectInterruptText
InjectInterruptTextOpts
InjectText
InjectTextNoMemory
InjectTextOpts 以下 Opts 变体让调用点在这一次注入上声明记忆与裁剪行为。
SetToolBlocks SetToolBlocks 插件工具注入多模态内容块(image_url/audio_url),内核在下一条

IOInjector.InjectInputMedia

InjectInputMedia(source, channel, text string, blocks []ContentBlock)

plugin.go:477

IOInjector.InjectInputMediaOpts

InjectInputMediaOpts(source, channel, text string, blocks []ContentBlock, opts InjectOptions)

plugin.go:488

IOInjector.InjectInputMediaSync

InjectInputMediaSync(source, channel, text string, blocks []ContentBlock) string

plugin.go:478

IOInjector.InjectInputMediaSyncOpts

InjectInputMediaSyncOpts(source, channel, text string, blocks []ContentBlock, opts InjectOptions) string

plugin.go:489

IOInjector.InjectInputSync

InjectInputSync(source, channel, text string) string

InjectInputSync 注入输入事件并同步等待 agent 回复,返回回复文本(无回复时返回空串)。 用于通道消息的完整闭环:收到入站 → agent 处理 → 回复取回 → 送回通道。

plugin.go:473

IOInjector.InjectInputSyncOpts

InjectInputSyncOpts(source, channel, text string, opts InjectOptions) string

plugin.go:487

IOInjector.InjectInterruptMedia

InjectInterruptMedia(source, channel, text string, blocks []ContentBlock)

plugin.go:479

IOInjector.InjectInterruptMediaOpts

InjectInterruptMediaOpts(source, channel, text string, blocks []ContentBlock, opts InjectOptions)

plugin.go:490

IOInjector.InjectInterruptText

InjectInterruptText(source, channel, text string)

plugin.go:468

IOInjector.InjectInterruptTextOpts

InjectInterruptTextOpts(source, channel, text string, opts InjectOptions)

示例插件里的真实用法

插件 位置 代码
rtvoice example/rtvoice/plugin.go:268 s.InjectInterruptTextOpts(p.name, p.name+"-in", text,

plugin.go:486

IOInjector.InjectText

InjectText(source, channel, text string)

plugin.go:469

IOInjector.InjectTextNoMemory

InjectTextNoMemory(source, channel, text string)

plugin.go:470

IOInjector.InjectTextOpts

InjectTextOpts(source, channel, text string, opts InjectOptions)

以下 Opts 变体让调用点在这一次注入上声明记忆与裁剪行为。

上面那些不带 opts 的方法等价于传零值 InjectOptions(记入记忆 + 不裁剪), 保留它们是为了不破坏已有插件;新代码应当用 Opts 变体把意图写清楚。

plugin.go:485

IOInjector.SetToolBlocks

SetToolBlocks(blocks []ContentBlock)

SetToolBlocks 插件工具注入多模态内容块(image_url/audio_url),内核在下一条 tool message 的 content 数组里带上这些块,让模型在后续轮次看到图/听到音频。

plugin.go:476

KernelControlAPI

KernelControlAPI:插件请求内核本身重启或关机。

为什么需要它

有些状态变更只能靠重启内核生效(典型:改了 LLM 源、换了主密钥、 升级了要重新装配的配置)。没有这条通道时,用户只能自己去 ssh 上敲 systemctl —— 而 WebUI 是大多数人唯一会用的界面。

语义边界(重要,别指望它做更多)

这两个方法都是异步的:它们只把请求投递给内核,立刻返回。 调用方(WebUI)应当先给用户一个明确回应("正在重启,页面稍后自动重连"), 而不是等待返回 —— 内核真的会在这几秒内消失。

进程会怎么走:

RequestKernelRestart ⇒ 内核清理(停插件、flush、CloseAll)→ exit 42
                       → guard 用**当前二进制**重拉,不经过 systemd、
                         不计失败轮次(这就是"热更新"那条路)
RequestKernelShutdown ⇒ 内核清理 → 通知 guard 主动停机 → systemd 不再拉起

★ 关机为什么不能用"guard 退出"实现:systemd 是 Restart=always,

guard 退出会被它无条件重新拉起。必须显式 `systemctl stop`)

失败语义

返回错误只表示请求没被接受(例如内核未装配控制通道、或该功能被禁用), 不表示"没重启成功"——成功投递后本进程就会退出,返回值本身没有后续。

方法 说明
RequestKernelRestart RequestKernelRestart 请求重启内核(热重启,guard 重拉)。
RequestKernelShutdown RequestKernelShutdown 请求关闭内核(systemd 不再拉起)。

KernelControlAPI.RequestKernelRestart

RequestKernelRestart(reason string) error

RequestKernelRestart 请求重启内核(热重启,guard 重拉)。

kernel.go:34

KernelControlAPI.RequestKernelShutdown

RequestKernelShutdown(reason string) error

RequestKernelShutdown 请求关闭内核(systemd 不再拉起)。

kernel.go:36

KnowledgeAPI

KnowledgeAPI provides access to the knowledge store.

方法 说明
Add
List
Search

KnowledgeAPI.Add

Add(name, content string) error

knowledge.go:6

KnowledgeAPI.List

List() ([]string, error)

knowledge.go:7

KnowledgeAPI.Search

Search(query string, topK int) ([]*Knowledge, error)

knowledge.go:5

LLMAPI

LLMAPI provides access to the LLM provider manager.

方法 说明
CurrentSource
ListSources
SetSource

LLMAPI.CurrentSource

CurrentSource() string

llm.go:7

LLMAPI.ListSources

ListSources() []string

llm.go:5

LLMAPI.SetSource

SetSource(name string) error

llm.go:6

MemoryAPI

MemoryAPI provides access to the graph memory (entity-relation store).

方法 说明
Commit
Introspect
MergeEntities
Purge
Recall

MemoryAPI.Commit

Commit(triples []Triple) error

memory.go:6

MemoryAPI.Introspect

Introspect() (map[string]interface{}, error)

memory.go:7

MemoryAPI.MergeEntities

MergeEntities(source, target string) (int, error)

memory.go:8

MemoryAPI.Purge

Purge(criteria map[string]string, mode string) (int, error)

memory.go:9

MemoryAPI.Recall

Recall(query []string, depth int) ([]Entity, []Relation, error)

memory.go:5

Plugin

Plugin is the interface every plugin must implement.

方法 说明
Name
Start
Stop

Plugin.Name

Name() string

plugin.go:15

Plugin.Start

Start(sdk *PluginSDK) error

plugin.go:16

Plugin.Stop

Stop() error

plugin.go:17

PluginMgrAPI

PluginMgrAPI 提供插件管理能力(外部插件可调用)。 由 bridge 注入 dispatch 实现,走 C ABI CORE_PLUGIN_RELOAD_ONE 等。

方法 说明
IsPluginDisabled IsPluginDisabled 查询插件是否被持久禁用。
IsPluginRuntimeStopped IsPluginRuntimeStopped 查询插件是否处于运行态关闭。
ListLoadedPlugins ListLoadedPlugins 列出已加载插件。
ReloadOne ReloadOne 重载单个插件(停止后重新加载)。
StartPlugin StartPlugin 运行态启动:现在拉起,不解除持久禁用。
StopPlugin StopPlugin 运行态关闭:现在停掉,不改持久状态。重启后按

PluginMgrAPI.IsPluginDisabled

IsPluginDisabled(name string) bool

IsPluginDisabled 查询插件是否被持久禁用。

plugin.go:537

PluginMgrAPI.IsPluginRuntimeStopped

IsPluginRuntimeStopped(name string) bool

IsPluginRuntimeStopped 查询插件是否处于运行态关闭。

plugin.go:564

PluginMgrAPI.ListLoadedPlugins

ListLoadedPlugins() []string

ListLoadedPlugins 列出已加载插件。

plugin.go:535

PluginMgrAPI.ReloadOne

ReloadOne(name string) error

ReloadOne 重载单个插件(停止后重新加载)。

plugin.go:533

PluginMgrAPI.StartPlugin

StartPlugin(name string) error

StartPlugin 运行态启动:现在拉起,不解除持久禁用。

被持久禁用的插件会被拒绝(提示先用 EnablePlugin)—— 持久禁用是用户的明确决定,运行态操作不该绕过它。

plugin.go:562

PluginMgrAPI.StopPlugin

StopPlugin(name string) error

StopPlugin 运行态关闭:现在停掉,不改持久状态。重启后按 auto_start + disabled_plugins 正常决定。

拒绝作用于 pluginmgr 自身与 webui(自锁/失联,内核层面拦死)。

plugin.go:557

RawFilesAPI

RawFilesAPI 让插件读写随包分发的资源(rawfile)。

是什么

hmapdev build 会把工程根 rawfile/ 下的全部内容(以及 plg.json rawfiles 声明的外部路径)打进 .hmap;内核解包时保留路径,落到:

<插件目录>/rawfile/<原相对路径>

本接口就是插件读这些资源的唯一入口。

为什么不让插件自己拼路径

插件不知道自己的安装目录:内核的 PluginDir() 是给插件管理器用的, 不暴露给插件自身。而且"包内资源"与"运行期生成的文件"必须分开 —— 前者只读、随升级替换,后者可写、跨升级保留。混在一个目录里, 升级时会误删运行期数据,或把旧资源留在原地。

所以:

RawFiles().Read("main/prompt.txt")   → 读**包内**资源(只读)
RawFiles().WorkDir()                 → 拿**可写**工作目录(运行期数据放这)

安全边界

所有路径都相对 rawfile/ 根,必须拒绝路径穿越(../)。 资源文件来自包,包可能被替换 —— 让插件能借它读到 /etc/shadow 就是一个真实的提权面。

方法 说明
Dir Dir 返回包内资源的根目录(只读)。
Exists Exists 报告某个资源是否存在。
List List 列出某个目录下的条目(相对 rawfile/ 根;"" 表示根)。
Read Read 读一个包内资源(路径相对 rawfile/ 根)。
WorkDir WorkDir 返回一个可写目录,供插件放运行期生成的文件。

RawFilesAPI.Dir

Dir() string

Dir 返回包内资源的根目录(只读)。

给需要把整个目录交给外部工具的插件用(如把 node 目录的路径 传给 exec.Command)。不要往这里写东西 —— 它在包里,随升级替换。

rawfiles.go:49

RawFilesAPI.Exists

Exists(rel string) bool

Exists 报告某个资源是否存在。

rawfiles.go:43

RawFilesAPI.List

List(rel string) ([]string, error)

List 列出某个目录下的条目(相对 rawfile/ 根;"" 表示根)。

返回的名字是相对 path 的单层名(不是全路径),目录名带 / 后缀。

rawfiles.go:40

RawFilesAPI.Read

Read(rel string) ([]byte, error)

Read 读一个包内资源(路径相对 rawfile/ 根)。

例:Read("main/prompt.txt") 读 <插件目录>/rawfile/main/prompt.txt。

rawfiles.go:35

RawFilesAPI.WorkDir

WorkDir() string

WorkDir 返回一个可写目录,供插件放运行期生成的文件。

与 Dir() 分开的原因:Dir() 里的东西随升级被整体替换, 运行期数据放那里会在下次升级时丢失。

rawfiles.go:55

SecretsAPI

SecretsAPI 是插件访问内核变量库的接口。

为什么要有它(去重)

在此之前,commondev 插件自带了一整套加密存储(secrets.json + AES-256-GCM + 别名表 + ${ENV} 解析),与内核的 internal/security 完全重复:

           内核 security.Store        插件 unifiedSecretFile
落盘        secrets.json               secrets.json
加密        AES-256-GCM                AES-256-GCM
别名        HMAC 自动派生              用户指定
引用语法     @{sec_xxx}                 @{name}

重复的代价不只是代码量:两套变量库互不可见 —— 内核拦截时登记的 变量,插件看不到;插件登记的,内核拦截不认识。结果是同一个凭据可能 在两处各存一份,而使用者不知道哪个才是权威。

★ 安全设计:明文的边界

内核的硬约束 S1 是「不提供取回明文的接口」—— 理由是一旦 LLM 能取回, 它就能把明文写回上下文,整层防护作废。

那为什么插件可以拿到明文?因为边界不在插件侧:

① 内核在**插件 handler 之前**就把工具参数里的 @{别名} 填好了
   (`StageHost.ExecuteTool` → `security.FillMap`)—— 插件本来
   就能从 args 里读到明文,这是设计使然(否则插件无法用凭据)。
② 明文若被插件**返回**给内核,会被两处拦回别名:
   · `ContextEvent` 的 scrub(记忆/历史侧路)
   · provider 装饰器(出向,LLM 永不看到)

两条都有判据钉住(leakback_test.go)。所以:

**暴露 Fill 不新增泄漏面** —— 它不是「开了一个口子」,
而是把「本来就存在的能力」从各插件自己实现改为统一接口。

⚠️ 但绝不能暴露 Reveal(alias) -> 明文 这样的按名字直取接口: 那会让插件(以及通过插件操纵它的 LLM)枚举别名并逐个取值。 本接口刻意没有这样的方法 —— 见下面 Fill 与 List 的签名。

与 Encrypt/Decrypt 的区别

插件常把凭据存进自己的配置文件(如 pi 的 auth.json)。那些文件权限 0600 但明文落盘。Encrypt/Decrypt 提供 at-rest 加密, 密钥由内核统一管理,插件不必自持密钥(自持会让密钥散落、无法轮换)。

方法 说明
Available Available 报告变量库是否可用(内核未启用安全模块时为 false)。
Decrypt Decrypt 解密由 Encrypt 产生的串。
Encrypt Encrypt 加密一段字节用于插件自己落盘(密钥由内核管理)。
Fill Fill 把一段文本里的 @{别名} 换成明文。
Forget Forget 按别名删除一个变量。
List List 列出变量库的元信息(别名/来源/备注/命中次数)—— 不含明文。
Store Store 把一个凭据登记进内核变量库,返回其别名。

SecretsAPI.Available

Available() bool

Available 报告变量库是否可用(内核未启用安全模块时为 false)。

★ 插件应据此响亮失败而不是静默降级 —— 例如 「密钥不可用 ⇒ 拒绝明文存储」。

secrets.go:100

SecretsAPI.Decrypt

Decrypt(stored string) ([]byte, error)

Decrypt 解密由 Encrypt 产生的串。

secrets.go:94

SecretsAPI.Encrypt

Encrypt(plain []byte) (string, error)

Encrypt 加密一段字节用于插件自己落盘(密钥由内核管理)。 返回的串带版本前缀,可直接存文件。

secrets.go:91

SecretsAPI.Fill

Fill(text string) (out string, replaced int, aliases []string)

Fill 把一段文本里的 @{别名} 换成明文。

用途:插件需要把明文喂给不经过内核参数回填的地方 —— 例如 commondev 的 enc_shell 要把命令经 stdin 送给 bash (不是 argv,避免本机其他进程从 /proc//cmdline 读到明文)。

★ 为什么这个方法是安全的(而不是"开了个取回明文的口子"): 插件本来就能从 args 里拿到内核填好的明文 —— 内核在 handler 之前就填了。Fill 只是让插件能对自己构造的文本 做同样的事(如读文件内容、拼一段脚本)。

它不是按名字直取:要填谁必须先在文本里写出那个别名, 而别名本身不泄漏明文(HMAC 派生)。想枚举就得先 List, 而 List 只给元信息 —— 拿到别名后仍只能用,不能读回。

返回替换处数与用到的别名(便于插件做输出脱敏时的比对)。

secrets.go:87

SecretsAPI.Forget

Forget(alias string) error

Forget 按别名删除一个变量。

★ 变量库不自动淘汰(用户明确):泄漏控制靠主动删除,不靠超时。 所以凭据用完后应主动删。

secrets.go:69

SecretsAPI.List

List() []SecretInfo

List 列出变量库的元信息(别名/来源/备注/命中次数)—— 不含明文。

★ 刻意返回元信息而非值:调用方拿不到明文,只能引用。

secrets.go:63

SecretsAPI.Store

Store(value, note string) (alias string, err error)

Store 把一个凭据登记进内核变量库,返回其别名。

之后在工具参数里写 @{别名},内核会在调用插件前换回真实值。

★ 只返回别名,不回显明文 —— 避免插件(或它转述给 LLM 的内容) 顺带把明文带出去。

note 是给人看的用途说明(如「Gitea 推送令牌」)。

secrets.go:58

SecurityAPI

SecurityAPI 是插件声明自身安全策略的接口。

为什么需要它(用户裁定 2026-10-26,2026-10-29 补齐实现)

插件可以声明自身安全限制策略,并在插件自身 inputch 触发时携带参数 选择其中一种 —— 正好对应 QQ 插件内部 owner 与非 owner 的差别对待。

这是可选参数,不声明安全策略则使用内核默认安全策略。

分工:插件只声明,内核只执行

插件:DeclareProfile / DeclareProfiles            (只声明,不判定)
内核:CheckTool / CheckToolFor / ActivateProfile  (只执行,不信任插件)

★ 为什么判定必须在内核:插件崩溃或被重载时,声明不能跟着消失 (否则"挂掉插件"就是绕过策略的手段);而且判定要与全局基线、主体策略 走同一条链 —— 分散到各插件就是"被约束者自己决定约束", 本仓已有实测教训(QQ 的 beforeToolcall 自己判身份/白名单/硬私有, 结果每个插件各写一套、拒绝语义散落、插件崩了策略就没了)。

声明 vs 全局基线:两层,职责不重叠

① 全局基线(部署方配置 core.security.*):不受插件影响,防"插件自己放水"
② 档位(本接口):单次请求的**收窄**

档位只能收窄:内核的判定顺序是「全局 deny → 档位 deny → 主体 deny → 全局 allow → 档位 allow」,全局黑名单永远先判,档位绕不过去。

缺省语义(重要)

不声明 / 不传 = 不使用档位,一切判定沿用内核默认策略

也就是说这是可选能力:不用的插件行为与之前完全一致。 而一旦传了未声明的档位名,内核会报错而不是静默回落 —— 拼错档位名却默默用默认策略是安全事故的经典形态。

典型用法(QQ 插件)

// Start 阶段:声明两个档位(只声明,不判定)
s.Security().DeclareProfiles([]SecurityProfile{
    {Name: "qq.owner", Owner: "qq", AllowTools: []string{"*"}},
    {Name: "qq.guest", Owner: "qq",
     DenyTools: []string{"cmd_*", "files_write"},
     SensitiveWords: []string{"内部代号"}},
})

// 注入阶段:按发送者身份选择档位
s.InjectTextOpts(source, channel, text, sdk.InjectOptions{
    SecurityProfile: "qq.guest",
})

名字约束

档位名必须是 <owner>.<档位>,其中 owner 等于插件名。这条在内核侧 强制(否则插件 A 能声明 qq.owner 并夺走插件 B 的档位语义)。

方法 说明
DeclareProfile DeclareProfile 声明一个命名档位。同名重复声明会覆盖(同一 owner
DeclareProfiles DeclareProfiles 批量声明(任一失败即整体失败,便于早失败)。

SecurityAPI.DeclareProfile

DeclareProfile(p SecurityProfile) error

DeclareProfile 声明一个命名档位。同名重复声明会覆盖(同一 owner 只能覆盖自己的档位,不能覆盖别人的)。

返回 error 的情形: · 名字不符合 <owner>.<档位> 形式 · owner 与本插件名不一致(试图声明别人的档位) · 安全模块未启用(此时没有可声明的策略容器)

security.go:66

SecurityAPI.DeclareProfiles

DeclareProfiles(ps []SecurityProfile) error

DeclareProfiles 批量声明(任一失败即整体失败,便于早失败)。

为什么提供批量版:QQ 这类插件有 3~4 个档位,逐个判断 error 会让 Start 里堆满重复代码;而"声明到一半失败"是更难排查的状态。

security.go:72

SettingsAPI

方法 说明
DataDir DataDir returns the plugin-specific data directory (guaranteed to exist):
Defs Defs returns config definitions matching the prefix.
Dump Dump returns all config values.
Get Get reads the plugin's own config value (config_ table).
GetCore GetCore reads the core config table.
GetPlugin GetPlugin reads another plugin's config table.
GetSecret GetSecret reads a value written by SetSecret (transparently decrypted).
List List returns all keys matching the given prefix.
ListCore ListCore lists core config keys matching the prefix.
ListPlugin ListPlugin lists another plugin's config keys matching the prefix.
Plugins Plugins returns a list of all plugin config namespaces.
RegisterDef RegisterDef registers a config definition for UI display.
Set Set writes a config value to the plugin's own config table.
SetCore SetCore writes to the core config table.
SetPlugin SetPlugin writes to another plugin's config table.
SetSecret SetSecret writes a value that is ALWAYS encrypted at rest, regardless of

SettingsAPI.DataDir

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:40

SettingsAPI.Defs

Defs(prefix string) []*ConfigDef

Defs returns config definitions matching the prefix.

settings.go:55

SettingsAPI.Dump

Dump() map[string]interface{}

Dump returns all config values.

settings.go:58

SettingsAPI.Get

Get(key string) (interface{}, error)

Get reads the plugin's own config value (config_ table).

settings.go:5

SettingsAPI.GetCore

GetCore(key string) (interface{}, error)

GetCore reads the core config table.

settings.go:29

SettingsAPI.GetPlugin

GetPlugin(plugin, key string) (interface{}, error)

GetPlugin reads another plugin's config table.

settings.go:43

SettingsAPI.GetSecret

GetSecret(key string) (string, error)

GetSecret reads a value written by SetSecret (transparently decrypted).

settings.go:23

SettingsAPI.List

List(prefix string) ([]string, error)

List returns all keys matching the given prefix.

settings.go:26

SettingsAPI.ListCore

ListCore(prefix string) ([]string, error)

ListCore lists core config keys matching the prefix.

settings.go:35

SettingsAPI.ListPlugin

ListPlugin(plugin, prefix string) ([]string, error)

ListPlugin lists another plugin's config keys matching the prefix.

settings.go:49

SettingsAPI.Plugins

Plugins() []string

Plugins returns a list of all plugin config namespaces.

settings.go:61

SettingsAPI.RegisterDef

RegisterDef(def ConfigDef)

RegisterDef registers a config definition for UI display.

settings.go:52

SettingsAPI.Set

Set(key string, value interface{}) error

Set writes a config value to the plugin's own config table.

settings.go:8

SettingsAPI.SetCore

SetCore(key string, value interface{}) error

SetCore writes to the core config table.

settings.go:32

SettingsAPI.SetPlugin

SetPlugin(plugin, key string, value interface{}) error

SetPlugin writes to another plugin's config table.

settings.go:46

SettingsAPI.SetSecret

SetSecret(key string, value interface{}) error

SetSecret writes a value that is ALWAYS encrypted at rest, regardless of the key name. Use it when the key name is neutral (e.g. "blob", "conn") and so cannot be detected as sensitive automatically.

Returns an error (not a silent plaintext write) when no master key is installed — a plugin that believes it stored a secret must not end up with plaintext on disk.

The companion read is GetSecret. ConfigDef.Secret/Type=="password" and sensitive key names are encrypted automatically and do NOT need this.

settings.go:20

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
GetPerson
GetRelations
GetTrait
ListPersons

SocialAPI.GetNetwork

GetNetwork(name string, depth int) ([]*PersonProfile, error)

memory.go:104

SocialAPI.GetPerson

GetPerson(name string) (*PersonProfile, error)

memory.go:101

SocialAPI.GetRelations

GetRelations(name string) ([]SocialRelation, error)

memory.go:103

SocialAPI.GetTrait

GetTrait(name, trait string) (string, bool)

memory.go:102

SocialAPI.ListPersons

ListPersons() ([]string, error)

memory.go:105

TextMemoryAPI

TextMemoryAPI provides access to chronological text event storage.

方法 说明
Append

TextMemoryAPI.Append

Append(evt TextEvent) error

memory.go:44

AudioURL

type AudioURL struct { URL string `json:"url"` }

plugin.go:1247

AutoStartOnDemand

const AutoStartOnDemand

AutoStartOnDemand 表示启用但不自启:待 agent 显式拉起。

★ 用它的插件必须配 skill,否则 agent 无从得知要先拉起。

autostart.go:68

AutoStartOnKernel

const AutoStartOnKernel

AutoStartOnKernel 表示伴随内核启动(缺少该字段时的默认形态)。

autostart.go:64

EffectiveProxyAuth

func EffectiveProxyAuth(auth string) string

EffectiveProxyAuth 返回生效的鉴权模式(空串与旧值都归一化为 ProxyAuthAgent)。

归一化旧值是关键:下游一律只可能拿到 ProxyAuthAgent,不必各自兼容旧值 (否则每个判断点都要写 == ProxyAuthAgent || == ProxyAuthAgentLegacy, 漏一处就是鉴权绕过)。

proxy.go:192

ToolError.Error

func (e *ToolError) Error() string

Error 实现 error,便于工具同时走 (ToolError, error) 通道。

plugin.go:393

FacilityAgent

const FacilityAgent

FacilityAgent 是主 Agent 就绪(最晚的一个,通常在插件加载前)。

facilities.go:63

FacilityConfig

const FacilityConfig

FacilityConfig 是配置快照(cfg)就绪。

facilities.go:37

FacilityDevice

const FacilityDevice

FacilityDevice 是设备/工具数据面就绪(工具注册与分发、inputch、outputch)。

facilities.go:71

FacilityDocStore

const FacilityDocStore

FacilityDocStore 是文档记忆就绪。

facilities.go:53

FacilityKernelIO

const FacilityKernelIO

FacilityKernelIO 是 IOManager + EventBus 就绪。

facilities.go:39

FacilityKnowledge

const FacilityKnowledge

FacilityKnowledge 是知识库就绪。

facilities.go:55

FacilityLLM

const FacilityLLM

FacilityLLM 是多源 Provider 管理就绪。

facilities.go:51

FacilityLua

const FacilityLua

FacilityLua 是 Lua 协议适配 VM 就绪。

facilities.go:43

FacilityMemory

const FacilityMemory

FacilityMemory 是图记忆(含索引/社交图/蒸馏)就绪。

facilities.go:41

FacilityMultimodal

const FacilityMultimodal

FacilityMultimodal 是多模态向量空间就绪。

facilities.go:57

FacilityONNX

const FacilityONNX

FacilityONNX 是依存句法分析器就绪。

facilities.go:59

FacilitySecurity

const FacilitySecurity

FacilitySecurity 是安全防火墙就绪。

它不是"某项能力",而是横跨 Core 与全部外部模块的数据面闸门: Core → 外部、外部 → Core 的每条数据流都经它审查。需要被安全审查后 才能访问外部模块的插件声明依赖它。

facilities.go:69

FacilityStageHost

const FacilityStageHost

FacilityStageHost 是阶段管道 + 插件注册表就绪。

facilities.go:61

FacilitySupervisor

const FacilitySupervisor

FacilitySupervisor 是代理生命周期守护就绪。

facilities.go:45

FacilityTextMemory

const FacilityTextMemory

FacilityTextMemory 是文本记忆就绪。

facilities.go:49

FacilityTracker

const FacilityTracker

FacilityTracker 是变更追踪(overlayfs)就绪。

facilities.go:47

ImageURL

type ImageURL struct { URL string `json:"url"` Detail string `json:"detail,omitempty"` }

plugin.go:1242

PluginSDK.KernelControl

func (s *PluginSDK) KernelControl() KernelControlAPI

KernelControl returns the kernel control API (restart / shutdown).

可能为 nil(宿主没接这条通道,或插件没有 kernel-control 能力组)。 调用方必须判 nil —— 拿到 nil 时应当如实告知用户"当前环境不支持", 而不是假装点了没反应的按钮还能用。

plugin.go:1038

KnownFacilities

func KnownFacilities() []string

KnownFacilities 返回本 SDK 已知的全部设施名(供工具链校验插件声明)。

★ 它让打包期就能发现"插件声明了不存在的设施名" —— 比运行期发现早得多 (运行期发现意味着用户已经装上了那个永不启动的插件)。

facilities.go:78

MemItem

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:349

NormalizeProxyHost

func NormalizeProxyHost(pluginName string) string

NormalizeProxyHost 由插件名派生默认 Host 标签。

下划线转连字符:插件名允许下划线(huawei_smarthome),但 DNS label 不允许, 直接用会导致该子域名无法解析——这里统一转换,避免每个插件各自碰运气。

proxy.go:225

OnDemandNeedsSkill

const OnDemandNeedsSkill

OnDemandNeedsSkill 说明 auto_start=false 的插件为什么必须有 skill。

这条不是"建议"而是功能完整性的一部分:

插件写了 auto_start=false 但没配 skill
  ⇒ 该插件的工具不出现在工具的初始清单里
  ⇒ 模型不知道它存在
  ⇒ 它永远不会被调用,也永远不会被拉起

也就是说,缺少 skill 的按需插件等价于没装,但比没装更糟 —— 它占着磁盘和安装记录,让人以为它可用。

autostart.go:82

PluginSDK

type PluginSDK struct { name string regTool ToolRegistrar regToolCtx ToolRegistrarCtx regStage StageRegistrar regAPI APIRegistrar regO …

PluginSDK is the main API surface provided to plugins at runtime. It wraps tool registration, settings, memory, knowledge, LLM, and IO injection.

plugin.go:611

ProxyAuthAgent

const ProxyAuthAgent

ProxyAuthAgent 表示由内核统一保护:浏览器走门户会话 (HologramAgent session cookie),非浏览器客户端走 X-API-Key。 两者都没有时返回 401,而不是把请求透传给上游。

proxy.go:149

ProxyAuthAgentLegacy

const ProxyAuthAgentLegacy

ProxyAuthAgentLegacy 是旧值的字面量,只用于兼容读取,不要在新声明里写它。

为什么必须继续接受(2026-10-22):Auth 是写进 plugin.json 的持久化 声明值,存量插件的配置文件里已经写着旧值(本机实测 huawei_smarthome 的 plugin.json:19 就是 "auth": "homeagent")。删掉这个分支 ⇒ 那些插件在加载 时被判为非法声明,反代直接不生效,且症状是"插件好好的但页面打不开"。

这正是 sdkModuleNames 处理 module 路径改名时的同一条教训:协议值的 改名必须保留旧值的读取路径,否则存量数据无声失效。

proxy.go:167

ProxyAuthHomeAgent

const ProxyAuthHomeAgent

ProxyAuthHomeAgent 是 ProxyAuthAgent 的旧名,仅为存量源码保留。

Deprecated: 用 ProxyAuthAgent。存量插件的 plugin.json 里写的旧值仍被 ValidProxyAuth/EffectiveProxyAuth 接受(见 ProxyAuthAgentLegacy)。

proxy.go:174

ProxyAuthNone

const ProxyAuthNone

ProxyAuthNone 表示不经内核鉴权,直接把请求转发给上游。

适用场景:上游自己有鉴权且调用方不是浏览器(设备/嵌入式客户端), 或上游是刻意公开的服务。选用它意味着信任上游自身的鉴权, 且该服务在网络层可达范围内对所有人开放。

proxy.go:156

ProxyDef

type ProxyDef struct { // Name 是同一插件内多条声明的唯一标识(如 "ui"、"api")。 // 运行期由 RegisterProxy 的第一个参数填入;声明式由 …

反向代理声明:插件告诉 HologramAgent「我起了个 HTTP 服务,请把它反代出去」。

为什么需要:插件自带 Web UI / HTTP API 时,监听地址在插件自己的配置里 (如 127.0.0.1:12100),外部无从得知;而 webui 的对外端口通常只有一个 (默认 :8080,且常经 frp 单端口隧道穿透)。没有声明机制时,用户只能 「知道端口 + 自己配转发」,插件换端口就失效。

设计取舍——声明式而非注册式:声明写在 plugin.json 里,由 HologramAgent 在加载插件时读取聚合,而不是让插件在运行期调 API 注册。理由: 1. 静态可发现:未启动/已崩溃的插件,其服务声明依然可见(可给出准确报错 「插件 X 声明了 ui 但目标 127.0.0.1:12100 不可达」,而不是静默 404); 2. 可版本化:声明随插件包一起分发、可 diff、可审计; 3. 旧内核无害:manifest 解析忽略未知字段,未支持该能力的 HologramAgent 读旧 插件、或旧 HologramAgent 读新插件都不会报错。

与 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
                  —— 静默错路由,页面能开但功能全坏。

于是「同一个插件必须同时支持两种形态」这条要求,等价于:

**插件内部一律使用相对路径**(或基于 <base>/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

type ProxyRegistrar func(name string, def ProxyDef)

ProxyRegistrar 由内核注入(与 ToolRegistrar / InputChannelRegistrar 同族)。 插件不直接调它,用 RegisterProxy。

为什么需要运行期注册(明明有 plugin.json 自动发现):内置插件 (编译进内核、没有独立插件目录与 plugin.json,如 remotedevice)扫不到; 而它们恰恰最需要被反代出去(设备网关就是内置的)。两种来源互补: - 外部插件 → plugin.json 的 proxies(静态,未启动也可见) - 内置插件 → RegisterProxy(运行期,随 Start 注册)

proxy.go:329

PluginSDK.RawFiles

func (s *PluginSDK) RawFiles() RawFilesAPI

SetMemoryAPI sets the memory API (called by the core at startup). RawFiles 返回随包分发资源的读取接口(内核装配时注入)。

未注入时返回 nil —— 那是旧内核或测试环境。插件应判空后响亮失败, 而不是拿一个空实现继续(会表现为"资源读出来是空的",与"资源不存在"难区分)。

plugin.go:941

PluginSDK.RegisterProxy

func (s *PluginSDK) RegisterProxy(name string, def ProxyDef)

RegisterProxy 声明一个需要 HologramAgent 反代出去的服务。

与 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:358

PluginSDK.RegisterToolCtx

func (s *PluginSDK) RegisterToolCtx(name string, def ToolDef, handler ToolHandlerCtx) error

RegisterToolCtx 注册一个可取消的工具(可选,长任务推荐)。

宿主不支持时自动降级到 RegisterTool 并包一层 context.Background(): 功能不丢,只是拿不到取消。插件不必为此写两套代码。

plugin.go:770

SDKVersion

var SDKVersion

SDKVersion 是对外暴露的 SDK 版本号。

plugin.go:11

ScenePolicyAuto

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:132

ScenePolicyNone

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:133

SecretInfo

type SecretInfo struct { Alias string `json:"alias"` Source string `json:"source"` // intercepted | submitted Note string `json:"note,omitempty"` CreatedAt stri …

SecretInfo 是一条变量的元信息(不含明文)。

secrets.go:104

PluginSDK.Secrets

func (s *PluginSDK) Secrets() SecretsAPI

Social returns the social graph API (may be nil if not available). Secrets returns the kernel secret vault API.

★ 内核未启用安全模块时它不是 nil,而是 Available() 返回 false 的 实现 —— 这样插件可以统一地"先问 Available 再决定",不必到处判空。

plugin.go:727

PluginSDK.Security

func (s *PluginSDK) Security() SecurityAPI

Security 返回策略声明面(插件声明安全档位;判定由内核执行)。

★ 只声明、不判定:本接口没有 CheckTool 之类的方法 —— 那属于内核。 插件自判会让策略分散到每个插件、且插件崩溃即策略失效。

plugin.go:737

SecurityProfile

type SecurityProfile struct { // Name 是档位名,必须形如 `<owner>.<档位>`(如 "qq.guest")。 Name string // Owner 是声明者(插件名)。与 Name 的前缀� …

SecurityProfile 是一个命名安全档位的声明。

与 ToolPolicy 类配置的区别:档位是运行时可选的(按 inputch 输入 携带的参数选择),不是全局生效的。

security.go:79

PluginSDK.SetProxyRegistrar

func (s *PluginSDK) SetProxyRegistrar(r ProxyRegistrar)

SetProxyRegistrar 由内核注入。插件不直接调它(与 SetInputChannelRegistrar 同族)。

proxy.go:332

PluginSDK.SetToolRegistrarCtx

func (s *PluginSDK) SetToolRegistrarCtx(r ToolRegistrarCtx)

SetToolRegistrarCtx 由运行时注入带 ctx 的注册口。

插件业务代码不需要调用它:proc 模板在自己的初始化里注入, 之后插件用 RegisterToolCtx 注册的工具就会进入可取消路径。

plugin.go:760

ToolError

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:381

ToolHandlerCtx

type ToolHandlerCtx func(ctx context.Context, args map[string]interface{}) (interface{}, error)

ToolHandlerCtx 是可选的带取消语义 handler。

插件作者不需要改代码

内核超时后要让插件停止执行,这件事由运行时接住:proc 模板收到 call.cancel 就 cancel 对应调用的 ctx。用旧签名的 handler 照旧跑完, 行为与今天完全一致;只有主动想配合取消的长任务工具才需要这个类型。

什么时候值得用

执行体里有长阻塞(HTTP、等子进程、轮询设备)时,用 Ctx 版可以让取消 立即生效,而不是白跑完再丢结果。纯内存计算(毫秒级)没有必要。

约定

  • ctx 只在本次调用内有效,不要缓存;
  • 把 ctx 传给下游阻塞调用(http.NewRequestWithContext / exec.CommandContext);
  • 返回时用 defer 清理;不要假设调用方还在等结果(它可能已经走了)。

plugin.go:41

ToolRegistrarCtx

type ToolRegistrarCtx func(name string, def ToolDef, handler ToolHandlerCtx) error

ToolRegistrarCtx 是带 ctx 的注册口(由运行时/宿主注入)。

plugin.go:54

PluginSDK.UnregisterOutputChannel

仅内核内置插件可用

外部插件的桥接模板只注入 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,真正生效。外部插件要让通道下线,只能重载插件。

func (s *PluginSDK) UnregisterOutputChannel(name string) error

UnregisterOutputChannel 注销一个输出通道(动态通道随资源生灭时必须调用)。

plugin.go:874

ValidProxyAuth

func ValidProxyAuth(auth string) bool

ValidProxyAuth 校验 Auth 取值;空串合法(等价 ProxyAuthAgent)。

同时接受旧值 ProxyAuthAgentLegacy —— 存量 plugin.json 里写的仍是它。

proxy.go:179

ValidProxyHostLabel

func ValidProxyHostLabel(label string) bool

ValidProxyHostLabel 校验子域名标签是否合法(DNS label 规则)。

独立成导出函数:插件作者在写声明时、HologramAgent 在加载时、工具链在打包时 都要用同一套规则判定,避免三处各写一份而互相不一致。

proxy.go:203

ValidScenePolicy

func ValidScenePolicy(policy string) bool

ValidScenePolicy 校验场面策略取值;空串等价于 ScenePolicyAuto。

plugin.go:137

ValidateProxyDef

func ValidateProxyDef(d ProxyDef) string

ValidateProxyDef 校验一条反代声明,返回人类可读的错误说明(合法时为空)。

为什么要在 SDK 里做校验:HologramAgent 加载插件时必须能明确拒绝坏声明并说明 原因(而不是静默忽略导致用户以为配好了);插件作者也需要在本地就能查出 拼错的 Target/Host。同一套规则两端共用。

proxy.go:252