mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-21 17:38:10 +00:00
fix(plugin): "只声明出站通道"的告警改为插件加载完成后判定(此前按注册顺序误报 qq)
## 现象
生产日志(v1.3.1 启动)出现:
`[plugin] qq 只声明了输出通道 "qq",已按双向通道兜底登记 inputch;若要明确意图请显式 RegisterInputChannel`
用户据此问"qq 插件你没更新?"
## 查证:qq 没漏,是我的判据错了
- SDK 示例 `example/qq/plugin.go`:`RegisterOutputChannel("qq")` 在 368 行、
`RegisterInputChannel("qq", {NoMemory:true, Cleaner: inputCleaner})` 在 399 行 —— **先出站后入站**;
- 生产 `plugins/qq/plugin.bin`:版本 1.4.0,且二进制里含 `inputCleaner` 痕迹 ⇒ 确实调用了入站声明;
- 我的兜底告警在 **RegisterOutputChannel 的那一刻**判"有没有入站声明" ⇒ 对"先出站后入站"
这种完全合法的写法必然误报(a2a/acp/weather 同理)。
## 修法
告警判据从"注册时刻"改为"**插件 Start 结束后最终声明了什么**":
- `regOutput` 只保留兜底登记(功能不变),不再告警;
- 新增 `warnOutputOnlyChannels(plugin)`,在插件加载/重载成功后统一判定:
遍历该插件**最终**声明过的出站通道,只有始终没有对应入站声明的才告警,
且措辞改为"内核已兜底登记 inputch,若这是有意为之可忽略"。
- 判据与顺序解耦后,告警才代表真实缺口(例:weather 的 `weather_out` 与
`weather_in` 名字不同,出站名从未被声明为入站 —— 那条告警就是真的)。
## 验证
- 新增 `TestWarnOutputOnlyChannels`:①先出站后入站(qq 写法)**不告警**;
②只声明出站(weather 写法)**告警且只报那一个通道**。
- `go test ./internal/plugin/ ./internal/plugins/...` 全绿。
This commit is contained in:
43
internal/plugin/channel_warn_test.go
Normal file
43
internal/plugin/channel_warn_test.go
Normal file
@ -0,0 +1,43 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// 延迟判定的语义:看的是"插件 Start 结束后最终声明了什么",
|
||||
// 而不是"注册出站通道的那一刻有没有入站声明"。
|
||||
//
|
||||
// 为什么必须这样判:声明顺序自由 —— qq/weather 都是**先** RegisterOutputChannel
|
||||
// **后** RegisterInputChannel,按注册时刻判会把它们误报成"只声明了输出通道"
|
||||
// (实测发生过:用户据此以为 qq 插件没更新)。
|
||||
func TestWarnOutputOnlyChannels(t *testing.T) {
|
||||
r := NewRegistry()
|
||||
var buf bytes.Buffer
|
||||
oldOut := log.Writer()
|
||||
log.SetOutput(&buf)
|
||||
defer log.SetOutput(oldOut)
|
||||
|
||||
// ① 出站+入站都声明了(先出站后入站)⇒ 不该告警
|
||||
r.noteChannel("qq", "qq", true)
|
||||
r.noteChannel("qq", "qq", false)
|
||||
r.warnOutputOnlyChannels("qq")
|
||||
if s := buf.String(); s != "" {
|
||||
t.Fatalf("qq 声明了入站通道,不应告警,实际: %s", s)
|
||||
}
|
||||
|
||||
// ② 只声明出站 ⇒ 应告警,且只报这一个通道
|
||||
buf.Reset()
|
||||
r.noteChannel("weather", "weather_weather_out", true)
|
||||
r.noteChannel("weather", "weather_weather_in", false)
|
||||
r.warnOutputOnlyChannels("weather")
|
||||
out := buf.String()
|
||||
if !strings.Contains(out, "weather_weather_out") {
|
||||
t.Fatalf("只声明出站的通道应被告警,实际: %q", out)
|
||||
}
|
||||
if strings.Contains(out, "weather_weather_in") {
|
||||
t.Fatalf("已声明入站的通道不该被牵连,实际: %q", out)
|
||||
}
|
||||
}
|
||||
@ -307,12 +307,15 @@ func (r *Registry) buildSDK(name string) *sdk.PluginSDK {
|
||||
// 但历史插件常常只用 RegisterOutputChannel 声明(却用同一个名字注入输入,
|
||||
// 例:cli 只声明输出 "cli" 就用 InjectTextSync("cli", ...) 注入)。
|
||||
// 不兜底的话 inputch 登记表里没有它,"把 inputch 划给驻留子"直接失败
|
||||
// (实测报 `划入 inputch cli: inputch 未注册`)。兜底要**留痕**,
|
||||
// 否则插件作者永远不知道该补一行 RegisterInputChannel。
|
||||
// (实测报 `划入 inputch cli: inputch 未注册`)。
|
||||
//
|
||||
// ❗这里**不能**判"是否声明过入站通道"并告警:声明顺序是自由的,
|
||||
// 先 RegisterOutputChannel 再 RegisterInputChannel 是常见写法(qq 就是),
|
||||
// 按此刻的状态判会对它误报(实测:把 qq 报成"只声明了输出通道")。
|
||||
// 真正该问的问题是"插件 Start 结束后,这个出站通道有没有对应的入站声明" ——
|
||||
// 那在 load 完成后统一判(见 warnOutputOnlyChannels)。
|
||||
if _, ok := r.iom.LookupInputChannel(chName); !ok {
|
||||
_ = r.iom.RegisterInputChannelFrom(name, chName, agentIO.ChannelDef(def))
|
||||
log.Printf("[plugin] %s 只声明了输出通道 %q,已按双向通道兜底登记 inputch;"+
|
||||
"若要明确意图请显式 RegisterInputChannel", name, chName)
|
||||
}
|
||||
r.noteChannel(name, chName, true)
|
||||
return nil
|
||||
@ -452,6 +455,7 @@ func (r *Registry) Load(dir string) error {
|
||||
r.pluginAutoRestart[name] = plgSDK.AutoRestart()
|
||||
r.instances = append(r.instances, p)
|
||||
r.mu.Unlock()
|
||||
r.warnOutputOnlyChannels(name)
|
||||
log.Printf("[plugin] loaded: %s", name)
|
||||
}
|
||||
|
||||
@ -545,6 +549,7 @@ func (r *Registry) loadOne(plgDir, name string) bool {
|
||||
r.pluginAutoRestart[name] = plgSDK.AutoRestart()
|
||||
r.sdkRefs[name] = plgSDK
|
||||
r.instances = append(r.instances, plg)
|
||||
r.warnOutputOnlyChannels(name)
|
||||
if h := pluginEntryHash(plgDir); h != "" {
|
||||
r.pluginHashes[name] = h
|
||||
} else {
|
||||
@ -579,6 +584,31 @@ func (r *Registry) stageRegistrarFor() (func(plugin string, stage sdk.Stage, han
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// warnOutputOnlyChannels 在插件 Start 结束后,报告"只声明了出站、没有入站声明"的通道。
|
||||
//
|
||||
// 为什么放在 Start 之后:声明顺序自由(先出站后入站很常见),注册时刻的状态
|
||||
// 判不出意图。这里看的是**插件最终声明了什么**,因此不会误报 qq 这种写法。
|
||||
//
|
||||
// 注:这类通道内核已兜底登记 inputch(功能可用),告警只是提醒插件作者把意图写明。
|
||||
func (r *Registry) warnOutputOnlyChannels(plugin string) {
|
||||
r.channelsMu.Lock()
|
||||
set := r.pluginChannels[plugin]
|
||||
var only []string
|
||||
if set != nil {
|
||||
for ch := range set.outputs {
|
||||
if !set.inputs[ch] {
|
||||
only = append(only, ch)
|
||||
}
|
||||
}
|
||||
}
|
||||
r.channelsMu.Unlock()
|
||||
sort.Strings(only)
|
||||
for _, ch := range only {
|
||||
log.Printf("[plugin] %s 只声明了出站通道 %q(未 RegisterInputChannel);"+
|
||||
"内核已兜底登记 inputch,若这是有意为之可忽略", plugin, ch)
|
||||
}
|
||||
}
|
||||
|
||||
// noteChannel 记住插件注册了哪个通道,供卸载/崩溃时摘除。
|
||||
// forgetChannel 把某个通道从"本插件注册过哪些通道"的记账里摘掉(注销通道时用)。
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user