Files
HomeAgent/internal/agent/io/inputch.go
JianFeeeee 3c961b7bd7 feat(channel): N1a —— inputch 一等化(归属插件/归属 agent/容量/共享登记表)+ 单工具多视图总览
设计:docs/zh/resident-subagent-design.md §4.6(里程碑 N1a)。

## 用户要求

「父 agent 可以看到所有已注册的 inputch 以及 inputch 的划分情况,用**单工具多视图**方式构筑」

## 登记层(internal/agent/io/inputch.go)

inputch 是**最基本的输入路由单位**(由插件注册,一个插件可注册多个),
所以登记表以 inputch 为键,每条记录:

    名字 / 归属插件 / 归属 agent(被划给谁) / 容量 / 默认回程 outputch / 记忆策略(ChannelDef)

- 新增 `ChannelRegistry`,设计成**可共享对象**(`*ChannelRegistry`):
  根 agent 与驻留子共用同一份,"划入/授权"才有意义;`SetChannelRegistry` 注入。
- **插件重载不得抹掉划分**:重复登记只更新「归属插件 + 策略」,
  保留已有 Owner/Capacity/Output(否则一次 reload 就把父做的划分清空)。
- `Assign` 语义按设计 R6 默认:**读写授权,不转移所有权**(Plugin 与 Owner 分别记录)。
- 原 `inputChannels map[string]ChannelDef` 被登记表取代;`GetInputChannelDef` 保持兼容。
- `plugin.Registry` 注册时带上**归属插件名**(此前完全无归属信息)。

## 总览工具(internal/agent/core/inputch.go)

单工具 **`input_channels`** + `view` 参数(不是一堆小工具):

    all(默认)= 全部已注册(带归属插件)
    mine       = 划给本 agent 的
    unassigned = 尚未划出的
    by_agent   = 划分情况总览(按归属分组)
    detail     = 单个 inputch 全字段(需 name)

未知 view **报错并列出可用值**(拼错不得被静默当成默认视图);登记表为空时明确说明。

## 验收

- `internal/agent/io/inputch_test.go`:归属记录、重载保划分、Assign/视图数据面、
  **跨 manager 共享登记表**、策略查询向后兼容 —— 5 项
- `internal/agent/core/inputch_test.go`:单工具多视图逐视图断言(含未知 view 与空表)—— 2 项
- 全仓 `go test ./...` 37 包 ok / 0 FAIL;`-race ./internal/agent/... ./internal/plugin/...` 干净
2026-09-13 09:06:50 +08:00

153 lines
4.9 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 io
// inputch 是**最基本的输入路由单位**(设计见 docs/zh/resident-subagent-design.md §4
//
// 一个插件可以注册多个 inputch每个 inputch 是彼此独立的路由单位:
// 可以被划给不同的 agent、可以分别限额。中断输入与排队输入**两类都从 inputch 进出**
// 而"中断 vs 排队"是每条输入自己的类别 —— 不是 inputch 的属性。
//
// 本文件只放**登记表**(谁是注册者、划给了谁、容量多少、记忆策略是什么)。
// 路由本身发生在进内核之前:投递方决定"这条输入投给哪个 inputch"。
import (
"errors"
"sort"
"sync"
)
var (
// ErrInputChannelUnknown 表示引用了未注册的 inputch。
ErrInputChannelUnknown = errors.New("inputch 未注册")
// ErrInputChannelNameEmpty 表示 inputch 名为空。
ErrInputChannelNameEmpty = errors.New("inputch 名不能为空")
)
// InputChannel 是一个 inputch 的完整登记记录。
type InputChannel struct {
// Name 是路由单位 id全局唯一如 "qq"、"webui"、"qq/device-2")。
Name string `json:"name"`
// Plugin 是注册它的插件名("插件可注册多个 inputch",归属可追溯)。
Plugin string `json:"plugin,omitempty"`
// Owner 是**被划给的 agent id**"" = 未分配,归根 agent/内核默认)。
Owner string `json:"owner,omitempty"`
// Capacity 是该 inputch 的队列容量0 = 用内核默认值)。
Capacity int `json:"capacity,omitempty"`
// Output 是该 inputch 的默认回程输出通道("" = 由来源/调用方决定)。
//
// 注意:这不是"内核路由"—— 输出仍然是 agent 的主动调用;这里只是登记
// "这个 inputch 的回复默认该往哪个 outputch 走"的映射依据。
Output string `json:"output,omitempty"`
// Def 是记忆/上下文策略(沿用 ChannelDefNoMemory / Cleaner / ContextPolicy
// Cleaner 是函数故本字段不可序列化json:"-")。
Def ChannelDef `json:"-"`
}
// ChannelRegistry 是 inputch 的登记表。
//
// 它被设计成**可共享对象**`*ChannelRegistry`):根 agent 与它的驻留子共用同一份,
// 这样"划入/授权"才有意义;每个 IOManager 默认自带一份(向后兼容)。
type ChannelRegistry struct {
mu sync.RWMutex
channels map[string]InputChannel
}
// NewChannelRegistry 构造一个空的 inputch 登记表。
func NewChannelRegistry() *ChannelRegistry {
return &ChannelRegistry{channels: make(map[string]InputChannel)}
}
// Register 登记/更新一个 inputch。
//
// 重复登记(插件重载)时**保留已有的 Owner/Capacity/Output**,只更新
// Plugin 与 Def —— 否则一次插件重载就会把父 agent 做的划分抹掉。
func (r *ChannelRegistry) Register(ch InputChannel) error {
if ch.Name == "" {
return ErrInputChannelNameEmpty
}
r.mu.Lock()
defer r.mu.Unlock()
if r.channels == nil {
r.channels = make(map[string]InputChannel)
}
if old, ok := r.channels[ch.Name]; ok {
if ch.Owner == "" {
ch.Owner = old.Owner
}
if ch.Capacity == 0 {
ch.Capacity = old.Capacity
}
if ch.Output == "" {
ch.Output = old.Output
}
if ch.Plugin == "" {
ch.Plugin = old.Plugin
}
}
r.channels[ch.Name] = ch
return nil
}
// Unregister 注销一个 inputch。
func (r *ChannelRegistry) Unregister(name string) {
r.mu.Lock()
defer r.mu.Unlock()
delete(r.channels, name)
}
// Lookup 查询一个 inputch。
func (r *ChannelRegistry) Lookup(name string) (InputChannel, bool) {
r.mu.RLock()
defer r.mu.RUnlock()
ch, ok := r.channels[name]
return ch, ok
}
// Assign 把一个 inputch **划给**某个 agent可同时给定容量
//
// 语义(默认取值,见设计文档 R6**读写授权**,不转移所有权 ——
// 登记表仍记录 Plugin谁注册的与 Owner划给了谁两件事。
func (r *ChannelRegistry) Assign(name, agentID string, capacity int) error {
r.mu.Lock()
defer r.mu.Unlock()
ch, ok := r.channels[name]
if !ok {
return ErrInputChannelUnknown
}
ch.Owner = agentID
if capacity > 0 {
ch.Capacity = capacity
}
r.channels[name] = ch
return nil
}
// List 返回全部已注册 inputch按名字稳定排序
func (r *ChannelRegistry) List() []InputChannel {
r.mu.RLock()
defer r.mu.RUnlock()
out := make([]InputChannel, 0, len(r.channels))
for _, ch := range r.channels {
out = append(out, ch)
}
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
return out
}
// ListByOwner 返回划给某个 agent 的 inputchowner == "" 时返回**未分配**的)。
func (r *ChannelRegistry) ListByOwner(agentID string) []InputChannel {
var out []InputChannel
for _, ch := range r.List() {
if ch.Owner == agentID {
out = append(out, ch)
}
}
return out
}
// Count 返回已注册 inputch 数量。
func (r *ChannelRegistry) Count() int {
r.mu.RLock()
defer r.mu.RUnlock()
return len(r.channels)
}