Background
cmd/hotplex/ 是 CLI 入口模块(~5000 LOC,53 文件),负责 Cobra 命令注册、Gateway DI 容器、路由注册和消息适配器初始化。模块已在 cycle 7-12 和 cycle 103 完成两轮分析(issues 212, 231, 245, 260)。本轮为 Phase 1 第三次分析,聚焦 messaging_init.go 和 bot_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: resolveSlackGate 和 resolveFeishuGate 是近完全相同的 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:
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: applyBotAttrsToSlack 和 applyBotAttrsToFeishu 是近完全相同的 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:
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:
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:
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
Background
cmd/hotplex/是 CLI 入口模块(~5000 LOC,53 文件),负责 Cobra 命令注册、Gateway DI 容器、路由注册和消息适配器初始化。模块已在 cycle 7-12 和 cycle 103 完成两轮分析(issues 212, 231, 245, 260)。本轮为 Phase 1 第三次分析,聚焦messaging_init.go和bot_config_adapter.go中的 Slack/Feishu 对称代码。Scope: solid, dry, coupling — cycle 160 (模块分析通过 8)
Key files:
messaging_init.go,bot_config_adapter.go,routes.goFinding Summary
Findings
DRY
slack-feishu-parallel-gate-resolvers
Severity: Medium | Confidence: High | ROI: High
Location:
messaging_init.go:509-538,messaging_init.go:542-571Problem:
resolveSlackGate和resolveFeishuGate是近完全相同的 30 行函数,提取 dm/group/mention/from 字段并调用messaging.NewGate()。唯一差异是输入类型(SlackConfig/SlackBotConfigvsFeishuConfig/FeishuBotConfig)。两函数合计 60 行执行相同的结构性工作。Current Pattern:
Proposed Fix:
Acceptance Criteria:
resolveSlackGate和resolveFeishuGate合并为单一resolveGate函数GateFields方法或接口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-575Problem:
applyBotAttrsToSlack和applyBotAttrsToFeishu是近完全相同的 43 行函数,将admin.BotConfigAttrs非零字段复制到平台特定的 bot 配置结构体。唯一差异是凭证字段(BotToken/AppTokenvsAppID/AppSecret)和目标结构体类型。86 行手写字段逐一拷贝。Current Pattern:
Proposed Fix:
Acceptance Criteria:
make test通过extract-bot-attrs-massive-switch-duplication
Severity: Medium | Confidence: High | ROI: Medium
Location:
bot_config_adapter.go:291-387Problem:
extractBotAttrs包含两个巨大的 switch 分支(slack: 297-339, feishu: 341-383),结构完全相同 — 先尝试 bot 级配置,再回退到平台级MessagingPlatformConfig。每个分支 ~40 行相同字段拷贝到admin.BotConfigAttrs。函数总计 96 行。Current Pattern:
Proposed Fix:
Acceptance Criteria:
AttrProvider接口extractBotAttrs函数从 96 行降至 ~20 行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:87Problem:
messaging.DefaultBotRegistry()从 3 个文件中的 7 个调用点直接访问全局单例,未通过GatewayDeps注入。这与项目 CLAUDE.md 中记录的手动 DI 政策("no wire/dig, all manual constructor injection")矛盾。测试无法注入 mock registry。Current Pattern:
Proposed Fix:
Acceptance Criteria:
BotRegistry添加到GatewayDeps并注入到botConfigAdapterbot_config_adapter.go和routes.go中所有DefaultBotRegistry()直接调用(7 处)messaging_init.go中的初始化调用保留(它是 registry 的创建点)botConfigAdapter使用注入的 registrymake test通过Implementation Priority
Recommended starting point: gate-resolvers 最简单(两个纯函数,无状态依赖),消除后为 attr-appliers 和 extract-bot-attrs 建立模式基础。
Out of Scope
Verification
make test通过,无回归make lint不产生新警告hotplex gateway start正常初始化多 bot 场景hotplex doctor通过所有诊断检查