Skip to content

refactor(cmd/hotplex): Slack/Feishu parallel function DRY + BotRegistry DIP #519

@hrygo

Description

@hrygo

Background

cmd/hotplex/ 是 CLI 入口模块(~5000 LOC,53 文件),负责 Cobra 命令注册、Gateway DI 容器、路由注册和消息适配器初始化。模块已在 cycle 7-12 和 cycle 103 完成两轮分析(issues 212, 231, 245, 260)。本轮为 Phase 1 第三次分析,聚焦 messaging_init.gobot_config_adapter.go 中的 Slack/Feishu 对称代码。

Scope: solid, dry, coupling — cycle 160 (模块分析通过 8)
Key files: messaging_init.go, bot_config_adapter.go, routes.go


Finding Summary

Category Critical High Medium Low
DRY 0 0 3 0
SOLID 0 0 1 0
合计 0 0 4 0

Findings

DRY

slack-feishu-parallel-gate-resolvers

Severity: Medium | Confidence: High | ROI: High
Location: messaging_init.go:509-538, messaging_init.go:542-571

Problem: resolveSlackGateresolveFeishuGate 是近完全相同的 30 行函数,提取 dm/group/mention/from 字段并调用 messaging.NewGate()。唯一差异是输入类型(SlackConfig/SlackBotConfig vs FeishuConfig/FeishuBotConfig)。两函数合计 60 行执行相同的结构性工作。

Current Pattern:

func resolveSlackGate(platformCfg config.SlackConfig, botCfg *config.SlackBotConfig) *messaging.Gate {
	dm := platformCfg.DMPolicy
	group := platformCfg.GroupPolicy
	mention := platformCfg.RequireMention
	from := platformCfg.AllowFrom
	// ... 8 more lines of field extraction ...
	if botCfg != nil {
		if botCfg.DMPolicy != "" { dm = botCfg.DMPolicy }
		// ... 8 more lines of override ...
	}
	return messaging.NewGate(dm, group, mention, from, dmFrom, groupFrom)
}
// resolveFeishuGate: exact same body, different types

Proposed Fix:

type GateFields struct {
	DMPolicy, GroupPolicy string
	RequireMention        bool
	AllowFrom, AllowDMFrom, AllowGroupFrom []string
}
func resolveGate(platform, botCfg GateFields) *messaging.Gate {
	return messaging.NewGate(platform.DMPolicy, /* ... */)
}
// SlackBotConfig/FeishuBotConfig expose GateFields via a method.

Acceptance Criteria:

  • resolveSlackGateresolveFeishuGate 合并为单一 resolveGate 函数
  • Slack/Feishu 配置类型暴露 GateFields 方法或接口
  • 新增 Gate 字段只需修改一处
  • make test 通过

slack-feishu-parallel-attr-appliers

Severity: Medium | Confidence: High | ROI: Medium
Location: bot_config_adapter.go:486-529, bot_config_adapter.go:532-575

Problem: applyBotAttrsToSlackapplyBotAttrsToFeishu 是近完全相同的 43 行函数,将 admin.BotConfigAttrs 非零字段复制到平台特定的 bot 配置结构体。唯一差异是凭证字段(BotToken/AppToken vs AppID/AppSecret)和目标结构体类型。86 行手写字段逐一拷贝。

Current Pattern:

func applyBotAttrsToSlack(bot *config.SlackBotConfig, attrs *admin.BotConfigAttrs) {
	if attrs.WorkerType != "" { bot.WorkerType = attrs.WorkerType }
	if attrs.WorkDir != "" { bot.WorkDir = attrs.WorkDir }
	if attrs.DMPolicy != "" { bot.DMPolicy = attrs.DMPolicy }
	// ... 12 more identical field checks ...
	// Only these differ:
	if attrs.BotToken != "" { bot.BotToken = attrs.BotToken }
	if attrs.AppToken != "" { bot.AppToken = attrs.AppToken }
}
// applyBotAttrsToFeishu: same body, different credential fields

Proposed Fix:

func applyCommonAttrs(target CommonBotFields, attrs *admin.BotConfigAttrs) {
	if attrs.WorkerType != "" { target.SetWorkerType(attrs.WorkerType) }
	// ... shared field mapping ...
}
// Platform-specific functions shrink to credential handling only (~5 lines each).

Acceptance Criteria:

  • 通用字段映射提取为共享函数
  • 平台特定函数仅处理凭证字段(~5 行)
  • 新增 bot 级配置字段只需修改一处
  • make test 通过

extract-bot-attrs-massive-switch-duplication

Severity: Medium | Confidence: High | ROI: Medium
Location: bot_config_adapter.go:291-387

Problem: extractBotAttrs 包含两个巨大的 switch 分支(slack: 297-339, feishu: 341-383),结构完全相同 — 先尝试 bot 级配置,再回退到平台级 MessagingPlatformConfig。每个分支 ~40 行相同字段拷贝到 admin.BotConfigAttrs。函数总计 96 行。

Current Pattern:

func extractBotAttrs(cfg *config.Config, platform, name string) *admin.BotConfigAttrs {
	attrs := &admin.BotConfigAttrs{}
	switch platform {
	case "slack":
		bot := resolveSlackBot(cfg, name)
		if bot != nil {
			attrs.WorkerType = bot.WorkerType
			// ... 14 more identical field copies ...
		} else {
			sc := &cfg.Messaging.Slack.MessagingPlatformConfig
			attrs.WorkerType = sc.WorkerType
			// ... same 14 fields from platform fallback ...
		}
	case "feishu":
		// ... exact same structure with different type names ...
	}
	return attrs
}

Proposed Fix:

type AttrProvider interface {
	CommonFields() (workerType, workDir string, ...)
	STTAttrs() *admin.STTAttrs
	TTSAttrs() *admin.TTSAttrs
}
func extractBotAttrs(cfg *config.Config, platform, name string) *admin.BotConfigAttrs {
	provider := resolveProvider(cfg, platform, name) // returns AttrProvider
	return provider.ToAttrs()
}

Acceptance Criteria:

  • Slack 和 Feishu 分支的字段映射提取为共享 AttrProvider 接口
  • extractBotAttrs 函数从 96 行降至 ~20 行
  • 新增 bot 属性只需修改 AttrProvider 实现
  • make test 通过

SOLID

global-bot-registry-access-bypasses-di

Severity: Medium | Confidence: High | ROI: Medium
Aspect: solid (DIP)
Location: bot_config_adapter.go:37,63,160,233,282, messaging_init.go:67, routes.go:87

Problem: messaging.DefaultBotRegistry() 从 3 个文件中的 7 个调用点直接访问全局单例,未通过 GatewayDeps 注入。这与项目 CLAUDE.md 中记录的手动 DI 政策("no wire/dig, all manual constructor injection")矛盾。测试无法注入 mock registry。

Current Pattern:

// bot_config_adapter.go - reads global each time:
func (a *botConfigAdapter) GetBotConfig(...) {
	registry := messaging.DefaultBotRegistry()  // global singleton
	entry, ok := registry.GetByName(name)
	// ...
}
func (a *botConfigAdapter) CreateBot(...) {
	registry := messaging.DefaultBotRegistry()  // called again
	// ...
}

Proposed Fix:

type GatewayDeps struct {
	// ... existing fields ...
	BotRegistry *messaging.BotRegistry
}
type botConfigAdapter struct {
	cfgStore   *config.ConfigStore
	registry   *messaging.BotRegistry  // injected, not global
}
func newBotConfigAdapter(cfgStore *config.ConfigStore, dir, path string, registry *messaging.BotRegistry) *botConfigAdapter {
	return &botConfigAdapter{cfgStore: cfgStore, registry: registry, ...}
}

Acceptance Criteria:

  • BotRegistry 添加到 GatewayDeps 并注入到 botConfigAdapter
  • 移除 bot_config_adapter.goroutes.go 中所有 DefaultBotRegistry() 直接调用(7 处)
  • messaging_init.go 中的初始化调用保留(它是 registry 的创建点)
  • 添加测试验证 botConfigAdapter 使用注入的 registry
  • make test 通过

Implementation Priority

Finding Priority Effort Risk Impact
slack-feishu-parallel-gate-resolvers P1 Small Low ~60 行 → ~30 行,新平台字段单点修改
slack-feishu-parallel-attr-appliers P1 Medium Low ~86 行 → ~45 行,字段映射统一
extract-bot-attrs-massive-switch-duplication P2 Medium Low 96 行 → ~20 行,新增属性单点修改
global-bot-registry-access-bypasses-di P2 Medium Medium 7 处全局调用改为注入,启用测试

Recommended starting point: gate-resolvers 最简单(两个纯函数,无状态依赖),消除后为 attr-appliers 和 extract-bot-attrs 建立模式基础。


Out of Scope

  • config.Load() 重复调用(security.go/status.go)— 已被 issue 260 覆盖
  • cron_update.go applyFlags 冗长 — cobra flag API 固有限制
  • runGateway 函数长度 — DI composition root 预期形状
  • Gateway 平台 switch (OCP) — 仅 2 个平台,过早抽象

Verification

  • make test 通过,无回归
  • make lint 不产生新警告
  • hotplex gateway start 正常初始化多 bot 场景
  • hotplex doctor 通过所有诊断检查

Metadata

Metadata

Assignees

No one assigned

    Labels

    P3Medium: tech debt, refactoring, improvementsarchitectureDomain: design patterns, coupling, separation of concernsarea/cliScope: cobra commands, service management, updaterefactorRefactor: DRY, SOLID, code quality improvements

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions