mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-21 09:28:14 +00:00
feat(channel): N1b —— 输出通道授权集合(三处过滤一致)+ 输出通道→目标 agent 的 inputch 解析
设计:docs/zh/resident-subagent-design.md §4.5(里程碑 N1b)。 ## 输出通道授权集合(默认完整授权,父可收窄) `AgentConfig.AllowedOutputs`(nil/空 = 完整授权)。三处过滤点必须一致, 否则会出现「列表里看不到、按名字还能调」的裂缝: 1. **工具表**:不为未授权的通道生成 output_send__X(模型看不到就不会调) 2. **列表工具**:output_list_channels 只列授权的 3. **调用点**:凭名字直调未授权的输出门必须被拒(纵深防御) ## 输出通道 → 目标 agent 的 inputch 解析 `ChannelRegistry.BindOutputTarget / ResolveOutputTarget`: 把输出通道解析成「目标 agent + 目标 inputch」,这是"输出可寻址到具体 agent" (子→父、父→指定子)的**数据面**;真正的跨 agent 投递在里程碑 N4。 未登记的输出通道 ok=false —— 表示由传输层 device 自行处理(qq/webui 这类)。 已登记目标的输出通道,在 output_list_channels 里会标出「目标: <agent> / inputch <名字>」。 ## 验收 `internal/agent/core/output_grant_test.go`(3 项): - 默认完整授权:全部输出门生成 + 列表含全部 - 白名单收窄:三个过滤点同时生效(工具表 / 列表 / 直调被拒) - 目标解析:绑定/解析、未登记由传输层处理、列表标出目标、空名报错 全仓 go test ./... 37 包 ok / 0 FAIL。
This commit is contained in:
@ -112,7 +112,23 @@
|
|||||||
- **子 → 主**:子直接打到主(经输出通道投进主的 inputch);
|
- **子 → 主**:子直接打到主(经输出通道投进主的 inputch);
|
||||||
- **主 → 指定某个子**:父经输出通道投进**指定子**的 inputch。
|
- **主 → 指定某个子**:父经输出通道投进**指定子**的 inputch。
|
||||||
|
|
||||||
### 4.5 通道的一等化(实现要求)
|
### 4.5 已经落地/待落地的两件事
|
||||||
|
|
||||||
|
**已落地(N1a)**:inputch 登记表(归属插件 / 归属 agent / 容量 / 默认回程 / 策略)+ 共享登记表
|
||||||
|
+ 单工具多视图总览(`input_channels`,见 §4.6)。
|
||||||
|
|
||||||
|
**已落地(N1b)**:
|
||||||
|
- **输出通道授权集合**[已定:默认完整授权,父可收窄]:
|
||||||
|
`AgentConfig.AllowedOutputs`(nil/空 = 全部)。三处过滤点必须一致,
|
||||||
|
否则会出现"列表里看不到、按名字还能调"的裂缝:
|
||||||
|
1. **工具表**:不为未授权的通道生成 `output_send__X`(模型看不到就不会调);
|
||||||
|
2. **列表工具**:`output_list_channels` 只列授权的(已登记目标的会标出"目标: agent / inputch");
|
||||||
|
3. **调用点**:凭名字直调未授权的输出门**必须被拒**(纵深防御)。
|
||||||
|
- **输出通道 → 目标 agent 的 inputch 解析**:`ChannelRegistry.BindOutputTarget` /
|
||||||
|
`ResolveOutputTarget`(未登记的通道由传输层 device 自行处理,如 qq/webui)。
|
||||||
|
这是"输出可寻址到具体 agent"的数据面;真正的跨 agent 投递在 N4。
|
||||||
|
|
||||||
|
### 4.5.1 通道的一等化(后续要求)
|
||||||
|
|
||||||
现在 `Source` / `OutputChannel` 只是字符串标签,`IOManager.inputCh` 是**一条全局 channel**,
|
现在 `Source` / `OutputChannel` 只是字符串标签,`IOManager.inputCh` 是**一条全局 channel**,
|
||||||
`inputChannels` 只是策略表(`ChannelDef`:NoMemory / Cleaner / ContextPolicy),
|
`inputChannels` 只是策略表(`ChannelDef`:NoMemory / Cleaner / ContextPolicy),
|
||||||
|
|||||||
@ -68,6 +68,9 @@ type Agent struct {
|
|||||||
// 为 nil 时门禁与工具都静默关闭(例如单测里不接配置的场景)。
|
// 为 nil 时门禁与工具都静默关闭(例如单测里不接配置的场景)。
|
||||||
personaStore PersonaStore
|
personaStore PersonaStore
|
||||||
|
|
||||||
|
// 被授权的输出通道集合(空 = 完整授权,见 AgentConfig.AllowedOutputs)。
|
||||||
|
allowedOutputs []string
|
||||||
|
|
||||||
// 插件注册表(用于 plgreload)
|
// 插件注册表(用于 plgreload)
|
||||||
pluginReg *plugin.Registry
|
pluginReg *plugin.Registry
|
||||||
pluginDir string
|
pluginDir string
|
||||||
@ -200,12 +203,17 @@ type AgentConfig struct {
|
|||||||
MultimodalSpace vector.MultimodalEmbedder
|
MultimodalSpace vector.MultimodalEmbedder
|
||||||
// EmbeddingProvider / EmbeddingError 是向量空间的配置身份与打开失败原因,
|
// EmbeddingProvider / EmbeddingError 是向量空间的配置身份与打开失败原因,
|
||||||
// 供 healthcheck_kernel 状态报告区分「未配置 / 打开失败 / 已启用」。
|
// 供 healthcheck_kernel 状态报告区分「未配置 / 打开失败 / 已启用」。
|
||||||
EmbeddingProvider string
|
EmbeddingProvider string
|
||||||
EmbeddingError string
|
EmbeddingError string
|
||||||
FusionCfg CrossModalFusionConfig // 跨模态融合权重;零值用默认
|
FusionCfg CrossModalFusionConfig // 跨模态融合权重;零值用默认
|
||||||
Personality *agentPkg.Personality
|
Personality *agentPkg.Personality
|
||||||
PersonaStore PersonaStore // 人格设定的读写面(首启门禁 + persona_set 工具)
|
PersonaStore PersonaStore // 人格设定的读写面(首启门禁 + persona_set 工具)
|
||||||
PluginReg *plugin.Registry
|
PluginReg *plugin.Registry
|
||||||
|
// AllowedOutputs 是本 agent **被授权的输出通道集合**(设计 §4.4 / R2)。
|
||||||
|
//
|
||||||
|
// nil 或空 = **完整授权**(默认);非空 = 白名单,只允许列出的输出通道。
|
||||||
|
// 父 agent 创建驻留子时用它收窄子的输出能力。
|
||||||
|
AllowedOutputs []string
|
||||||
PluginDir string
|
PluginDir string
|
||||||
DistillInterval time.Duration
|
DistillInterval time.Duration
|
||||||
ArchiveInterval time.Duration // 冷文档归档间隔(L2→L3),0 则使用 DistillInterval
|
ArchiveInterval time.Duration // 冷文档归档间隔(L2→L3),0 则使用 DistillInterval
|
||||||
@ -296,6 +304,7 @@ func New(cfg AgentConfig) *Agent {
|
|||||||
mediaStore: cfg.MediaStore,
|
mediaStore: cfg.MediaStore,
|
||||||
personality: cfg.Personality,
|
personality: cfg.Personality,
|
||||||
personaStore: cfg.PersonaStore,
|
personaStore: cfg.PersonaStore,
|
||||||
|
allowedOutputs: cfg.AllowedOutputs,
|
||||||
pluginReg: cfg.PluginReg,
|
pluginReg: cfg.PluginReg,
|
||||||
pluginDir: cfg.PluginDir,
|
pluginDir: cfg.PluginDir,
|
||||||
distillInterval: cfg.DistillInterval,
|
distillInterval: cfg.DistillInterval,
|
||||||
@ -344,6 +353,31 @@ func (a *Agent) Stop() {
|
|||||||
|
|
||||||
func (a *Agent) ID() types.AgentID { return a.id }
|
func (a *Agent) ID() types.AgentID { return a.id }
|
||||||
|
|
||||||
|
// IsOutputAllowed 报告某个输出通道是否被授权给本 agent。
|
||||||
|
//
|
||||||
|
// 默认(未配置白名单)= **完整授权**;这是"默认完整授权、父可收窄"的落点。
|
||||||
|
func (a *Agent) IsOutputAllowed(channel string) bool {
|
||||||
|
if len(a.allowedOutputs) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, c := range a.allowedOutputs {
|
||||||
|
if c == channel {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResolveOutputTarget 解析输出通道的投递目标(agent + inputch)。
|
||||||
|
//
|
||||||
|
// ok=false 表示该输出通道由传输层(device 通道,如 qq/webui)自行处理。
|
||||||
|
func (a *Agent) ResolveOutputTarget(channel string) (agentIO.OutputTarget, bool) {
|
||||||
|
if a.io == nil || a.io.ChannelRegistry() == nil {
|
||||||
|
return agentIO.OutputTarget{}, false
|
||||||
|
}
|
||||||
|
return a.io.ChannelRegistry().ResolveOutputTarget(channel)
|
||||||
|
}
|
||||||
|
|
||||||
// isDuplicateInput 判断是否为短窗口内的重复输入(防 webui/GUI 断线重连消息重放)。
|
// isDuplicateInput 判断是否为短窗口内的重复输入(防 webui/GUI 断线重连消息重放)。
|
||||||
// key=source+"|"+content;窗口内重复返回 true 并刷新时间戳(持续轰炸时保持拦截)。
|
// key=source+"|"+content;窗口内重复返回 true 并刷新时间戳(持续轰炸时保持拦截)。
|
||||||
const duplicateInputWindow = 10 * time.Second
|
const duplicateInputWindow = 10 * time.Second
|
||||||
|
|||||||
@ -16,6 +16,11 @@ func (a *Agent) executeOutputSendTool(tc agentAPI.ToolCall) string {
|
|||||||
if channel == "" || payload == "" || rawType == "" {
|
if channel == "" || payload == "" || rawType == "" {
|
||||||
return "工具名称格式: output_send__{channel},payload 和 type 不能为空"
|
return "工具名称格式: output_send__{channel},payload 和 type 不能为空"
|
||||||
}
|
}
|
||||||
|
// 授权闸(纵深防御):模型可能凭名字直接调未授权的输出门。
|
||||||
|
if !a.IsOutputAllowed(channel) {
|
||||||
|
return fmt.Sprintf("通道 [%s] 未授权给本 agent。可用通道见 output_list_channels", channel)
|
||||||
|
}
|
||||||
|
|
||||||
meta, _ := tc.Arguments["meta"].(string)
|
meta, _ := tc.Arguments["meta"].(string)
|
||||||
|
|
||||||
caps := a.io.GetChannelCapabilities(channel)
|
caps := a.io.GetChannelCapabilities(channel)
|
||||||
@ -146,7 +151,14 @@ func (a *Agent) executeOutputListChannels() string {
|
|||||||
if ch.OutputCaps == 0 {
|
if ch.OutputCaps == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
parts = append(parts, fmt.Sprintf(" - %s: [%s] %s", ch.Name, ch.OutputCaps.String(), ch.Description))
|
if !a.IsOutputAllowed(ch.Name) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
line := fmt.Sprintf(" - %s: [%s] %s", ch.Name, ch.OutputCaps.String(), ch.Description)
|
||||||
|
if t, ok := a.ResolveOutputTarget(ch.Name); ok {
|
||||||
|
line += fmt.Sprintf("(目标: %s / inputch %s)", orDash(t.AgentID), orDash(t.InputCh))
|
||||||
|
}
|
||||||
|
parts = append(parts, line)
|
||||||
for _, t := range ch.Tools {
|
for _, t := range ch.Tools {
|
||||||
parts = append(parts, fmt.Sprintf(" 工具: %s - %s", t.Name, t.Description))
|
parts = append(parts, fmt.Sprintf(" 工具: %s - %s", t.Name, t.Description))
|
||||||
}
|
}
|
||||||
|
|||||||
138
internal/agent/core/output_grant_test.go
Normal file
138
internal/agent/core/output_grant_test.go
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
// N1b:输出通道授权集合 + 输出通道 → 目标 agent 的 inputch 解析。
|
||||||
|
//
|
||||||
|
// 设计依据 docs/zh/resident-subagent-design.md §4.4(通道分配:不对称)与 R2
|
||||||
|
// (插件与工具由父授权,**默认完整授权**)。
|
||||||
|
//
|
||||||
|
// 三个过滤点必须一致,否则会出现"列表里看不到、但按名字还能调"的裂缝:
|
||||||
|
// ① 工具表(不为未授权的通道生成 output_send__X)
|
||||||
|
// ② 列表工具(output_list_channels 只列授权的)
|
||||||
|
// ③ 调用点(凭名字直调也必须被拒 —— 纵深防御)
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
agentAPI "gitcode.com/JianFeeeee/HomeAgent/internal/agent/api"
|
||||||
|
agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// registerFakeOutput 注册一个假的输出通道(device 通道)。
|
||||||
|
func registerFakeOutput(t *testing.T, a *Agent, name string) {
|
||||||
|
t.Helper()
|
||||||
|
if err := a.io.RegisterDevice(&mockOutputDevice{name: name, caps: agentIO.CapText}); err != nil {
|
||||||
|
t.Fatalf("注册测试通道 %s 失败: %v", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toolNames(a *Agent) []string {
|
||||||
|
var names []string
|
||||||
|
for _, t := range a.buildToolDefs() {
|
||||||
|
m, ok := t.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fn, _ := m["function"].(map[string]interface{})
|
||||||
|
if n, _ := fn["name"].(string); n != "" {
|
||||||
|
names = append(names, n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasTool(names []string, want string) bool {
|
||||||
|
for _, n := range names {
|
||||||
|
if n == want {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认(未配置白名单)= 完整授权:所有输出通道都能用。
|
||||||
|
func TestOutputGrant_DefaultIsFull(t *testing.T) {
|
||||||
|
a := newPreemptAgent(t, newPreemptProvider())
|
||||||
|
registerFakeOutput(t, a, "qq")
|
||||||
|
registerFakeOutput(t, a, "webui")
|
||||||
|
|
||||||
|
if !a.IsOutputAllowed("qq") || !a.IsOutputAllowed("webui") {
|
||||||
|
t.Fatal("默认应为完整授权")
|
||||||
|
}
|
||||||
|
names := toolNames(a)
|
||||||
|
if !hasTool(names, "output_send__qq") || !hasTool(names, "output_send__webui") {
|
||||||
|
t.Fatalf("默认完整授权下应生成全部输出门,实际 %v", names)
|
||||||
|
}
|
||||||
|
if out := a.executeOutputListChannels(); !strings.Contains(out, "qq") || !strings.Contains(out, "webui") {
|
||||||
|
t.Fatalf("默认完整授权下列表应含全部通道:\n%s", out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 白名单收窄:三个过滤点必须一致。
|
||||||
|
func TestOutputGrant_NarrowedWhitelist(t *testing.T) {
|
||||||
|
a := newPreemptAgent(t, newPreemptProvider())
|
||||||
|
a.allowedOutputs = []string{"webui"} // 模拟父创建子时收窄
|
||||||
|
registerFakeOutput(t, a, "qq")
|
||||||
|
registerFakeOutput(t, a, "webui")
|
||||||
|
|
||||||
|
if a.IsOutputAllowed("qq") {
|
||||||
|
t.Fatal("白名单外的通道不应被授权")
|
||||||
|
}
|
||||||
|
if !a.IsOutputAllowed("webui") {
|
||||||
|
t.Fatal("白名单内的通道应被授权")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ① 工具表
|
||||||
|
names := toolNames(a)
|
||||||
|
if hasTool(names, "output_send__qq") {
|
||||||
|
t.Fatalf("未授权的通道不该生成输出门工具:%v", names)
|
||||||
|
}
|
||||||
|
if !hasTool(names, "output_send__webui") {
|
||||||
|
t.Fatalf("已授权的通道应生成输出门工具:%v", names)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ② 列表工具
|
||||||
|
out := a.executeOutputListChannels()
|
||||||
|
if strings.Contains(out, "qq") {
|
||||||
|
t.Fatalf("列表不应含未授权通道:\n%s", out)
|
||||||
|
}
|
||||||
|
if !strings.Contains(out, "webui") {
|
||||||
|
t.Fatalf("列表应含已授权通道:\n%s", out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ③ 调用点(凭名字直调)
|
||||||
|
got := a.executeOutputSendTool(agentAPI.ToolCall{
|
||||||
|
ID: "c1", Name: "output_send__qq",
|
||||||
|
Arguments: map[string]interface{}{"payload": "hi", "type": "text"},
|
||||||
|
})
|
||||||
|
if !strings.Contains(got, "未授权") {
|
||||||
|
t.Fatalf("未授权的输出门必须被拒,实际 %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 输出通道 → 目标 agent 的 inputch 的解析("输出可寻址到具体 agent")。
|
||||||
|
func TestOutputTarget_Resolution(t *testing.T) {
|
||||||
|
a := newPreemptAgent(t, newPreemptProvider())
|
||||||
|
reg := a.io.ChannelRegistry()
|
||||||
|
|
||||||
|
if err := reg.BindOutputTarget("to-child-1", "child-1", "sub/in"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
tgt, ok := a.ResolveOutputTarget("to-child-1")
|
||||||
|
if !ok || tgt.AgentID != "child-1" || tgt.InputCh != "sub/in" {
|
||||||
|
t.Fatalf("解析结果=%+v ok=%v", tgt, ok)
|
||||||
|
}
|
||||||
|
// 未登记的输出通道由传输层处理(如 qq/webui 这类 device 通道)。
|
||||||
|
if _, ok := a.ResolveOutputTarget("qq"); ok {
|
||||||
|
t.Fatal("未登记目标解析的输出通道不应解析出 agent")
|
||||||
|
}
|
||||||
|
if err := reg.BindOutputTarget("", "x", "y"); err == nil {
|
||||||
|
t.Fatal("空输出通道名应报错")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 列表工具在已登记时带出目标,便于模型知道"这条通道发给谁"。
|
||||||
|
registerFakeOutput(t, a, "to-child-1")
|
||||||
|
if out := a.executeOutputListChannels(); !strings.Contains(out, "child-1") {
|
||||||
|
t.Fatalf("已登记目标的输出通道应在列表里标出目标:\n%s", out)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -580,6 +580,11 @@ func (a *Agent) buildToolDefs() []interface{} {
|
|||||||
if ch.Type != agentIO.DeviceOutput && ch.Type != agentIO.DeviceIO {
|
if ch.Type != agentIO.DeviceOutput && ch.Type != agentIO.DeviceIO {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
// 输出通道授权(设计 §4.4/R2):默认完整授权;父可用白名单收窄子的输出能力。
|
||||||
|
// 未授权就不生成 output_send__X —— 模型看不到它,自然不会调。
|
||||||
|
if !a.IsOutputAllowed(ch.Name) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
capStr := a.io.GetChannelCapabilities(ch.Name).String()
|
capStr := a.io.GetChannelCapabilities(ch.Name).String()
|
||||||
desc := ch.Description
|
desc := ch.Description
|
||||||
if desc == "" {
|
if desc == "" {
|
||||||
|
|||||||
@ -49,11 +49,25 @@ type InputChannel struct {
|
|||||||
type ChannelRegistry struct {
|
type ChannelRegistry struct {
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
channels map[string]InputChannel
|
channels map[string]InputChannel
|
||||||
|
// outputTargets 把**输出通道**解析成"目标 agent 的哪个 inputch"。
|
||||||
|
// 这是"输出可寻址到具体 agent"的依据(子→父、父→指定子)。
|
||||||
|
outputTargets map[string]OutputTarget
|
||||||
|
}
|
||||||
|
|
||||||
|
// OutputTarget 是一个输出通道的投递目标。
|
||||||
|
type OutputTarget struct {
|
||||||
|
// AgentID 是目标 agent("" = 本 agent / 由传输层通道 device 自行处理)。
|
||||||
|
AgentID string `json:"agent_id,omitempty"`
|
||||||
|
// InputCh 是目标 agent 上接收它的 inputch("" = 与输出通道同名)。
|
||||||
|
InputCh string `json:"inputch,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewChannelRegistry 构造一个空的 inputch 登记表。
|
// NewChannelRegistry 构造一个空的 inputch 登记表。
|
||||||
func NewChannelRegistry() *ChannelRegistry {
|
func NewChannelRegistry() *ChannelRegistry {
|
||||||
return &ChannelRegistry{channels: make(map[string]InputChannel)}
|
return &ChannelRegistry{
|
||||||
|
channels: make(map[string]InputChannel),
|
||||||
|
outputTargets: make(map[string]OutputTarget),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register 登记/更新一个 inputch。
|
// Register 登记/更新一个 inputch。
|
||||||
@ -144,6 +158,43 @@ func (r *ChannelRegistry) ListByOwner(agentID string) []InputChannel {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BindOutputTarget 登记"输出通道 → 目标 agent 的 inputch"的解析。
|
||||||
|
//
|
||||||
|
// 例:父把子用的输出通道 "to-child-1" 绑到 (child-1, "sub/in"),
|
||||||
|
// 于是子经该通道发出的消息会投进 child-1 的 sub/in。
|
||||||
|
func (r *ChannelRegistry) BindOutputTarget(output, agentID, inputCh string) error {
|
||||||
|
if output == "" {
|
||||||
|
return errors.New("输出通道名不能为空")
|
||||||
|
}
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
if r.outputTargets == nil {
|
||||||
|
r.outputTargets = make(map[string]OutputTarget)
|
||||||
|
}
|
||||||
|
r.outputTargets[output] = OutputTarget{AgentID: agentID, InputCh: inputCh}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResolveOutputTarget 解析一个输出通道的目标;未登记时 ok=false
|
||||||
|
// (意味着由传输层通道自行处理,如 qq/webui 这类 device 通道)。
|
||||||
|
func (r *ChannelRegistry) ResolveOutputTarget(output string) (OutputTarget, bool) {
|
||||||
|
r.mu.RLock()
|
||||||
|
defer r.mu.RUnlock()
|
||||||
|
t, ok := r.outputTargets[output]
|
||||||
|
return t, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListOutputTargets 返回全部已登记的目标解析(按输出通道名排序)。
|
||||||
|
func (r *ChannelRegistry) ListOutputTargets() map[string]OutputTarget {
|
||||||
|
r.mu.RLock()
|
||||||
|
defer r.mu.RUnlock()
|
||||||
|
out := make(map[string]OutputTarget, len(r.outputTargets))
|
||||||
|
for k, v := range r.outputTargets {
|
||||||
|
out[k] = v
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// Count 返回已注册 inputch 数量。
|
// Count 返回已注册 inputch 数量。
|
||||||
func (r *ChannelRegistry) Count() int {
|
func (r *ChannelRegistry) Count() int {
|
||||||
r.mu.RLock()
|
r.mu.RLock()
|
||||||
|
|||||||
Reference in New Issue
Block a user