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+(旧核心会忽略这些字段,不报错但不生效)。
This commit is contained in:
JianFeeeee
2026-09-11 20:29:30 +08:00
parent b2eafdf885
commit ba49dfda44
12 changed files with 543 additions and 61 deletions

View File

@ -520,7 +520,8 @@ func (p *Plugin) checkReminders() {
p.mu.Unlock() p.mu.Unlock()
for _, msg := range injectMsgs { for _, msg := range injectMsgs {
p.sdk.InjectInterruptText("calendar", "calendar", msg) // NoMemory日程到点提醒不是记忆内容。
p.sdk.InjectInterruptTextOpts("calendar", "calendar", msg, sdk.InjectOptions{NoMemory: true})
} }
} }

View File

@ -292,8 +292,9 @@ func (p *Plugin) periodicCheck() {
continue continue
} }
if p.sdk != nil { if p.sdk != nil {
p.sdk.InjectInterruptText(p.name, p.name, // NoMemory这是定时提醒不是记忆内容。
fmt.Sprintf("注意,你还有%d条待办未完成请检查", n)) p.sdk.InjectInterruptTextOpts(p.name, p.name,
fmt.Sprintf("注意,你还有%d条待办未完成请检查", n), sdk.InjectOptions{NoMemory: true})
} }
} }
} }

View File

@ -1434,7 +1434,8 @@ func (p *Plugin) handleWebhook(w http.ResponseWriter, r *http.Request) {
} }
if p.sdk != nil { if p.sdk != nil {
p.sdk.InjectInterruptText(p.name, p.name, interrupt) // NoMemoryHTTP 侧来的中断提示,不是对话内容。
p.sdk.InjectInterruptTextOpts(p.name, p.name, interrupt, sdk.InjectOptions{NoMemory: true})
} }
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
} }
@ -2529,8 +2530,10 @@ func (p *Plugin) handleDownloadFile(args map[string]interface{}) (interface{}, e
p.updateDownloadTask(t, "done", savePath, "") p.updateDownloadTask(t, "done", savePath, "")
log.Printf("[qq] 文件下载完成: %s", savePath) log.Printf("[qq] 文件下载完成: %s", savePath)
if p.sdk != nil { if p.sdk != nil {
p.sdk.InjectInterruptText(p.name, p.name, // NoMemory下载完成的状态通知不是记忆内容。
fmt.Sprintf("文件下载完成: %s保存在 %s", filepath.Base(savePath), savePath)) p.sdk.InjectInterruptTextOpts(p.name, p.name,
fmt.Sprintf("文件下载完成: %s保存在 %s", filepath.Base(savePath), savePath),
sdk.InjectOptions{NoMemory: true})
} }
} else { } else {
errMsg = "下载失败,文件可能已过期" errMsg = "下载失败,文件可能已过期"

View File

@ -300,7 +300,10 @@ func (p *Plugin) checkFeed(sub FeedSub) {
lines = append(lines, line) lines = append(lines, line)
} }
p.sdk.InjectInterruptText("rss", "rss", strings.Join(lines, "\n")) // 中断注入是「系统通知」NoMemory 写明意图:这类提醒不参与记忆计算,
// 原文仍进上下文(模型当轮看得到)。
p.sdk.InjectInterruptTextOpts("rss", "rss", strings.Join(lines, "\n"),
sdk.InjectOptions{NoMemory: true})
p.saveData() p.saveData()
} }

View File

@ -28,6 +28,16 @@ var (
// 存量插件不需要改一行也不需要重编:新增方法由**插件调用、内核实现** // 存量插件不需要改一行也不需要重编:新增方法由**插件调用、内核实现**
// 不调就不受影响。想用新字段的插件重编即可。 // 不调就不受影响。想用新字段的插件重编即可。
// //
// 1.2.0:注入行为的记忆/裁剪标志位。**全部是新增,无签名变更**
// - InjectOptions{NoMemory, ContextPolicy}
// - IOInjector 的六个 *Opts 变体(排队/中断/同步/带媒体各一对)
// - ChannelDef.ContextPolicy顺带给 ChannelDef 补上 JSON tag
// 它要跨进程传给内核,而 Cleaner 是函数必须忽略;无 tag 时只能
// 手写字段白名单,新增字段会被静默丢掉)
// 语义:零值 InjectOptions 与旧的三参数方法完全等价(记入记忆 +
// 不裁剪),因此存量插件不需要改一行也不需要重编。
// 裁剪ContextPolicy=prune必须显式声明——它会归档丢弃低相关事件。
//
// ❗main 分支上此值是**下一个未发布中版本**;已发布的值看对应的 // ❗main 分支上此值是**下一个未发布中版本**;已发布的值看对应的
// release/vX.Y.x 分支与 tag见 核心仓 docs/git-branching.md §2.1 与 §七.1)。 // release/vX.Y.x 分支与 tag见 核心仓 docs/git-branching.md §2.1 与 §七.1)。
// //
@ -56,6 +66,10 @@ var (
// doc.insertWithMedia / io.injectMedia* 这些 RPC调用会返回 unknown method // doc.insertWithMedia / io.injectMedia* 这些 RPC调用会返回 unknown method
// 这里仍写 1.0.0因为它是「SDK 能在其上运行」的下限; // 这里仍写 1.0.0因为它是「SDK 能在其上运行」的下限;
// 媒体接口是可选能力,不用就不受影响。 // 媒体接口是可选能力,不用就不受影响。
//
// ⚠️ 1.2.0 新增的注入标志位同理需要核心 **1.2.0+**:内核在 1.2.0 之前会
// 忽略注入参数里的 no_memory/context_policy 字段(不会报错,但不生效)。
// 想用这些标志位的插件应当要求核心 1.2.0+;不用就不受影响。
CoreVersion = "1.0.0" CoreVersion = "1.0.0"
) )

149
package/build-examples.sh Normal file
View File

@ -0,0 +1,149 @@
#!/usr/bin/env bash
# 给 SDK 发版打包**示例插件**的 .hmap 产物。
#
# 为什么要在 SDK 仓库里发示例插件的 hmap
# 插件二进制与内核是**协议绑定**的internal/plugin/proc/protocol.go 的
# ProtocolVersion + 统一共享内存区魔数。SDK 升版往往同时意味着协议变化,
# 而示例插件qq/memo/browser/…)是使用者最常直接安装的东西。
# 如果 SDK 只发工具链不发示例产物,使用者要么自己重编、要么用到与本版 SDK
# 不匹配的旧产物——后者的表现是握手失败(协议/魔数不匹配),而且看起来像
# 「插件坏了」而不是「版本不配套」。
#
# 用法:
# package/build-examples.sh [TARGET] [OUT_DIR]
# TARGET native(默认) | linux/amd64 | linux/arm64 | darwin/amd64 | darwin/arm64 | windows/amd64 | all
# OUT_DIR 产物目录(默认 build/examples
#
# 产物:
# <OUT_DIR>/<name>_<goos>_<goarch>.hmap 每个示例插件一份
# <OUT_DIR>/SHA256SUMS 全部产物齐全**之后**才计算
# <OUT_DIR>/MANIFEST.txt 版本、协议版本、产自哪个 commit
#
# 纪律(与本项目其它构建脚本一致):
# 1. 判成功看**产物是否存在**不看退出码——plugindev 对部分错误只打印不退出。
# 2. SHA256SUMS 必须在全部产物生成完毕后一次算完,边打边算会漏掉后生成的包。
set -uo pipefail
SDK_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
TARGET="${1:-native}"
OUT_DIR="${2:-$SDK_ROOT/build/examples}"
GO="${GO:-$(command -v go 2>/dev/null || echo go)}"
case "$TARGET" in
native) GOOS=""; GOARCH="" ;;
linux/amd64) GOOS=linux; GOARCH=amd64 ;;
linux/arm64) GOOS=linux; GOARCH=arm64 ;;
darwin/amd64) GOOS=darwin; GOARCH=amd64 ;;
darwin/arm64) GOOS=darwin; GOARCH=arm64 ;;
windows/amd64)
# 明确拒绝,而不是让调用方拿到一句深层 Go 编译错误。
# 协议 2 的统一共享内存区只移植到了 Unix内核 internal/plugin/proc/
# shmpass_windows.go 仍是旧的 SHM_STAGE/SHM_EVTRING 两段布局,
# 插件模板 proc_shm_windows.go 也缺 attachUnifiedShm。
echo "windows 目标暂不支持:协议 2 的统一共享内存区未移植到 Windows内核与插件模板均缺实现。" >&2
exit 1
;;
all)
echo "本脚本一次只构建一个平台;请由 package/build.sh 传入具体目标。" >&2
exit 1
;;
*)
echo "Unknown target: $TARGET" >&2
echo "Usage: $0 [native|linux/amd64|linux/arm64|darwin/amd64|darwin/arm64|windows/amd64|all] [OUT_DIR]" >&2
exit 1
;;
esac
export CGO_ENABLED=0
# 按平台逐个构建,**不用** bundle 模式:
# - bundle 会连 windows 一起编,而协议 2 的统一共享区尚未移植到 Windows
# (内核 shmpass_windows.go 仍是旧的两段布局),必然失败;
# - 逐平台构建每个目标都产出一份 .hmap正是发版要附的产物。
# 平台名解析成本脚本后面用(校验和与 MANIFEST 都要写清楚是哪个平台)。
if [ -z "${GOOS:-}" ]; then
GOOS="$(go env GOOS)"; GOARCH="$(go env GOARCH)"
fi
# 1) 先保证工具链可用:示例必须用**本仓当前源码**构建,否则产物协议与这一版 SDK 不符。
# 允许外部指定(发版脚本会在跨平台构建后把刚产出的工具链路径传进来)。
PLUGINDEV="${PLUGINDEV:-$SDK_ROOT/build/plugindev}"
if [ ! -x "$PLUGINDEV" ]; then
echo "[examples] 先构建 plugindev ..."
( cd "$SDK_ROOT/tools/plugindev" && "$GO" build -o "$PLUGINDEV" . ) || {
echo "[examples] plugindev 构建失败,无法继续" >&2; exit 1; }
fi
if [ ! -x "$PLUGINDEV" ]; then
echo "[examples] plugindev 不存在或不可执行:$PLUGINDEV" >&2
exit 1
fi
echo "=== 协议 ==="
echo " ProtocolVersion = $(grep -m1 '^const ProtocolVersion' "$SDK_ROOT/../internal/plugin/proc/protocol.go" 2>/dev/null | grep -oE '[0-9]+' || echo '?(本仓非内核仓,跳过)')"
mkdir -p "$OUT_DIR"
# 清掉上一次的校验和:残留的 SHA256SUMS 会掩盖本次缺产物。
rm -f "$OUT_DIR"/SHA256SUMS "$OUT_DIR"/MANIFEST.txt
ok=0
fail=0
failed_names=""
for dir in "$SDK_ROOT"/example/*/; do
[ -f "$dir/plugin.go" ] || continue
name="$(basename "$dir")"
# 清掉旧产物:残留会让人(和本脚本)误判成功。
rm -rf "$dir/build" "$dir/dist"
out=$( cd "$dir" && "$PLUGINDEV" build --no-bundle --target "$GOOS/$GOARCH" 2>&1 )
rc=$?
# 判据是**退出码 + 产物存在**,两者都要。
# 只看退出码plugindev 曾经出错也退 0已修但脚本不该依赖它「现在」是对的
# 只看产物:部分平台失败时会留下上一次的产物,看起来像成功。
hmap="$(ls "$dir"/dist/*.hmap 2>/dev/null | head -1)"
if [ $rc -eq 0 ] && [ -n "$hmap" ]; then
# 保留插件自己声明的产物名(它用的是 plg.json 的 name_en是插件的身份
# 只在前面加平台前缀避免多平台互相覆盖。
dest="$OUT_DIR/${GOOS}_${GOARCH}_$(basename "$hmap")"
cp "$hmap" "$dest"
printf "✓ %-14s → %s (%s)\n" "$name" "$(basename "$dest")" "$(du -h "$dest" | cut -f1)"
ok=$((ok + 1))
else
printf "✗ %-14s 构建失败 (rc=%d)\n" "$name" "$rc"
echo "$out" | tail -6 | sed 's/^/ /'
fail=$((fail + 1))
failed_names="$failed_names $name"
fi
done
echo
echo "示例产物: 成功 $ok / 失败 $fail"
[ -n "$failed_names" ] && echo "失败:$failed_names"
# 有失败就不算发版闭环:宁可整个中断,也不要发出「少几个插件」的包。
if [ $fail -ne 0 ]; then
echo "[examples] 有示例构建失败,不生成 SHA256SUMS" >&2
exit 1
fi
# 2) 全部产物齐了才算校验和。
( cd "$OUT_DIR" && sha256sum ./*.hmap > SHA256SUMS )
VERSION="${VERSION:-$(git -C "$SDK_ROOT" describe --tags --dirty 2>/dev/null || echo unknown)}"
COMMIT="${COMMIT:-$(git -C "$SDK_ROOT" rev-parse --short HEAD 2>/dev/null || echo unknown)}"
{
echo "sdk_version: $VERSION"
echo "sdk_commit: $COMMIT"
echo "target: $GOOS/$GOARCH"
echo "plugins: $ok"
echo "built_at: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo
echo "这些 .hmap 与本版 SDK 的插件协议绑定,必须与同版本内核配套安装。"
echo "校验sha256sum -c SHA256SUMS"
} > "$OUT_DIR/MANIFEST.txt"
echo "[examples] 产物: $OUT_DIR"
echo "[examples] 清单: $OUT_DIR/MANIFEST.txt"
echo "[examples] 校验: $OUT_DIR/SHA256SUMS"

View File

@ -28,7 +28,7 @@ case "$TARGET" in
;; ;;
*) *)
echo "Unknown target: $TARGET" echo "Unknown target: $TARGET"
echo "Usage: $0 [native|linux/amd64|linux/arm64|darwin/amd64|darwin/arm64|windows/amd64|all] [all|plugindev]" echo "Usage: $0 [native|linux/amd64|linux/arm64|darwin/amd64|darwin/arm64|windows/amd64|all] [all|plugindev|examples]"
exit 1 exit 1
esac esac
@ -55,9 +55,32 @@ build_plugindev() {
cd "$PROJECT_ROOT" cd "$PROJECT_ROOT"
} }
# 示例插件产物随 SDK 一起发。
#
# 为什么必须发:插件二进制与内核是**协议绑定**的ProtocolVersion + 统一共享
# 内存区魔数。SDK 升版常伴随协议变化,只发工具链不发示例产物,使用者很可能
# 拿旧产物去装,表现是握手失败(魔数不匹配)——看起来像「插件坏了」而不是
# 「版本不配套」。
#
# 用刚构建出来的那把工具链(而非 PATH 里的),保证产物与本次发版同源。
build_examples() {
local dev="$BUILD_DIR/plugindev${SUFFIX:+_$SUFFIX}"
[ "$GOOS" = "windows" ] && dev="${dev}.exe"
echo "[BUILD] example plugins ${GOOS:-linux}/${GOARCH:-amd64}$BUILD_DIR/examples"
PLUGINDEV="$dev" VERSION="$VERSION" bash "$PROJECT_ROOT/package/build-examples.sh" "$TARGET" "$BUILD_DIR/examples"
echo " OK"
}
case "$COMPONENT" in case "$COMPONENT" in
all|plugindev) build_plugindev ;; all)
# 工具链必须先建完:示例用它来构建(同源保证协议一致)。
build_plugindev
build_examples
;;
plugindev) build_plugindev ;;
examples) build_examples ;;
*) *)
echo "Unknown component: $COMPONENT" echo "Unknown component: $COMPONENT"
exit 1 exit 1
;;
esac esac

View File

@ -35,12 +35,63 @@ const (
StageAfterOutput Stage = "after_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 语义一致。 // ChannelDef 描述通道在记忆计算层的行为,与 ToolDef.NoMemory/Cleaner 语义一致。
// NoMemory: 此通道输入/输出不参与记忆计算(向量化/关键词提取/蒸馏),但原文保留在上下文中 // NoMemory: 此通道输入/输出不参与记忆计算(向量化/关键词提取/蒸馏),但原文保留在上下文中
// Cleaner: 计算层过滤函数,不改原文;仅在向量化/jieba/蒸馏/存档提取关键词时调用 // Cleaner: 计算层过滤函数,不改原文;仅在向量化/jieba/蒸馏/存档提取关键词时调用
// ContextPolicy: 此通道的输入到达后是否据此裁剪上下文,默认 none不裁剪
//
// JSON tag 是必需的:通道定义要跨进程传给内核,而 Cleaner 是函数(必须忽略)。
// 没有 tag 时既无法整体 marshalfunc 不支持),又会诱使调用方手写字段白名单——
// 那样新增字段会被静默丢掉。
type ChannelDef struct { type ChannelDef struct {
NoMemory bool NoMemory bool `json:"no_memory,omitempty"`
Cleaner func(string) string Cleaner func(string) string `json:"-"`
ContextPolicy string `json:"context_policy,omitempty"`
} }
// StageContext provides context for stage handlers. // StageContext provides context for stage handlers.
@ -106,7 +157,7 @@ type ToolDef struct {
Parameters map[string]interface{} `json:"parameters"` Parameters map[string]interface{} `json:"parameters"`
NoMemory bool `json:"no_memory,omitempty"` // 此工具输出不参与记忆计算,但原文保留 NoMemory bool `json:"no_memory,omitempty"` // 此工具输出不参与记忆计算,但原文保留
Cleaner func(string) string `json:"-"` // 计算层过滤函数,不改原文;仅在向量化/jieba/蒸馏时调用 Cleaner func(string) string `json:"-"` // 计算层过滤函数,不改原文;仅在向量化/jieba/蒸馏时调用
ContextPolicy string `json:"context_policy,omitempty"` // 工具上下文策略:"none"(默认) / "prune" ContextPolicy string `json:"context_policy,omitempty"` // 上下文策略:""(默认,不裁剪) / ContextPolicyNone / ContextPolicyPrune
} }
// IOInjector provides methods for injecting input and interrupts into the agent pipeline. // IOInjector provides methods for injecting input and interrupts into the agent pipeline.
@ -125,6 +176,17 @@ type IOInjector interface {
InjectInputMedia(source, channel, text string, blocks []ContentBlock) InjectInputMedia(source, channel, text string, blocks []ContentBlock)
InjectInputMediaSync(source, channel, text string, blocks []ContentBlock) string InjectInputMediaSync(source, channel, text string, blocks []ContentBlock) string
InjectInterruptMedia(source, channel, text string, blocks []ContentBlock) 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. // EventType identifies the kind of system event.
@ -505,54 +567,94 @@ func (s *PluginSDK) injector() IOInjector {
} }
// InjectInterruptText injects a text interrupt that can preempt current LLM processing. // InjectInterruptText injects a text interrupt that can preempt current LLM processing.
// 等价于 InjectInterruptTextOpts(..., InjectOptions{}):记入记忆、不裁剪。
func (s *PluginSDK) InjectInterruptText(source, channel, text string) { func (s *PluginSDK) InjectInterruptText(source, channel, text string) {
if io := s.injector(); io != nil { s.InjectInterruptTextOpts(source, channel, text, InjectOptions{})
io.InjectInterruptText(source, channel, text)
}
} }
// InjectText injects a text message into the agent pipeline. // InjectText injects a text message into the agent pipeline.
// 等价于 InjectTextOpts(..., InjectOptions{}):记入记忆、不裁剪。
func (s *PluginSDK) InjectText(source, channel, text string) { func (s *PluginSDK) InjectText(source, channel, text string) {
if io := s.injector(); io != nil { s.InjectTextOpts(source, channel, text, InjectOptions{})
io.InjectText(source, channel, text)
}
} }
// InjectTextNoMemory injects a text message without generating memory. // InjectTextNoMemory injects a text message without generating memory.
// 等价于 InjectTextOpts(..., InjectOptions{NoMemory: true})。
func (s *PluginSDK) InjectTextNoMemory(source, channel, text string) { func (s *PluginSDK) InjectTextNoMemory(source, channel, text string) {
if io := s.injector(); io != nil { s.InjectTextOpts(source, channel, text, InjectOptions{NoMemory: true})
io.InjectTextNoMemory(source, channel, text)
}
} }
// InjectInputSync injects a text message and synchronously waits for the agent reply, // 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 // returning the reply text (empty string if none). Replies must be dispatched back
// to the source channel by the caller. // to the source channel by the caller.
func (s *PluginSDK) InjectInputSync(source, channel, text string) string { func (s *PluginSDK) InjectInputSync(source, channel, text string) string {
io := s.injector() return s.InjectInputSyncOpts(source, channel, text, InjectOptions{})
if io == nil {
return ""
}
return io.InjectInputSync(source, channel, text)
} }
// InjectInputMedia 注入带媒体内容块image_url/audio_url的输入。 // InjectInputMedia 注入带媒体内容块image_url/audio_url的输入。
// blocks 会落进媒体存储被记忆引用捕获,同时作为当前轮 content 数组 // blocks 会落进媒体存储被记忆引用捕获,同时作为当前轮 content 数组
// 发给 LLM让模型在「本轮」就看到图/听到音频——区别于 SetToolBlocks // 发给 LLM让模型在「本轮」就看到图/听到音频——区别于 SetToolBlocks
// 的「下一轮 tool message」语义。 // 的「下一轮 tool message」语义。
// 等价于 InjectInputMediaOpts(..., InjectOptions{})。
func (s *PluginSDK) InjectInputMedia(source, channel, text string, blocks []ContentBlock) { func (s *PluginSDK) InjectInputMedia(source, channel, text string, blocks []ContentBlock) {
if io := s.injector(); io != nil { s.InjectInputMediaOpts(source, channel, text, blocks, InjectOptions{})
io.InjectInputMedia(source, channel, text, blocks)
}
} }
// InjectInputMediaSync 注入带媒体内容块的输入并同步等待 agent 回复。 // InjectInputMediaSync 注入带媒体内容块的输入并同步等待 agent 回复。
// 等价于 InjectInputMediaSyncOpts(..., InjectOptions{})。
func (s *PluginSDK) InjectInputMediaSync(source, channel, text string, blocks []ContentBlock) string { 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() io := s.injector()
if io == nil { if io == nil {
return "" return ""
} }
return io.InjectInputMediaSync(source, channel, text, blocks) 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 处理。 // InjectInterruptMedia 注入带媒体内容块的中断,可抢占当前 LLM 处理。

View File

@ -43,6 +43,7 @@ type injectCall struct {
channel string channel string
text string text string
blocks []ContentBlock blocks []ContentBlock
opts InjectOptions // 调用点声明的记忆/裁剪行为
} }
func (r *recordingInjector) record(c injectCall) { func (r *recordingInjector) record(c injectCall) {
@ -93,6 +94,41 @@ func (r *recordingInjector) InjectInterruptMedia(s, c, t string, b []ContentBloc
r.record(injectCall{kind: "interruptMedia", source: s, channel: c, text: t, blocks: b}) r.record(injectCall{kind: "interruptMedia", source: s, channel: c, text: t, blocks: b})
} }
// ---- 带 InjectOptions 的注入:记录 opts 以便测试断言标志位确实传到了内核 ----
func (r *recordingInjector) InjectTextOpts(s, c, t string, o InjectOptions) {
r.nText.Add(1)
r.record(injectCall{kind: "textOpts", source: s, channel: c, text: t, opts: o})
}
func (r *recordingInjector) InjectInterruptTextOpts(s, c, t string, o InjectOptions) {
r.nInterrupt.Add(1)
r.record(injectCall{kind: "interruptTextOpts", source: s, channel: c, text: t, opts: o})
}
func (r *recordingInjector) InjectInputSyncOpts(s, c, t string, o InjectOptions) string {
r.nSync.Add(1)
r.record(injectCall{kind: "syncOpts", source: s, channel: c, text: t, opts: o})
return "reply:" + t
}
func (r *recordingInjector) InjectInputMediaOpts(s, c, t string, b []ContentBlock, o InjectOptions) {
r.nMedia.Add(1)
r.record(injectCall{kind: "mediaOpts", source: s, channel: c, text: t, blocks: b, opts: o})
}
func (r *recordingInjector) InjectInputMediaSyncOpts(s, c, t string, b []ContentBlock, o InjectOptions) string {
r.nMedia.Add(1)
r.nSync.Add(1)
r.record(injectCall{kind: "mediaSyncOpts", source: s, channel: c, text: t, blocks: b, opts: o})
return "reply:" + t
}
func (r *recordingInjector) InjectInterruptMediaOpts(s, c, t string, b []ContentBlock, o InjectOptions) {
r.nMedia.Add(1)
r.record(injectCall{kind: "interruptMediaOpts", source: s, channel: c, text: t, blocks: b, opts: o})
}
func (r *recordingInjector) snapshot() []injectCall { func (r *recordingInjector) snapshot() []injectCall {
r.mu.Lock() r.mu.Lock()
defer r.mu.Unlock() defer r.mu.Unlock()

View File

@ -20,6 +20,15 @@ type BuildConfig struct {
Replaces []string Replaces []string
} }
// buildFailed 记录本次构建是否有平台失败。
//
// 为什么要它:这两个构建函数遇到错误只是 Printf 后 return而 cmdBuild 返回
// void于是**构建失败却以 0 退出**。调用方批量重编脚本、CI、发版脚本
// 只能靠翻日志发现失败——实测中一个示例的 windows 目标编译失败,脚本却报
// 「17/17 全绿」,并因此少产出 16 个 .hmap。
// 判成功要看退出码,不能靠人读日志。
var buildFailed bool
func cmdBuild(args []string) { func cmdBuild(args []string) {
// Read all config from plg.json first // Read all config from plg.json first
plg, err := readPlgJSON("plg.json") plg, err := readPlgJSON("plg.json")
@ -92,11 +101,18 @@ func cmdBuild(args []string) {
if bundle || len(targets) == 0 { if bundle || len(targets) == 0 {
buildBundle(plg, outDir, sdkPath) buildBundle(plg, outDir, sdkPath)
return } else {
for _, t := range targets {
buildTarget(plg, t, outDir, sdkPath)
}
} }
for _, t := range targets { // 以非零码退出调用方批量重编、CI、发版脚本靠退出码判成败。
buildTarget(plg, t, outDir, sdkPath) // 以前这里直接 return失败也退 0于是「构建失败」只能靠人翻日志发现——
// 实测中就因此把一次部分失败当成了全绿。
if buildFailed {
fmt.Println("error: 至少一个目标构建失败(详见上面日志)")
os.Exit(1)
} }
} }
@ -105,13 +121,33 @@ func cmdBuild(args []string) {
// 子进程模式下各平台产物同名plugin.bin——进程边界即 ABI 边界, // 子进程模式下各平台产物同名plugin.bin——进程边界即 ABI 边界,
// 不存在平台特有扩展名,故 zip 内按平台加后缀区分; // 不存在平台特有扩展名,故 zip 内按平台加后缀区分;
// 内核安装时按当前平台挑对应条目重命名为 plugin.bin。 // 内核安装时按当前平台挑对应条目重命名为 plugin.bin。
//
// **不含 windows**:插件只能运行在 homed 能跑的平台上,而 homed 已明确放弃
// Windows 原生支持(插件体系依赖 fd 继承 + 统一共享内存区的段内偏移,
// Windows 句柄模型无法表达。Windows 用户走 WSL2而 WSL2 就是 linux/amd64。
var allBundleTargets = []struct { var allBundleTargets = []struct {
target string target string
entry string // 二进制在 zip 中的文件名 entry string // 二进制在 zip 中的文件名
}{ }{
{"linux/amd64", "plugin.bin.linux.amd64"}, {"linux/amd64", "plugin.bin.linux.amd64"},
{"darwin/amd64", "plugin.bin.darwin.amd64"}, {"darwin/amd64", "plugin.bin.darwin.amd64"},
{"windows/amd64", "plugin.bin.windows.amd64"}, }
// checkTargetSupported 在构建前拦下**已知不支持**的目标,给出可执行的报错。
//
// 为什么要有它:插件运行在 homed 的进程里,所以目标平台必须是 homed 能跑的。
// homed 已放弃 Windows 原生(原因:插件依赖 fd 继承与统一共享内存区段内偏移,
// Windows 句柄模型无法表达),却还去构建 windows 插件,结果是死在一句
// 「undefined: attachUnifiedShm」——看起来像代码 bug实际是平台策略。
// 这里换成明确的结论,并且**不静默跳过**:静默跳过会让人以为产出的包里包含 windows。
func checkTargetSupported(target string) error {
if strings.HasPrefix(target, "windows/") {
return fmt.Errorf("不支持 windows 插件目标:插件运行在 homed 内," +
"而 homed 已放弃 Windows 原生支持(插件体系依赖 fd 继承与统一共享内存区" +
"段内偏移解引用Windows 句柄模型无法表达。Windows 请用 WSL2——" +
"它就是 linux/amd64用 --target linux/amd64 即可")
}
return nil
} }
func buildBundle(plg *PlgConfig, outDir string, sdkPath string) { func buildBundle(plg *PlgConfig, outDir string, sdkPath string) {
@ -122,6 +158,7 @@ func buildBundle(plg *PlgConfig, outDir string, sdkPath string) {
runtimeCleanup, err := generateProcRuntime() runtimeCleanup, err := generateProcRuntime()
if err != nil { if err != nil {
fmt.Printf(" error: %v\n", err) fmt.Printf(" error: %v\n", err)
buildFailed = true
return return
} }
defer runtimeCleanup() defer runtimeCleanup()
@ -132,9 +169,15 @@ func buildBundle(plg *PlgConfig, outDir string, sdkPath string) {
var binaries []binEntry var binaries []binEntry
for _, bt := range allBundleTargets { for _, bt := range allBundleTargets {
if err := checkTargetSupported(bt.target); err != nil {
fmt.Printf(" error: %v\n", err)
buildFailed = true
return
}
cfg, errMsg := resolveBuild(bt.target) cfg, errMsg := resolveBuild(bt.target)
if cfg == nil { if cfg == nil {
fmt.Printf(" error: %s\n", errMsg) fmt.Printf(" error: %s\n", errMsg)
buildFailed = true
return return
} }
@ -151,7 +194,10 @@ func buildBundle(plg *PlgConfig, outDir string, sdkPath string) {
fmt.Printf(" compiling %s/%s (子进程模式CGO_ENABLED=0)...\n", cfg.goos, cfg.goarch) fmt.Printf(" compiling %s/%s (子进程模式CGO_ENABLED=0)...\n", cfg.goos, cfg.goarch)
if err := cmd.Run(); err != nil { if err := cmd.Run(); err != nil {
// 单平台失败即整包失败bundle 少一个平台就是个坏包,
// 却仍会生成 .hmap 让人以为打包成功。
fmt.Printf(" error: build %s/%s: %v\n", cfg.goos, cfg.goarch, err) fmt.Printf(" error: build %s/%s: %v\n", cfg.goos, cfg.goarch, err)
buildFailed = true
return return
} }
binaries = append(binaries, binEntry{src: outPath, zip: bt.entry}) binaries = append(binaries, binEntry{src: outPath, zip: bt.entry})
@ -544,6 +590,13 @@ func buildTarget(plg *PlgConfig, target, outDir, sdkPath string) {
} }
defer runtimeCleanup() defer runtimeCleanup()
// 已知未实现的目标在编译前拦下,给可执行的报错(见 checkTargetSupported
if err := checkTargetSupported(target); err != nil {
fmt.Printf(" error: %v\n", err)
buildFailed = true
return
}
// Auto-link thirdpart/ contents + source_dirs + replace targets // Auto-link thirdpart/ contents + source_dirs + replace targets
thirdpartCleanup := linkThirdpart(plg, target) thirdpartCleanup := linkThirdpart(plg, target)
defer thirdpartCleanup() defer thirdpartCleanup()
@ -562,6 +615,7 @@ func buildTarget(plg *PlgConfig, target, outDir, sdkPath string) {
fmt.Printf(" compiling %s/%s (子进程模式CGO_ENABLED=0)...\n", cfg.goos, cfg.goarch) fmt.Printf(" compiling %s/%s (子进程模式CGO_ENABLED=0)...\n", cfg.goos, cfg.goarch)
if err := cmd.Run(); err != nil { if err := cmd.Run(); err != nil {
fmt.Printf(" error: build %s/%s: %v\n", cfg.goos, cfg.goarch, err) fmt.Printf(" error: build %s/%s: %v\n", cfg.goos, cfg.goarch, err)
buildFailed = true
return return
} }

View File

@ -253,15 +253,40 @@ func putValueInArena(v interface{}) (SharedRef, bool) {
// //
// 共享内存对插件开发者完全透明SDK 层只看得到 string。 // 共享内存对插件开发者完全透明SDK 层只看得到 string。
func callWithText(method, source, channel, text string) (json.RawMessage, error) { func callWithText(method, source, channel, text string) (json.RawMessage, error) {
return callWithTextOpts(method, source, channel, text, sdk.InjectOptions{})
}
// callWithTextOpts 是 callWithText 的带标志位版本。
//
// 只在标志位非零时才写入参数:零值(记入记忆 + 不裁剪)与旧参数形态完全一致,
// 便于内核侧做兼容与灰度。
func callWithTextOpts(method, source, channel, text string, opts sdk.InjectOptions) (json.RawMessage, error) {
if ref, ok := putInArena(text); ok { if ref, ok := putInArena(text); ok {
defer arenaFree(ref) defer arenaFree(ref)
return callCore(method, map[string]interface{}{ args := map[string]interface{}{
"source": source, "channel": channel, "text_ref": ref, "source": source, "channel": channel, "text_ref": ref,
}) }
applyInjectOpts(args, opts)
return callCore(method, args)
} }
return callCore(method, map[string]string{ args := map[string]interface{}{
"source": source, "channel": channel, "text": text, "source": source, "channel": channel, "text": text,
}) }
applyInjectOpts(args, opts)
return callCore(method, args)
}
// applyInjectOpts 把 InjectOptions 摊进注入参数字典(仅非零值)。
func applyInjectOpts(args map[string]interface{}, opts sdk.InjectOptions) {
if opts.NoMemory {
args["no_memory"] = true
}
if opts.ContextPolicy != "" {
args["context_policy"] = opts.ContextPolicy
}
if opts.CleanerName != "" {
args["cleaner_name"] = opts.CleanerName
}
} }
// ---- 全局状态 ---- // ---- 全局状态 ----
@ -670,8 +695,10 @@ func buildPluginSDK(name string) *sdk.PluginSDK {
} }
handlerMu.Unlock() handlerMu.Unlock()
return callCoreVoid("input.register", map[string]interface{}{ return callCoreVoid("input.register", map[string]interface{}{
"name": chName, "name": chName,
"def": map[string]interface{}{"NoMemory": def.NoMemory}, // 整个结构体:手写字段白名单会把新增字段静默丢掉
// ChannelDef.Cleaner 已标 json:"-",可以整体 marshal
"def": def,
"has_cleaner": def.Cleaner != nil, "has_cleaner": def.Cleaner != nil,
}) })
}) })
@ -680,18 +707,32 @@ func buildPluginSDK(name string) *sdk.PluginSDK {
type procIO struct{} type procIO struct{}
// 下面六个三参数方法是 *Opts 变体的零值糖:记入记忆 + 不裁剪。
func (procIO) InjectText(s, c, t string) { func (procIO) InjectText(s, c, t string) {
// 忽略错误:注入是 fire-and-forget与内联路径语义一致 procIO{}.InjectTextOpts(s, c, t, sdk.InjectOptions{})
_, _ = callWithText("io.injectText", s, c, t)
} }
func (procIO) InjectInterruptText(s, c, t string) { func (procIO) InjectInterruptText(s, c, t string) {
_, _ = callWithText("io.injectInterrupt", s, c, t) procIO{}.InjectInterruptTextOpts(s, c, t, sdk.InjectOptions{})
} }
func (procIO) InjectTextNoMemory(s, c, t string) { func (procIO) InjectTextNoMemory(s, c, t string) {
_, _ = callWithText("io.injectTextNoMem", s, c, t) procIO{}.InjectTextOpts(s, c, t, sdk.InjectOptions{NoMemory: true})
} }
func (procIO) InjectInputSync(s, c, t string) string { func (procIO) InjectInputSync(s, c, t string) string {
raw, err := callWithText("io.injectInputSync", s, c, t) return procIO{}.InjectInputSyncOpts(s, c, t, sdk.InjectOptions{})
}
// 以下为带标志位的注入opts 决定这次注入是否进记忆、是否据此裁剪上下文。
func (procIO) InjectTextOpts(s, c, t string, opts sdk.InjectOptions) {
// 忽略错误:注入是 fire-and-forget与内联路径语义一致
_, _ = callWithTextOpts("io.injectText", s, c, t, opts)
}
func (procIO) InjectInterruptTextOpts(s, c, t string, opts sdk.InjectOptions) {
_, _ = callWithTextOpts("io.injectInterrupt", s, c, t, opts)
}
func (procIO) InjectInputSyncOpts(s, c, t string, opts sdk.InjectOptions) string {
raw, err := callWithTextOpts("io.injectInputSync", s, c, t, opts)
if err != nil { if err != nil {
return "" return ""
} }
@ -721,12 +762,28 @@ func (procIO) SetToolBlocks(blocks []sdk.ContentBlock) {
// 带媒体的注入:插件主动发起一轮带图/音频的对话。 // 带媒体的注入:插件主动发起一轮带图/音频的对话。
// 与 SetToolBlocks 的区别是媒体在**本轮**就到模型手上,而不是等下一条 tool message。 // 与 SetToolBlocks 的区别是媒体在**本轮**就到模型手上,而不是等下一条 tool message。
func (procIO) InjectInputMedia(s, c, t string, blocks []sdk.ContentBlock) { func (procIO) InjectInputMedia(s, c, t string, blocks []sdk.ContentBlock) {
callCoreVoid("io.injectMedia", mediaArgs(s, c, t, blocks)) procIO{}.InjectInputMediaOpts(s, c, t, blocks, sdk.InjectOptions{})
} }
func (procIO) InjectInputMediaSync(s, c, t string, blocks []sdk.ContentBlock) string { func (procIO) InjectInputMediaSync(s, c, t string, blocks []sdk.ContentBlock) string {
return procIO{}.InjectInputMediaSyncOpts(s, c, t, blocks, sdk.InjectOptions{})
}
func (procIO) InjectInterruptMedia(s, c, t string, blocks []sdk.ContentBlock) {
procIO{}.InjectInterruptMediaOpts(s, c, t, blocks, sdk.InjectOptions{})
}
func (procIO) InjectInputMediaOpts(s, c, t string, blocks []sdk.ContentBlock, opts sdk.InjectOptions) {
args, free := mediaArgsOwned(s, c, t, blocks) args, free := mediaArgsOwned(s, c, t, blocks)
defer free() defer free()
applyInjectOpts(args, opts)
callCoreVoid("io.injectMedia", args)
}
func (procIO) InjectInputMediaSyncOpts(s, c, t string, blocks []sdk.ContentBlock, opts sdk.InjectOptions) string {
args, free := mediaArgsOwned(s, c, t, blocks)
defer free()
applyInjectOpts(args, opts)
raw, err := callCore("io.injectMediaSync", args) raw, err := callCore("io.injectMediaSync", args)
if err != nil { if err != nil {
return "" return ""
@ -738,20 +795,13 @@ func (procIO) InjectInputMediaSync(s, c, t string, blocks []sdk.ContentBlock) st
return r.Reply return r.Reply
} }
func (procIO) InjectInterruptMedia(s, c, t string, blocks []sdk.ContentBlock) { func (procIO) InjectInterruptMediaOpts(s, c, t string, blocks []sdk.ContentBlock, opts sdk.InjectOptions) {
args, free := mediaArgsOwned(s, c, t, blocks) args, free := mediaArgsOwned(s, c, t, blocks)
defer free() defer free()
applyInjectOpts(args, opts)
callCoreVoid("io.injectInterruptMedia", args) callCoreVoid("io.injectInterruptMedia", args)
} }
// mediaArgs 构造媒体注入参数,并在返回前释放临时共享槽。
// 只能用在调用结束后立即返回的路径fire-and-forget
func mediaArgs(s, c, t string, blocks []sdk.ContentBlock) map[string]interface{} {
args, free := mediaArgsOwned(s, c, t, blocks)
defer free()
return args
}
// mediaArgsOwned 构造媒体注入参数,并返回释放函数。 // mediaArgsOwned 构造媒体注入参数,并返回释放函数。
// //
// 为什么要返回释放函数而不是自己 defer调用方可能是需要等应答的同步调用 // 为什么要返回释放函数而不是自己 defer调用方可能是需要等应答的同步调用

View File

@ -83,12 +83,27 @@ type ToolResult struct {
} }
type ToolDef struct { type ToolDef struct {
Name string `json:"name"` Name string `json:"name"`
Plugin string `json:"plugin,omitempty"` Plugin string `json:"plugin,omitempty"`
Description string `json:"description"` Description string `json:"description"`
Parameters map[string]interface{} `json:"parameters"` Parameters map[string]interface{} `json:"parameters"`
NoMemory bool `json:"no_memory,omitempty"` NoMemory bool `json:"no_memory,omitempty"`
Cleaner func(string) string `json:"-"` Cleaner func(string) string `json:"-"`
ContextPolicy string `json:"context_policy,omitempty"`
}
// 上下文策略取值,与公共 SDK 一致。
const (
ContextPolicyNone = "none"
ContextPolicyPrune = "prune"
)
// InjectOptions 与公共 SDK 同构:声明一次注入是否记入记忆、是否据此裁剪上下文、
// 以及用哪个已注册的通道 cleaner 清洗注入内容。
type InjectOptions struct {
NoMemory bool
ContextPolicy string
CleanerName string
} }
type IOInjector interface { type IOInjector interface {
@ -101,6 +116,14 @@ type IOInjector interface {
InjectInputMediaSync(source, channel, text string, blocks []ContentBlock) string InjectInputMediaSync(source, channel, text string, blocks []ContentBlock) string
InjectInterruptMedia(source, channel, text string, blocks []ContentBlock) InjectInterruptMedia(source, channel, text string, blocks []ContentBlock)
SetToolBlocks(blocks []ContentBlock) SetToolBlocks(blocks []ContentBlock)
// 1.2.0 带标志位的注入,与公共 SDK 同构。
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)
} }
// ContentBlock 与公共 SDK 同构OpenAI 多模态内容块格式)。 // ContentBlock 与公共 SDK 同构OpenAI 多模态内容块格式)。
@ -391,6 +414,29 @@ func (IOInjectorImpl) SetToolBlocks(blocks []ContentBlock) {
logf("set_tool_blocks: blocks=%d", len(blocks)) logf("set_tool_blocks: blocks=%d", len(blocks))
} }
// ---- 带 InjectOptions 的注入 ----
func (IOInjectorImpl) InjectTextOpts(source, channel, text string, opts InjectOptions) {
logf("inject_text_opts: source=%s channel=%s no_memory=%v policy=%s", source, channel, opts.NoMemory, opts.ContextPolicy)
}
func (IOInjectorImpl) InjectInterruptTextOpts(source, channel, text string, opts InjectOptions) {
logf("inject_interrupt_opts: source=%s channel=%s no_memory=%v policy=%s", source, channel, opts.NoMemory, opts.ContextPolicy)
}
func (IOInjectorImpl) InjectInputSyncOpts(source, channel, text string, opts InjectOptions) string {
logf("inject_sync_opts: source=%s channel=%s no_memory=%v policy=%s", source, channel, opts.NoMemory, opts.ContextPolicy)
return ""
}
func (IOInjectorImpl) InjectInputMediaOpts(source, channel, text string, blocks []ContentBlock, opts InjectOptions) {
logf("inject_input_media_opts: source=%s channel=%s blocks=%d no_memory=%v policy=%s", source, channel, len(blocks), opts.NoMemory, opts.ContextPolicy)
}
func (IOInjectorImpl) InjectInputMediaSyncOpts(source, channel, text string, blocks []ContentBlock, opts InjectOptions) string {
logf("inject_input_media_sync_opts: source=%s channel=%s blocks=%d no_memory=%v policy=%s", source, channel, len(blocks), opts.NoMemory, opts.ContextPolicy)
return ""
}
func (IOInjectorImpl) InjectInterruptMediaOpts(source, channel, text string, blocks []ContentBlock, opts InjectOptions) {
logf("inject_interrupt_media_opts: source=%s channel=%s blocks=%d no_memory=%v policy=%s", source, channel, len(blocks), opts.NoMemory, opts.ContextPolicy)
}
type PluginSDK struct { type PluginSDK struct {
Name string Name string
mu sync.RWMutex mu sync.RWMutex