Background
internal/messaging/feishu/ 是飞书 WebSocket 适配器 + STT,包含 ~10 个 .go 源文件(~3000 行)。模块负责飞书事件分发、消息格式化、流式卡片更新、交互(权限/Q&A)、以及 SDK 日志桥接。Related: issue 500(messaging 父包 SOLID/DRY)。
Scope: solid, dry, coupling — cycle 149 (模块分析通过 1)
Key files: streaming.go, conn.go, sdk_logger.go, handler.go
Finding Summary
| Category |
Critical |
High |
Medium |
Low |
| SOLID/SRP |
0 |
0 |
1 |
0 |
| DRY |
0 |
0 |
2 |
1 |
| Coupling |
0 |
0 |
1 |
0 |
| 合计 |
0 |
0 |
4 |
1 |
Findings
SOLID/SRP
streaming-card-controller-srp
Severity: Medium | Confidence: High | ROI: Medium
Location: streaming.go:76-130, streaming.go:603-711
Problem: StreamingCardController(1091 行)管理两个独立关注点:(1) 内容累积 + flush 调度(Write/Flush/flushLoop),和 (2) 卡片生命周期状态机(phase transitions, EnsureCard, enableStreaming, Close)。Close 方法单独 108 行跨越 final flush、integrity check、streaming disable、header update。20+ 字段混合生命周期状态(phase, cardID, streamingActive)与内容状态(buf, lastFlushed, bufRunes)与交付配置(client, limiter, chatType)。
Current Pattern:
type StreamingCardController struct {
phase atomic.Int32 // lifecycle state
cardID string // lifecycle state
buf strings.Builder // content buffer
lastFlushed string // content buffer
bufRunes int // content buffer
client *lark.Client // delivery config
limiter *FeishuRateLimiter // delivery config
// ... 12 more fields mixing concerns
}
Proposed Fix (低投入选项 — 拆分 Close):
func (c *StreamingCardController) Close(ctx context.Context) error {
c.stopFlushLoop()
c.MarkAllToolsDone()
content := c.finalContent()
c.finalFlush(ctx, content)
c.disableStreamingAndFinalize(ctx, content)
return nil
}
Acceptance Criteria:
DRY
sdk-logger-boilerplate-dry
Severity: Medium | Confidence: High | ROI: Medium
Location: sdk_logger.go:75-102
Problem: SlogLogger 的 Debug/Info/Warn/Error 4 个方法体完全相同:sdkLogFilter(redactURL(fmt.Sprint(args...))) → 空 check → log。唯一差异是 slog level 常量。如果 filter 或 redaction 逻辑变更,4 处必须同步修改。
Current Pattern:
func (s SlogLogger) Debug(_ context.Context, args ...any) {
msg := sdkLogFilter(redactURL(fmt.Sprint(args...)))
if msg == "" { return }
s.Logger.Log(context.Background(), slog.LevelDebug, msg)
}
func (s SlogLogger) Info(_ context.Context, args ...any) {
msg := sdkLogFilter(redactURL(fmt.Sprint(args...)))
if msg == "" { return }
s.Logger.Log(context.Background(), slog.LevelInfo, msg)
}
Proposed Fix:
func (s SlogLogger) logf(level slog.Level, args ...any) {
msg := sdkLogFilter(redactURL(fmt.Sprint(args...)))
if msg == "" { return }
s.Logger.Log(context.Background(), level, msg)
}
func (s SlogLogger) Debug(_ context.Context, args ...any) { s.logf(slog.LevelDebug, args...) }
func (s SlogLogger) Info(_ context.Context, args ...any) { s.logf(slog.LevelInfo, args...) }
func (s SlogLogger) Warn(_ context.Context, args ...any) { s.logf(slog.LevelWarn, args...) }
func (s SlogLogger) Error(_ context.Context, args ...any) { s.logf(slog.LevelError, args...) }
Acceptance Criteria:
content-pipeline-dry
Severity: Medium | Confidence: High | ROI: Medium
Location: conn.go:349, conn.go:542-544, streaming.go:628
Problem: 3 步内容管道 OptimizeMarkdownStyle(SanitizeForCard(SanitizeText(content))) 在 3+ 调用点重复。正确的顺序(sanitize → optimize)是隐含契约,每个调用者必须记住。如果管道增加步骤或顺序变更,所有调用点必须同步更新,遗漏可能导致未净化内容发送到飞书卡片。
Current Pattern:
// conn.go:349
return c.sendOrReply(ctx, OptimizeMarkdownStyle(SanitizeForCard(messaging.SanitizeText(content))))
// conn.go:542
return c.adapter.replyMessage(ctx, replyToMsgID, OptimizeMarkdownStyle(SanitizeForCard(text)), false)
// conn.go:544
return c.adapter.sendTextMessage(ctx, chatID, OptimizeMarkdownStyle(SanitizeForCard(text)))
Proposed Fix:
func prepareCardContent(text string) string {
return OptimizeMarkdownStyle(SanitizeForCard(messaging.SanitizeText(text)))
}
// Callers:
return c.sendOrReply(ctx, prepareCardContent(content))
return c.adapter.replyMessage(ctx, replyToMsgID, prepareCardContent(text), false)
Acceptance Criteria:
duplicate-stream-ctrl-accessor
Severity: Low | Confidence: High | ROI: High
Location: conn.go:99-108
Problem: FeishuConn 同时有 GetStreamCtrl()(exported)和 getStreamCtrl()(unexported)实现完全相同。handler.go 在同一包内调用 exported 版本,完全可以用 unexported 版本。
Current Pattern:
func (c *FeishuConn) GetStreamCtrl() *StreamingCardController {
c.mu.RLock()
defer c.mu.RUnlock()
return c.streamCtrl
}
func (c *FeishuConn) getStreamCtrl() *StreamingCardController {
c.mu.RLock()
defer c.mu.RUnlock()
return c.streamCtrl
}
Proposed Fix: 移除 GetStreamCtrl(),将 handler.go 调用改为 getStreamCtrl()。
Acceptance Criteria:
Coupling
feishu-conn-coupling-to-adapter
Severity: Medium | Confidence: Medium | ROI: Medium
Location: conn.go:17-37, conn.go:139-143, conn.go:502-510
Problem: FeishuConn 持有 adapter *Adapter 直接指针,深度访问 6+ 字段(larkClient, rateLimiter, Log, resolveBotName(), phrases)。创建双向依赖:Adapter 创建 FeishuConn,FeishuConn 反向访问 Adapter 内部。尤其体现在 resetStreamCtrl() 和 writeContent() 中构造新 StreamingCardController 时直接读取 adapter 字段。
Current Pattern:
// conn.go:502-510
newCtrl := NewStreamingCardController(
c.adapter.larkClient, c.adapter.rateLimiter, c.adapter.Log,
c.adapter.resolveBotName(), c.turnCount, c.lastBranch, c.workDir,
c.adapter.phrases,
)
Proposed Fix: 定义 connDeps 结构体或接口,Adapter 在构造时注入依赖而非暴露自身。
Acceptance Criteria:
Implementation Priority
| Finding |
Priority |
Effort |
Risk |
Impact |
| duplicate-stream-ctrl-accessor |
P0 |
Small |
Low |
2 行减少,消除混淆 |
| sdk-logger-boilerplate-dry |
P1 |
Small |
Low |
~15 行减少,统一日志路径 |
| content-pipeline-dry |
P1 |
Small |
Low |
消除隐含顺序契约,防止遗漏 |
| streaming-card-controller-srp |
P2 |
Medium |
Medium |
Close 可读性改善 |
| feishu-conn-coupling-to-adapter |
P3 |
Medium |
Medium |
解耦 Conn↔Adapter |
Recommended starting point: duplicate-stream-ctrl-accessor — 最小改动,消除明显的重复访问器
Out of Scope
- StreamingCardController flushLoop time.After 泄漏 — 实际使用 time.NewTicker,无泄漏
- permission keyword matching 提取到共享包 — Slack 使用 button-based,无跨适配器重复
- OptimizeMarkdownStyle regex pipeline 简化 — 6-pass pipeline 有清晰注释,各有用途
- sendPermissionRequest/sendQuestionRequest 卡片构建模式 — 各交互类型卡片结构差异大
Verification
Background
internal/messaging/feishu/是飞书 WebSocket 适配器 + STT,包含 ~10 个 .go 源文件(~3000 行)。模块负责飞书事件分发、消息格式化、流式卡片更新、交互(权限/Q&A)、以及 SDK 日志桥接。Related: issue 500(messaging 父包 SOLID/DRY)。Scope: solid, dry, coupling — cycle 149 (模块分析通过 1)
Key files:
streaming.go,conn.go,sdk_logger.go,handler.goFinding Summary
Findings
SOLID/SRP
streaming-card-controller-srp
Severity: Medium | Confidence: High | ROI: Medium
Location:
streaming.go:76-130,streaming.go:603-711Problem: StreamingCardController(1091 行)管理两个独立关注点:(1) 内容累积 + flush 调度(Write/Flush/flushLoop),和 (2) 卡片生命周期状态机(phase transitions, EnsureCard, enableStreaming, Close)。Close 方法单独 108 行跨越 final flush、integrity check、streaming disable、header update。20+ 字段混合生命周期状态(phase, cardID, streamingActive)与内容状态(buf, lastFlushed, bufRunes)与交付配置(client, limiter, chatType)。
Current Pattern:
Proposed Fix (低投入选项 — 拆分 Close):
Acceptance Criteria:
Close拆分为 3-4 个聚焦子方法(stopFlushLoop, finalFlush, disableStreamingAndFinalize)make test零回归DRY
sdk-logger-boilerplate-dry
Severity: Medium | Confidence: High | ROI: Medium
Location:
sdk_logger.go:75-102Problem: SlogLogger 的 Debug/Info/Warn/Error 4 个方法体完全相同:
sdkLogFilter(redactURL(fmt.Sprint(args...)))→ 空 check → log。唯一差异是 slog level 常量。如果 filter 或 redaction 逻辑变更,4 处必须同步修改。Current Pattern:
Proposed Fix:
Acceptance Criteria:
logf私有方法统一 4 个日志级别content-pipeline-dry
Severity: Medium | Confidence: High | ROI: Medium
Location:
conn.go:349,conn.go:542-544,streaming.go:628Problem: 3 步内容管道
OptimizeMarkdownStyle(SanitizeForCard(SanitizeText(content)))在 3+ 调用点重复。正确的顺序(sanitize → optimize)是隐含契约,每个调用者必须记住。如果管道增加步骤或顺序变更,所有调用点必须同步更新,遗漏可能导致未净化内容发送到飞书卡片。Current Pattern:
Proposed Fix:
Acceptance Criteria:
prepareCardContent函数封装完整 3 步管道OptimizeMarkdownStyle(SanitizeForCard(...))调用点替换TestPrepareCardContent验证管道顺序make test零回归duplicate-stream-ctrl-accessor
Severity: Low | Confidence: High | ROI: High
Location:
conn.go:99-108Problem: FeishuConn 同时有
GetStreamCtrl()(exported)和getStreamCtrl()(unexported)实现完全相同。handler.go 在同一包内调用 exported 版本,完全可以用 unexported 版本。Current Pattern:
Proposed Fix: 移除
GetStreamCtrl(),将 handler.go 调用改为getStreamCtrl()。Acceptance Criteria:
GetStreamCtrl()方法getStreamCtrl()替代GetStreamCtrl()(grep 确认)Coupling
feishu-conn-coupling-to-adapter
Severity: Medium | Confidence: Medium | ROI: Medium
Location:
conn.go:17-37,conn.go:139-143,conn.go:502-510Problem: FeishuConn 持有
adapter *Adapter直接指针,深度访问 6+ 字段(larkClient, rateLimiter, Log, resolveBotName(), phrases)。创建双向依赖:Adapter 创建 FeishuConn,FeishuConn 反向访问 Adapter 内部。尤其体现在 resetStreamCtrl() 和 writeContent() 中构造新 StreamingCardController 时直接读取 adapter 字段。Current Pattern:
Proposed Fix: 定义 connDeps 结构体或接口,Adapter 在构造时注入依赖而非暴露自身。
Acceptance Criteria:
*Adapterc.adapter.XXX字段make test零回归Implementation Priority
Recommended starting point: duplicate-stream-ctrl-accessor — 最小改动,消除明显的重复访问器
Out of Scope
Verification
make test通过,无回归go test -race ./internal/messaging/feishu/...通过