Skip to content

refactor(messaging): SOLID + coupling — PlatformAdapter concrete Bridge dep, empty SessionManager, ExtractPlatformKeys switch, bidirectional init #337

@aaronwong1989

Description

@aaronwong1989

Background

internal/messaging/ 共享层包含 21 个源文件(~4,759 行),负责平台适配器基类、Session bridge、交互管理、斜杠命令解析、文本格式化。Slack 和 Feishu 子模块各自嵌入 BaseAdapter 实现平台特定逻辑。模块已有 issue 257 跟踪子模块间 DRY 问题。本次为 首次从 SOLID + coupling 角度分析共享层本身。

Scope: solid, dry, coupling — cycle 3 (模块分析通过 1)
Key files: platform_adapter.go, bridge.go, control_command.go, interaction.go, command_executor.go


Finding Summary

Category Critical High Medium Low
SOLID/SRP 0 0 2 0
SOLID/OCP 0 0 2 0
SOLID/ISP 0 0 1 0
DRY 0 1 1 0
Coupling 0 1 1 0
合计 0 2 7 0

Findings

Coupling

platformadapter-concrete-bridge-dependency

Severity: High | Confidence: High | ROI: High
Location: platform_adapter.go:132 (bridge *Bridge)

Problem: PlatformAdapter 持有具体 *Bridge 指针而非接口。所有平台适配器(Slack/Feishu)通过嵌入的 PlatformAdapter 间接依赖具体 *messaging.Bridge。这意味着:

  • 测试任何适配器必须构造真实 Bridge
  • Bridge API 变更波及所有适配器
  • HubInterface/HandlerInterface(已使用接口)的模式不一致

Current Pattern:

// platform_adapter.go:126-142 — concrete *Bridge
type PlatformAdapter struct {
    Log       *slog.Logger
    hub       HubInterface        // interface — good
    sm        SessionManager       // empty interface — bad (see I1)
    handler   HandlerInterface     // interface — good
    bridge    *Bridge             // concrete type — inconsistent
    Dedup     *Dedup
    Gate      *Gate
    Interactions *InteractionManager
    // ...
}

Proposed Fix: 定义 BridgeInterface 并在 PlatformAdapter 中使用:

type BridgeInterface interface {
    Handle(ctx context.Context, env *events.Envelope, pc PlatformConn) error
    JoinSession(ctx context.Context, sessionID string, pc PlatformConn) error
    Platform() PlatformType
    SetAdapter(adapter PlatformAdapterInterface)
}

type PlatformAdapter struct {
    bridge    BridgeInterface  // interface, not concrete
    // ...
}

Estimated Impact: 适配器可独立测试,与 HubInterface/HandlerInterface 模式一致

Acceptance Criteria:

  • BridgeInterface 定义在 platform_adapter.go
  • PlatformAdapter.bridge 类型从 *Bridge 改为 BridgeInterface
  • Bridge 实现 BridgeInterface
  • slack/feishu 适配器测试使用 mock BridgeInterface

DRY

four-parallel-command-maps

Severity: High | Confidence: High | ROI: High
Location: control_command.go:19, 31, 105, 120

Problem: 4 个概念相同的命令映射使用相同 lookup 模式但独立定义:

  • slashCommandMap (line 19) — /gcControlAction
  • naturalLanguageMap (line 31) — $休眠ControlAction
  • workerSlashMap (line 105) — /modelWorkerCommand
  • workerNLMap (line 120) — $模型WorkerCommand

ParseControlCommandParseWorkerCommand 也是结构相同的方法(normalize → lookup → return),仅返回类型不同。

Current Pattern:

// control_command.go:19-30 — ControlAction maps
var slashCommandMap = map[string]ControlAction{"/gc": ActionGC, "/reset": ActionReset, ...}
var naturalLanguageMap = map[string]ControlAction{"$休眠": ActionGC, "$重置": ActionReset, ...}

// control_command.go:105-118 — WorkerCommand maps
var workerSlashMap = map[string]WorkerCommand{"/model": CmdSetModel, "/skills": CmdSkills, ...}
var workerNLMap = map[string]WorkerCommand{"$模型": CmdSetModel, "$技能": CmdSkills, ...}

// Both parse functions: normalize(text) → map[text] → return(result, true)

Proposed Fix: 泛型命令映射类型:

type CommandMap[T any] struct {
    slash    map[string]T
    natural  map[string]T
}

func NewCommandMap[T any](slash, natural map[string]T) *CommandMap[T] { ... }

func (m *CommandMap[T]) Parse(text string) (T, bool) {
    normalized := strings.ToLower(strings.TrimSpace(text))
    if v, ok := m.slash[normalized]; ok { return v, true }
    if v, ok := m.natural[normalized]; ok { return v, true }
    var zero T; return zero, false
}

Estimated Impact: ~30 行减少,新增命令只需添加 map entry

Acceptance Criteria:

  • CommandMap[T] 泛型类型定义
  • ParseControlCommandParseWorkerCommand 使用 CommandMap.Lookup
  • 现有命令解析测试零回归

SOLID: SRP

platform-adapter-go-mixed-responsibilities

Severity: Medium | Confidence: High | ROI: Medium
Location: platform_adapter.go (215 lines)

Problem: 单文件混合 6 种职责:(1) PlatformType 类型+常量, (2) ExtractPlatformKeys switch, (3) PlatformAdapterInterface 接口, (4) Register/New 注册表, (5) PlatformAdapter 基础 struct, (6) HubInterface/HandlerInterface/SessionStarter 网关接口。新增平台类型需同时修改常量、switch、registry。

Current Pattern:

// platform_adapter.go — all in one file:
type PlatformType string  // type definition (line 14)
const (PlatformSlack, PlatformFeishu)  // constants (line 16-20)
func (p PlatformType) ExtractPlatformKeys(...) { switch p { ... } }  // OCP violation (line 23)
type PlatformAdapterInterface interface { ... }  // interface (line 58)
func Register(...) { }  // registry (line 82)
func New(...) { }  // factory (line 99)
type PlatformAdapter struct { ... }  // base struct (line 126)
type HubInterface interface { ... }  // gateway interface (line 145)

Proposed Fix: 按职责拆分:

platform_types.go     — PlatformType + constants + ExtractPlatformKeys
platform_interfaces.go — HubInterface, HandlerInterface, SessionStarter, BridgeInterface
platform_registry.go  — Register, New, RegisteredTypes
platform_adapter.go   — PlatformAdapter struct + PlatformAdapterInterface

Estimated Impact: 新增平台只需修改 types.go,不影响 adapter/registry/interfaces

Acceptance Criteria:

  • platform_adapter.go 拆分为 4 个文件
  • 每个文件单一职责
  • make test 零回归

bridge-platform-specific-envelopes

Severity: Medium | Confidence: High | ROI: Medium
Location: bridge.go:155-184 (MakeSlackEnvelope, MakeFeishuEnvelope)

Problem: Bridge 是平台无关的路由层,但包含 Slack 和 Feishu 特定的 envelope 构建方法。新增平台需要修改此文件。

Current Pattern:

// bridge.go:155-184 — platform-specific methods on platform-agnostic Bridge
func (b *Bridge) MakeSlackEnvelope(sessionID, channelID, threadTS string, env *events.Envelope) *events.Envelope {
    return b.MakeEnvelope(sessionID, map[string]string{
        "channel_id": channelID, "thread_ts": threadTS,
    }, env)
}

func (b *Bridge) MakeFeishuEnvelope(sessionID, chatID, msgID string, env *events.Envelope) *events.Envelope {
    return b.MakeEnvelope(sessionID, map[string]string{
        "chat_id": chatID, "message_id": msgID,
    }, env)
}

Proposed Fix: 只保留通用的 MakeEnvelope,平台适配器自行构建 platformKey map:

// Bridge only provides the generic method
func (b *Bridge) MakeEnvelope(sessionID string, platformKey map[string]string, env *events.Envelope) *events.Envelope {
    // ... existing logic
}

// Slack adapter calls:
envelope := bridge.MakeEnvelope(sessionID, map[string]string{"channel_id": chID, "thread_ts": ts}, env)

Estimated Impact: 消除 Bridge 对平台细节的依赖

Acceptance Criteria:

  • MakeSlackEnvelopeMakeFeishuEnvelope 从 Bridge 中移除
  • Slack/Feishu 适配器直接调用 MakeEnvelope 并传入 platformKey
  • make test 零回归

SOLID: OCP

extractplatformkeys-switch-chain

Severity: Medium | Confidence: High | ROI: Medium
Location: platform_adapter.go:23-55

Problem: PlatformType.ExtractPlatformKeys() 用 switch 分发到 Slack/Feishu 特定的字段提取逻辑。新增平台必须修改此方法。这与 adapter registration pattern(Register/New)矛盾 — registry 是开放的但 key extraction 是封闭的。

Current Pattern:

// platform_adapter.go:23-55
func (p PlatformType) ExtractPlatformKeys(conn any) (map[string]string, error) {
    switch p {
    case PlatformSlack:
        sc, ok := conn.(*SlackConn)
        // extract channel_id, thread_ts
    case PlatformFeishu:
        fc, ok := conn.(*FeishuConn)
        // extract chat_id, message_id
    default:
        return nil, fmt.Errorf("unsupported platform: %s", p)
    }
}

Proposed Fix: 将 key extraction 注册到 adapter registry:

type AdapterBuilder struct {
    NewFunc    func(log *slog.Logger) PlatformAdapterInterface
    ExtractKeys func(conn any) (map[string]string, error)
}

func Register(pt PlatformType, builder AdapterBuilder) { ... }

Estimated Impact: 新增平台只需调用 Register,无需修改任何共享层代码

Acceptance Criteria:

  • ExtractPlatformKeys 逻辑移入各平台 adapter
  • 共享层通过 registry 查找 platform-specific extraction
  • make test 零回归

SOLID: ISP

sessionmanager-empty-interface

Severity: Medium | Confidence: High | ROI: High
Location: platform_adapter.go:157

Problem: type SessionManager any 是空接口 — 没有定义任何方法。它被存储在 PlatformAdapter.smAdapterConfig 中,但从未通过此类型调用任何方法。实际的 session 操作通过 SessionStarter 接口完成。这个空接口:

  • 增加 coupling weight(所有 adapter 构造需传入)
  • 提供 zero compile-time safety
  • 误导读者以为 session 操作通过此类型进行

Current Pattern:

// platform_adapter.go:157
type SessionManager any  // empty interface!

// platform_adapter.go:131 — stored but never used for method calls
type PlatformAdapter struct {
    sm SessionManager  // dead weight
    // ...
}

// config.go:10 — required in config
type AdapterConfig struct {
    SM SessionManager  // must be passed but serves no purpose
    // ...
}

Proposed Fix: 移除 SessionManager 类型和所有引用:

type PlatformAdapter struct {
    // sm SessionManager — REMOVED
    hub     HubInterface
    handler HandlerInterface
    bridge  BridgeInterface
    // ...
}

type AdapterConfig struct {
    // SM SessionManager — REMOVED
    Hub     HubInterface
    Handler HandlerInterface
    Bridge  *Bridge
    // ...
}

Estimated Impact: 消除 dead dependency,简化 adapter 构造

Acceptance Criteria:

  • SessionManager 类型定义移除
  • PlatformAdapter.sm 字段移除
  • AdapterConfig.SM 字段移除
  • 所有 adapter 构造调用不再传入 SM 参数
  • make test 零回归

Coupling

bidirectional-bridge-platformadapter-dependency

Severity: Medium | Confidence: High | ROI: Medium
Location: platform_adapter.go:132 + bridge.go:27,61-68

Problem: Bridge 和 PlatformAdapter 存在双向依赖:

  • PlatformAdapter 持有 *Bridge(直接引用)
  • Bridge 通过 atomic.Value 持有 PlatformAdapterInterface(延迟设置)
  • PlatformAdapter.ConfigureWith 接收 *Bridge 作为参数
  • Bridge.SetAdapter 设置 adapter 到 atomic.Value

初始化顺序是:创建 Adapter → ConfigureWith(Bridge) → Bridge.SetAdapter(Adapter)。如果顺序错误,会产生 nil pointer panic。这是通过约定而非类型系统保证的脆弱初始化。

Current Pattern:

// platform_adapter.go:169 — receives *Bridge
func (pa *PlatformAdapter) ConfigureWith(cfg AdapterConfig) {
    pa.bridge = cfg.Bridge  // concrete *Bridge
}

// bridge.go:61-68 — stores adapter later
func (b *Bridge) SetAdapter(adapter PlatformAdapterInterface) {
    b.adapter.Store(adapter)  // delayed, must be called after ConfigureWith
}

Proposed Fix: 配合 C1 修复(引入 BridgeInterface),将双向依赖改为单向:

// PlatformAdapter only knows about BridgeInterface
// Bridge knows about PlatformAdapterInterface (via atomic.Value)
// Both depend on abstractions, not concretions

Estimated Impact: 消除 fragile initialization order

Acceptance Criteria:

  • PlatformAdapter.bridge 类型改为 BridgeInterface
  • 初始化顺序由构造函数保证(而非调用约定)
  • make test 零回归

Implementation Priority

Finding Priority Effort Risk Impact
sessionmanager-empty-interface P1 Small Low 消除 dead dependency,简化构造
four-parallel-command-maps P1 Small Low ~30 行减少,泛型统一
platformadapter-concrete-bridge-dependency P2 Medium Medium 解锁 mock 测试
extractplatformkeys-switch-chain P2 Medium Low 新平台零改动共享层
bridge-platform-specific-envelopes P3 Small Low Bridge 完全平台无关
bidirectional-bridge-platformadapter P3 Medium Low 消除 fragile init
platform-adapter-go-mixed P3 Small Low 文件拆分,职责清晰

Recommended starting point: sessionmanager-empty-interface — 最小改动(删除 dead code),最高回报


Out of Scope

  • Slack/Feishu 子模块间 interaction/command 重复 — 已在 issue 257 跟踪
  • control_command.go parsing+help 混合 — Low ROI,help text 是自然附属
  • AdapterConfig.Extras untyped map — Low ROI,platform-specific 配置
  • interaction.go 不一致 extraction — Low ROI,3 个函数差异足够大

Verification

  • make test 通过,无回归
  • make lint 不产生新警告
  • go test -race ./internal/messaging/... 通过

Metadata

Metadata

Assignees

No one assigned

    Labels

    P3Medium: tech debt, refactoring, improvementsarchitectureDomain: design patterns, coupling, separation of concernsarea/messagingScope: Slack/Feishu adapters, platform adapterrefactorRefactor: DRY, SOLID, code quality improvements

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions