Files
homeagent-sdk/sdk/plugin.go
JianFeeeee ba49dfda44 feat(sdk): 注入行为的记忆/裁剪标志位(纯追加)+ plugindev 退出码修复 + 示例 hmap 随发版
## 1. 注入标志位(公开 API 纯追加,无签名变更)

给注入行为补上工具早已有的两类声明位,并让通道定义也带上:
- `InjectOptions{NoMemory, ContextPolicy, CleanerName}`
- `IOInjector` 新增六个 `*Opts` 变体(排队/中断/同步 × 纯文本/带媒体)
- `ChannelDef.ContextPolicy`,并**补上 JSON tag**(Cleaner 标 `json:"-"`)

零值 InjectOptions 与旧的三参数方法完全等价(记入记忆 + 不裁剪),
存量插件不需要改一行、也不需要重编;旧方法保留为转发到零值 opts 的语法糖。

三条设计要点:
- **默认不裁剪**:裁剪会归档丢弃低相关事件,必须显式声明(ContextPolicy=prune)。
- **中断也允许声明 prune**(已确认):中断同样携带内容进上下文。
- `CleanerName`:注入的 source 未必是注册过的输入通道名,而注入内容常带
  ANSI/JSON 包装;允许显式指定用哪个已注册 cleaner 清洗。

顺带修掉一个易静默丢字段的坑:`ChannelDef` 原来没有 JSON tag,只能手写字段
白名单跨进程传(`{"NoMemory": ...}`),新增字段会被丢掉。现在模板整体传 `def`。

## 2. 示例调用点统一写明意图
rss/memo/calendar/qq 的中断注入显式 `NoMemory: true`(行为等价,写清语义)。

## 3. plugindev 出错却 exit 0(真缺陷)
`buildBundle`/`buildTarget` 遇错只 Printf 后 return,`cmdBuild` 返回 void,
于是**构建失败也退 0**。实测中一个示例的 windows 目标编译失败,批量脚本却报
「17/17 全绿」,并因此少产出 16 个 .hmap。现在累计 `buildFailed` 并以非零退出。

## 4. 平台策略:插件目标去掉 windows
homed 已放弃 Windows 原生(见核心仓 cmd/homed/platform_windows.go:插件体系依赖
fd 继承 + 统一共享内存区的段内偏移解引用,Windows 句柄模型无法表达),
插件只运行在 homed 能跑的平台上,故 `allBundleTargets` 去掉 windows,
并对 windows 目标给出**可执行的报错**(指引 WSL2),而不是让它死在一句
`undefined: attachUnifiedShm` 上。

## 5. 发版附带各示例插件的 .hmap
新增 `package/build-examples.sh` 并接入 `package/build.sh`(组件 all|plugindev|examples):
- 用**刚构建出来的**那把工具链构建示例,保证与本次发版同源
- 逐平台 `--no-bundle --target <os/arch>`(bundle 会连 windows 一起编)
- 判成功同时看**退出码 + 产物存在**
- 全部产物齐了才 `sha256sum`(边打边算会漏掉后生成的包)
- 有任一失败即整体失败,不生成 SHA256SUMS

## 6. 版本
SDK 仍为 1.2.0(main 是下一个未发布中版本);1.2.0 条目补记本次新增接口,
并注明新标志位需核心 1.2.0+(旧核心会忽略这些字段,不报错但不生效)。
2026-09-11 20:29:30 +08:00

759 lines
27 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package sdk
import (
"sync"
"gitcode.com/JianFeeeee/homeagent-sdk/meta"
)
// SDKVersion 是对外暴露的 SDK 版本号。
var SDKVersion = meta.Version
// Plugin is the interface every plugin must implement.
type Plugin interface {
Name() string
Start(sdk *PluginSDK) error
Stop() error
}
// ToolHandler is a function that handles a tool call.
type ToolHandler func(args map[string]interface{}) (interface{}, error)
// StageHandler is a function that handles a pipeline stage event.
type StageHandler func(ctx *StageContext) error
// Stage represents a point in the message processing pipeline.
type Stage string
const (
StageOnInput Stage = "on_input"
StagePreAction Stage = "pre_action"
StagePostAction Stage = "post_action"
StageBeforeToolcall Stage = "before_toolcall"
StageAfterToolcall Stage = "after_toolcall"
StageBeforeOutput Stage = "before_output"
StageAfterOutput Stage = "after_output"
)
// 上下文策略:决定一次工具调用/输入/注入是否依据其内容裁剪上下文。
//
// 默认(空串或 ContextPolicyNone**不裁剪**:裁剪会归档丢弃低相关事件,
// 必须由工具/通道/注入点显式声明才发生——否则一个只想往上下文里塞内容的
// 插件会在背后把别人的内容挤掉,且看不出是谁干的。
const (
ContextPolicyNone = "none"
ContextPolicyPrune = "prune"
)
// ValidContextPolicy 校验策略取值;空串等价于 ContextPolicyNone。
func ValidContextPolicy(policy string) bool {
switch policy {
case "", ContextPolicyNone, ContextPolicyPrune:
return true
}
return false
}
// InjectOptions 声明一次注入行为在记忆层与上下文层的表现。
//
// 零值 = 记入记忆 + 不裁剪上下文,与历史行为(三参数注入方法)完全一致,
// 因此调用方只有在确实需要改变行为时才需要填它。
//
// 为什么注入也要这两个标志:注入的内容来源千差万别——轮询到的频道消息
// 属于真实对话(该记),而“任务还在跑”“连接已重连”这类提醒不该污染记忆,
// 也不该把上下文按它的内容裁一遍。按调用点声明比按通道一刀切准确。
//
// NoMemory: 此次注入不参与记忆计算(向量化/关键词提取/蒸馏),原文仍留在上下文
// ContextPolicy: 此次注入后是否依据(清洗后的)内容裁剪上下文;默认不裁剪。
//
// 中断注入也允许声明 prune——它同样会携带内容进入上下文。
//
// CleanerName: 此次注入的内容用哪个**已注册的通道 cleaner** 清洗。
//
// 空串 = 按注入的 source 查通道定义(既有行为)。
// 为什么要能显式指定:注入的 source 未必是注册过的输入通道名,
// 而注入内容往往带 ANSI/JSON 包装,需要清洗后才是有效内容;
// 不指定就只能退到「按 source 查不到就不清洗」。
type InjectOptions struct {
NoMemory bool
ContextPolicy string
CleanerName string
}
// ChannelDef 描述通道在记忆计算层的行为,与 ToolDef.NoMemory/Cleaner 语义一致。
// NoMemory: 此通道输入/输出不参与记忆计算(向量化/关键词提取/蒸馏),但原文保留在上下文中
// Cleaner: 计算层过滤函数,不改原文;仅在向量化/jieba/蒸馏/存档提取关键词时调用
// ContextPolicy: 此通道的输入到达后是否据此裁剪上下文,默认 none不裁剪
//
// JSON tag 是必需的:通道定义要跨进程传给内核,而 Cleaner 是函数(必须忽略)。
// 没有 tag 时既无法整体 marshalfunc 不支持),又会诱使调用方手写字段白名单——
// 那样新增字段会被静默丢掉。
type ChannelDef struct {
NoMemory bool `json:"no_memory,omitempty"`
Cleaner func(string) string `json:"-"`
ContextPolicy string `json:"context_policy,omitempty"`
}
// StageContext provides context for stage handlers.
type StageContext struct {
mu sync.RWMutex
RawMessage string
UserID string
GroupID string
ContextMsgs []map[string]interface{}
LLMText string
ReasoningContent string
TokenUsage map[string]int
ToolCalls []ToolCall
ToolResults []ToolResult
FinalText string
Response *string
Phase Stage
Memory []MemItem
NoMemory bool
Extra map[string]interface{}
Errors []string // 阶段处理过程中的错误信息
}
func (c *StageContext) RLock() { c.mu.RLock() }
func (c *StageContext) RUnlock() { c.mu.RUnlock() }
func (c *StageContext) Lock() { c.mu.Lock() }
func (c *StageContext) Unlock() { c.mu.Unlock() }
func (c *StageContext) IsResponded() bool {
c.mu.RLock()
defer c.mu.RUnlock()
return c.Response != nil
}
// MemItem represents a memory item in stage context.
type MemItem struct {
Role string `json:"role"`
Content string `json:"content"`
Score float64 `json:"score"`
}
// ToolCall represents a model's request to call a tool.
type ToolCall struct {
ID string `json:"id"`
Name string `json:"name"`
Plugin string `json:"plugin,omitempty"`
Arguments map[string]interface{} `json:"arguments"`
}
// ToolResult represents the result of a tool call.
type ToolResult struct {
CallID string `json:"call_id"`
Name string `json:"name"`
Plugin string `json:"plugin,omitempty"`
Success bool `json:"success"`
Result interface{} `json:"result"`
}
// ToolDef describes a tool that the plugin exposes.
type ToolDef struct {
Name string `json:"name"`
Plugin string `json:"plugin,omitempty"`
Description string `json:"description"`
Parameters map[string]interface{} `json:"parameters"`
NoMemory bool `json:"no_memory,omitempty"` // 此工具输出不参与记忆计算,但原文保留
Cleaner func(string) string `json:"-"` // 计算层过滤函数,不改原文;仅在向量化/jieba/蒸馏时调用
ContextPolicy string `json:"context_policy,omitempty"` // 上下文策略:""(默认,不裁剪) / ContextPolicyNone / ContextPolicyPrune
}
// 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.
type IOInjector interface {
InjectInterruptText(source, channel, text string)
InjectText(source, channel, text string)
InjectTextNoMemory(source, channel, text string)
// InjectInputSync 注入输入事件并同步等待 agent 回复,返回回复文本(无回复时返回空串)。
// 用于通道消息的完整闭环:收到入站 → agent 处理 → 回复取回 → 送回通道。
InjectInputSync(source, channel, text string) string
// SetToolBlocks 插件工具注入多模态内容块image_url/audio_url内核在下一条
// tool message 的 content 数组里带上这些块,让模型在后续轮次看到图/听到音频。
SetToolBlocks(blocks []ContentBlock)
InjectInputMedia(source, channel, text string, blocks []ContentBlock)
InjectInputMediaSync(source, channel, text string, blocks []ContentBlock) string
InjectInterruptMedia(source, channel, text string, blocks []ContentBlock)
// 以下 Opts 变体让调用点在**这一次注入**上声明记忆与裁剪行为。
//
// 上面那些不带 opts 的方法等价于传零值 InjectOptions记入记忆 + 不裁剪),
// 保留它们是为了不破坏已有插件;新代码应当用 Opts 变体把意图写清楚。
InjectTextOpts(source, channel, text string, opts InjectOptions)
InjectInterruptTextOpts(source, channel, text string, opts InjectOptions)
InjectInputSyncOpts(source, channel, text string, opts InjectOptions) string
InjectInputMediaOpts(source, channel, text string, blocks []ContentBlock, opts InjectOptions)
InjectInputMediaSyncOpts(source, channel, text string, blocks []ContentBlock, opts InjectOptions) string
InjectInterruptMediaOpts(source, channel, text string, blocks []ContentBlock, opts InjectOptions)
}
// EventType identifies the kind of system event.
type EventType string
const (
EventRawInput EventType = "raw_input"
EventAgentOutput EventType = "agent_output"
EventAgentLLMChain EventType = "agent_llm_chain"
EventToolCall EventType = "tool_call"
EventReasoning EventType = "reasoning"
EventStage EventType = "stage"
EventSystem EventType = "system"
// 流式增量事件token 级):核心 process() 流式化后每收到一个增量块发布。
// 客户端可选订做真逐 token 渲染;聚合事件仍照常发布,旧订阅者不受影响。
EventReasoningDelta EventType = "reasoning_delta"
EventContentDelta EventType = "content_delta"
)
// Event represents a system event published by the kernel.
type Event struct {
Type EventType `json:"type"`
Source string `json:"source"`
Payload map[string]interface{} `json:"payload"`
Timestamp int64 `json:"timestamp"`
}
// EventHandler processes a system event.
type EventHandler func(evt *Event)
// EventSubscriber allows plugins to subscribe to kernel events.
// This is a restricted interface: plugins can subscribe but the kernel
// controls which events are delivered.
type EventSubscriber interface {
Subscribe(eventType EventType, handler EventHandler) func()
}
// PluginMgrAPI 提供插件管理能力(外部插件可调用)。
// 由 bridge 注入 dispatch 实现,走 C ABI CORE_PLUGIN_RELOAD_ONE 等。
type PluginMgrAPI interface {
// ReloadOne 重载单个插件(停止后重新加载)。
ReloadOne(name string) error
// ListLoadedPlugins 列出已加载插件。
ListLoadedPlugins() []string
// IsPluginDisabled 查询插件是否被禁用。
IsPluginDisabled(name string) bool
}
// StageScope controls which events a stage handler receives.
type StageScope int
const (
// StageScopeGlobal receives all stage events (default).
StageScopeGlobal StageScope = 0
// StageScopeOwnTools only receives events for this plugin's own tool calls
// (before_toolcall / after_toolcall only). Other stages degrade to global.
StageScopeOwnTools StageScope = 1
)
// ToolRegistrar registers a tool dynamically.
type ToolRegistrar func(name string, def ToolDef, handler ToolHandler) error
// StageRegistrar registers a stage handler.
type StageRegistrar func(stage Stage, handler StageHandler)
// APIRegistrar registers a plugin API for external access.
type APIRegistrar func(name string) error
// InputChannelRegistrar registers an input channel with its memory behavior.
type InputChannelRegistrar func(name string, def ChannelDef) error
// OutputChannelRegistrar registers an output channel that the output_send tool can use.
type OutputChannelRegistrar func(name string, caps int, desc string, def ChannelDef, handler ToolHandler) error
// Output capability flags
const (
CapText = 1
CapFile = 2
CapImage = 4
CapAudio = 8
CapStructured = 16
)
// PluginSDK is the main API surface provided to plugins at runtime.
// It wraps tool registration, settings, memory, knowledge, LLM, and IO injection.
type PluginSDK struct {
name string
regTool ToolRegistrar
regStage StageRegistrar
regAPI APIRegistrar
regOutput OutputChannelRegistrar
regInput InputChannelRegistrar
io IOInjector
mem MemoryAPI
textMem TextMemoryAPI
docMem DocMemoryAPI
know KnowledgeAPI
llm LLMAPI
sett SettingsAPI
social SocialAPI
events EventSubscriber
plgMgr PluginMgrAPI
// apiMu 保护上面这些由内核注入的 API 字段,以及 autoRestart。
//
// 这些字段的写方与读方天然跨 goroutine
// - 写方是内核(加载/重载插件时注入 API与插件自己SetAutoRestart
// - 读方是插件在 Start() 里起的后台 goroutine轮询、监听、定时器
// 都要拿 injector 往管道里注消息),以及内核 registry —— 它在
// 另一个 goroutine 读 AutoRestart() 决定崩溃后是否重启。
// SetAutoRestart 的文档用法本身就是「连接建立后再决定能否自动重启」,
// 而连接建立通常发生在后台 goroutine 里,于是这对读写必然并发。
//
// sdk/stress_test.go 的 -race 实测确认这是真竞态,不是理论风险。
// 未加锁时的生产表现是偶发 nil 解引用崩溃(读到半个接口值)。
//
// 约定:只在持锁期间取字段值,取完立刻释放再调用。
// 持锁调用会把 InjectInputSync 这类阻塞到 agent 回复(可达数分钟)的
// 方法与 SetIOInjector 串到一起,让插件重载卡死。
apiMu sync.RWMutex
autoRestart bool
stopMu sync.Mutex
stopHandlers []func()
removeMu sync.Mutex
removeHandlers []func()
}
// New creates a PluginSDK with the given dependencies.
func New(name string, sett SettingsAPI, regTool ToolRegistrar, regStage StageRegistrar, regAPI APIRegistrar, regOutput OutputChannelRegistrar) *PluginSDK {
return &PluginSDK{
name: name,
sett: sett,
regTool: regTool,
regStage: regStage,
regAPI: regAPI,
regOutput: regOutput,
autoRestart: true,
}
}
// PluginName returns the name of the plugin.
func (s *PluginSDK) PluginName() string { return s.name }
// Settings returns the settings API for reading/writing plugin configuration.
// sett 在 New 时一次性写入且无 setter故不需要加锁。
func (s *PluginSDK) Settings() SettingsAPI { return s.sett }
// Memory returns the graph memory API (may be nil if not available).
func (s *PluginSDK) Memory() MemoryAPI {
s.apiMu.RLock()
defer s.apiMu.RUnlock()
return s.mem
}
// TextMemory returns the text memory API (may be nil if not available).
func (s *PluginSDK) TextMemory() TextMemoryAPI {
s.apiMu.RLock()
defer s.apiMu.RUnlock()
return s.textMem
}
// DocMemory returns the document memory API (may be nil if not available).
func (s *PluginSDK) DocMemory() DocMemoryAPI {
s.apiMu.RLock()
defer s.apiMu.RUnlock()
return s.docMem
}
// Knowledge returns the knowledge store API (may be nil if not available).
func (s *PluginSDK) Knowledge() KnowledgeAPI {
s.apiMu.RLock()
defer s.apiMu.RUnlock()
return s.know
}
// LLM returns the LLM provider API (may be nil if not available).
func (s *PluginSDK) LLM() LLMAPI {
s.apiMu.RLock()
defer s.apiMu.RUnlock()
return s.llm
}
// Social returns the social graph API (may be nil if not available).
func (s *PluginSDK) Social() SocialAPI {
s.apiMu.RLock()
defer s.apiMu.RUnlock()
return s.social
}
// Events returns the event subscriber for listening to kernel events (may be nil if not available).
func (s *PluginSDK) Events() EventSubscriber {
s.apiMu.RLock()
defer s.apiMu.RUnlock()
return s.events
}
// RegisterTool registers a tool that the LLM can call.
func (s *PluginSDK) RegisterTool(name string, def ToolDef, handler ToolHandler) error {
if def.Plugin == "" {
def.Plugin = s.name
}
if s.regTool != nil {
return s.regTool(name, def, handler)
}
return nil
}
// RegisterStage registers a handler for a pipeline stage.
//
// scope: StageScopeGlobal (default) — receives all stage events.
// StageScopeOwnTools — only before_toolcall/after_toolcall for this plugin's tools.
func (s *PluginSDK) RegisterStage(stage Stage, handler StageHandler, scope ...StageScope) {
if s.regStage == nil {
return
}
sc := StageScopeGlobal
if len(scope) > 0 {
sc = scope[0]
}
if sc == StageScopeGlobal {
s.regStage(stage, handler)
return
}
// OwnTools scope — only for before_toolcall / after_toolcall
if stage != StageBeforeToolcall && stage != StageAfterToolcall {
s.regStage(stage, handler)
return
}
s.regStage(stage, func(ctx *StageContext) error {
ctx.RLock()
match := false
switch stage {
case StageBeforeToolcall:
match = len(ctx.ToolCalls) > 0 && ctx.ToolCalls[0].Plugin == s.name
case StageAfterToolcall:
match = len(ctx.ToolResults) > 0 && ctx.ToolResults[0].Plugin == s.name
}
ctx.RUnlock()
if !match {
return nil
}
return handler(ctx)
})
}
// RegisterPluginAPI registers this plugin's API for access by other plugins.
func (s *PluginSDK) RegisterPluginAPI(name string) error {
if s.regAPI != nil {
return s.regAPI(name)
}
return nil
}
// RegisterOutputChannel registers an output channel that the output_send tool can route to.
// name: channel name (e.g. "qq", "webui")
// caps: bitmask of supported output capabilities (CapText, CapFile, etc.)
// desc: description of the channel, expected meta format, and type enum
// def: 通道在记忆计算层的行为NoMemory/Cleaner
// handler: receives args map with keys: payload (string), type (string), meta (string|optional)
func (s *PluginSDK) RegisterOutputChannel(name string, caps int, desc string, def ChannelDef, handler ToolHandler) error {
s.apiMu.RLock()
reg := s.regOutput
s.apiMu.RUnlock()
if reg != nil {
return reg(name, caps, desc, def, handler)
}
return nil
}
// RegisterInputChannel registers an input channel with its memory behavior.
// def.NoMemory: 此通道输入不参与记忆计算
// def.Cleaner: 计算层对输入文本清洗后(不改原文)再向量化/提关键词
func (s *PluginSDK) RegisterInputChannel(name string, def ChannelDef) error {
s.apiMu.RLock()
reg := s.regInput
s.apiMu.RUnlock()
if reg != nil {
return reg(name, def)
}
return nil
}
// 以下 setter 由内核在启动/重载时调用,与插件后台 goroutine 的读并发,故加锁。
// SetOutputChannelRegistrar sets the output channel registrar (called by the core at startup).
func (s *PluginSDK) SetOutputChannelRegistrar(r OutputChannelRegistrar) {
s.apiMu.Lock()
s.regOutput = r
s.apiMu.Unlock()
}
// SetInputChannelRegistrar sets the input channel registrar (called by the core at startup).
func (s *PluginSDK) SetInputChannelRegistrar(r InputChannelRegistrar) {
s.apiMu.Lock()
s.regInput = r
s.apiMu.Unlock()
}
// SetIOInjector sets the IO injector (called by the core at startup).
func (s *PluginSDK) SetIOInjector(io IOInjector) {
s.apiMu.Lock()
s.io = io
s.apiMu.Unlock()
}
// SetMemoryAPI sets the memory API (called by the core at startup).
func (s *PluginSDK) SetMemoryAPI(mem MemoryAPI) {
s.apiMu.Lock()
s.mem = mem
s.apiMu.Unlock()
}
func (s *PluginSDK) SetTextMemoryAPI(tm TextMemoryAPI) {
s.apiMu.Lock()
s.textMem = tm
s.apiMu.Unlock()
}
func (s *PluginSDK) SetDocMemoryAPI(dm DocMemoryAPI) {
s.apiMu.Lock()
s.docMem = dm
s.apiMu.Unlock()
}
func (s *PluginSDK) SetKnowledgeAPI(kn KnowledgeAPI) {
s.apiMu.Lock()
s.know = kn
s.apiMu.Unlock()
}
func (s *PluginSDK) SetLLMAPI(llm LLMAPI) {
s.apiMu.Lock()
s.llm = llm
s.apiMu.Unlock()
}
func (s *PluginSDK) SetSocialAPI(social SocialAPI) {
s.apiMu.Lock()
s.social = social
s.apiMu.Unlock()
}
func (s *PluginSDK) SetEventSubscriber(es EventSubscriber) {
s.apiMu.Lock()
s.events = es
s.apiMu.Unlock()
}
// SetPluginMgrAPI sets the plugin manager API (called by the bridge at startup).
func (s *PluginSDK) SetPluginMgrAPI(pm PluginMgrAPI) {
s.apiMu.Lock()
s.plgMgr = pm
s.apiMu.Unlock()
}
// PluginMgr returns the plugin manager API (ReloadOne / ReloadPlugins / list).
// May be nil if the host did not wire it.
func (s *PluginSDK) PluginMgr() PluginMgrAPI {
s.apiMu.RLock()
defer s.apiMu.RUnlock()
return s.plgMgr
}
// ---- IO Convenience Methods ----
// injector 取当前 injector 的快照。
//
// 取完即释放锁再调用InjectInputSync 会阻塞到 agent 回复(可达数分钟),
// 若持锁调用,插件重载时的 SetIOInjector 会一起卡住。
func (s *PluginSDK) injector() IOInjector {
s.apiMu.RLock()
defer s.apiMu.RUnlock()
return s.io
}
// InjectInterruptText injects a text interrupt that can preempt current LLM processing.
// 等价于 InjectInterruptTextOpts(..., InjectOptions{}):记入记忆、不裁剪。
func (s *PluginSDK) InjectInterruptText(source, channel, text string) {
s.InjectInterruptTextOpts(source, channel, text, InjectOptions{})
}
// InjectText injects a text message into the agent pipeline.
// 等价于 InjectTextOpts(..., InjectOptions{}):记入记忆、不裁剪。
func (s *PluginSDK) InjectText(source, channel, text string) {
s.InjectTextOpts(source, channel, text, InjectOptions{})
}
// InjectTextNoMemory injects a text message without generating memory.
// 等价于 InjectTextOpts(..., InjectOptions{NoMemory: true})。
func (s *PluginSDK) InjectTextNoMemory(source, channel, text string) {
s.InjectTextOpts(source, channel, text, InjectOptions{NoMemory: true})
}
// InjectInputSync injects a text message and synchronously waits for the agent reply,
// returning the reply text (empty string if none). Replies must be dispatched back
// to the source channel by the caller.
func (s *PluginSDK) InjectInputSync(source, channel, text string) string {
return s.InjectInputSyncOpts(source, channel, text, InjectOptions{})
}
// InjectInputMedia 注入带媒体内容块image_url/audio_url的输入。
// blocks 会落进媒体存储被记忆引用捕获,同时作为当前轮 content 数组
// 发给 LLM让模型在「本轮」就看到图/听到音频——区别于 SetToolBlocks
// 的「下一轮 tool message」语义。
// 等价于 InjectInputMediaOpts(..., InjectOptions{})。
func (s *PluginSDK) InjectInputMedia(source, channel, text string, blocks []ContentBlock) {
s.InjectInputMediaOpts(source, channel, text, blocks, InjectOptions{})
}
// InjectInputMediaSync 注入带媒体内容块的输入并同步等待 agent 回复。
// 等价于 InjectInputMediaSyncOpts(..., InjectOptions{})。
func (s *PluginSDK) InjectInputMediaSync(source, channel, text string, blocks []ContentBlock) string {
return s.InjectInputMediaSyncOpts(source, channel, text, blocks, InjectOptions{})
}
// ---- 带 InjectOptions 的注入(声明记忆/裁剪行为)----
// InjectTextOpts 注入文本到 agent并在这一次注入上声明记忆与裁剪行为。
func (s *PluginSDK) InjectTextOpts(source, channel, text string, opts InjectOptions) {
if io := s.injector(); io != nil {
io.InjectTextOpts(source, channel, text, opts)
}
}
// InjectInterruptTextOpts 注入可抢占当前处理的中断文本。
//
// 中断也允许声明 ContextPolicyPrune中断同样携带内容进入上下文
// 是否需要据此裁剪由调用方决定(默认不裁剪)。
func (s *PluginSDK) InjectInterruptTextOpts(source, channel, text string, opts InjectOptions) {
if io := s.injector(); io != nil {
io.InjectInterruptTextOpts(source, channel, text, opts)
}
}
// InjectInputSyncOpts 注入输入并同步等待回复,同时在这次注入上声明记忆/裁剪行为。
func (s *PluginSDK) InjectInputSyncOpts(source, channel, text string, opts InjectOptions) string {
io := s.injector()
if io == nil {
return ""
}
return io.InjectInputSyncOpts(source, channel, text, opts)
}
// InjectInputMediaOpts 注入带媒体块的输入,并声明记忆/裁剪行为。
func (s *PluginSDK) InjectInputMediaOpts(source, channel, text string, blocks []ContentBlock, opts InjectOptions) {
if io := s.injector(); io != nil {
io.InjectInputMediaOpts(source, channel, text, blocks, opts)
}
}
// InjectInputMediaSyncOpts 注入带媒体块的输入并同步等待回复,同时声明记忆/裁剪行为。
func (s *PluginSDK) InjectInputMediaSyncOpts(source, channel, text string, blocks []ContentBlock, opts InjectOptions) string {
io := s.injector()
if io == nil {
return ""
}
return io.InjectInputMediaSyncOpts(source, channel, text, blocks, opts)
}
// InjectInterruptMediaOpts 注入带媒体块的中断,并声明记忆/裁剪行为。
func (s *PluginSDK) InjectInterruptMediaOpts(source, channel, text string, blocks []ContentBlock, opts InjectOptions) {
if io := s.injector(); io != nil {
io.InjectInterruptMediaOpts(source, channel, text, blocks, opts)
}
}
// InjectInterruptMedia 注入带媒体内容块的中断,可抢占当前 LLM 处理。
// blocks 随中断消息一起发给模型。
func (s *PluginSDK) InjectInterruptMedia(source, channel, text string, blocks []ContentBlock) {
if io := s.injector(); io != nil {
io.InjectInterruptMedia(source, channel, text, blocks)
}
}
// SetToolBlocks 在工具处理函数内注入多模态内容块,内核在下一条 tool message
// 的 content 数组里带上它们。需要「本轮就让模型看到」时用 InjectInputMedia。
func (s *PluginSDK) SetToolBlocks(blocks []ContentBlock) {
if io := s.injector(); io != nil {
io.SetToolBlocks(blocks)
}
}
// SetAutoRestart 设置插件是否允许内核自动重启(崩溃后自动重载)。
// 默认 true。如果插件有无法恢复的状态如外部连接应设为 false。
func (s *PluginSDK) SetAutoRestart(enabled bool) {
s.apiMu.Lock()
s.autoRestart = enabled
s.apiMu.Unlock()
}
// AutoRestart 返回插件是否允许自动重启。
func (s *PluginSDK) AutoRestart() bool {
s.apiMu.RLock()
defer s.apiMu.RUnlock()
return s.autoRestart
}
// RegisterStopHandler 注册插件停止阶段的清理回调。
// 注册的 handler 会在插件 Stop() 之前按"后注册先执行"的顺序调用,
// 适用于释放资源、落盘状态、关闭子进程等停止时清理操作。
// 可注册多个;执行后清空(进程停止前只执行一次)。
func (s *PluginSDK) RegisterStopHandler(fn func()) {
if fn == nil {
return
}
s.stopMu.Lock()
s.stopHandlers = append(s.stopHandlers, fn)
s.stopMu.Unlock()
}
// RunStopHandlers 执行全部已注册的 stop handler后注册先执行执行后清空幂等
// 由内核(内置插件)或插件桥接层(外部插件 z_bridge 的 StopPlugin在调用插件 Stop() 前执行。
func (s *PluginSDK) RunStopHandlers() {
s.stopMu.Lock()
handlers := append([]func(){}, s.stopHandlers...)
s.stopHandlers = nil
s.stopMu.Unlock()
for i := len(handlers) - 1; i >= 0; i-- {
handlers[i]()
}
}
// RegisterOnRemoveHandler 注册插件被删除(卸载)时的清理回调。
// 注册的 handler 会在插件目录被移除前按"后注册先执行"的顺序调用,
// 适用于清理外部资源、删除配置表、下线状态等删除后处理。
// 可注册多个;执行后清空(一次删除只执行一次)。
func (s *PluginSDK) RegisterOnRemoveHandler(fn func()) {
if fn == nil {
return
}
s.removeMu.Lock()
s.removeHandlers = append(s.removeHandlers, fn)
s.removeMu.Unlock()
}
// RunOnRemoveHandlers 执行全部已注册的 onRemove handler后注册先执行执行后清空幂等
// 由内核在卸载插件registry.RemovePlugin时、插件 Stop() 之后执行。
func (s *PluginSDK) RunOnRemoveHandlers() {
s.removeMu.Lock()
handlers := append([]func(){}, s.removeHandlers...)
s.removeHandlers = nil
s.removeMu.Unlock()
for i := len(handlers) - 1; i >= 0; i-- {
handlers[i]()
}
}
// ContentBlock 是多模态内容块OpenAI 格式text/image_url/audio_url
// 插件工具返回结果时可用 PluginSDK.SetToolBlocks 注入,让下一轮 LLM
// 请求在 tool message 的 content 数组里带上图片/音频,实现"模型看图/听音频"。
type ContentBlock struct {
Type string `json:"type"`
Text string `json:"text,omitempty"`
ImageURL *ImageURL `json:"image_url,omitempty"`
AudioURL *AudioURL `json:"audio_url,omitempty"`
}
type ImageURL struct {
URL string `json:"url"`
Detail string `json:"detail,omitempty"`
}
type AudioURL struct {
URL string `json:"url"`
}