diff --git a/internal/plugin/channel_warn_test.go b/internal/plugin/channel_warn_test.go new file mode 100644 index 0000000..b43c542 --- /dev/null +++ b/internal/plugin/channel_warn_test.go @@ -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) + } +} diff --git a/internal/plugin/registry.go b/internal/plugin/registry.go index 2a39c17..f8aa71d 100644 --- a/internal/plugin/registry.go +++ b/internal/plugin/registry.go @@ -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 把某个通道从"本插件注册过哪些通道"的记账里摘掉(注销通道时用)。 //