317 lines
7.4 KiB
Go
317 lines
7.4 KiB
Go
package stream
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
)
|
|
|
|
type contextKey struct{}
|
|
|
|
var streamerCtxKey = contextKey{}
|
|
|
|
// Listener 事件监听回调
|
|
type Listener func(StreamEvent)
|
|
|
|
// Streamer 流式事件发射器
|
|
type Streamer struct {
|
|
mu sync.RWMutex
|
|
listeners []Listener
|
|
includeDetails bool
|
|
eventCount int64
|
|
}
|
|
|
|
// NewStreamer 创建新的发射器
|
|
func NewStreamer(includeDetails bool, listeners ...Listener) *Streamer {
|
|
return &Streamer{
|
|
listeners: listeners,
|
|
includeDetails: includeDetails,
|
|
}
|
|
}
|
|
|
|
// AddListener 动态注册监听器
|
|
func (s *Streamer) AddListener(l Listener) {
|
|
if s == nil || l == nil {
|
|
return
|
|
}
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.listeners = append(s.listeners, l)
|
|
}
|
|
|
|
// Emit 发送一个流式事件
|
|
func (s *Streamer) Emit(ev StreamEvent) {
|
|
if s == nil {
|
|
return
|
|
}
|
|
// 详情按需过滤
|
|
if ev.Type == EventDetail && !s.includeDetails {
|
|
return
|
|
}
|
|
|
|
count := atomic.AddInt64(&s.eventCount, 1)
|
|
if ev.ID == "" {
|
|
ev.ID = fmt.Sprintf("evt_%d_%d", time.Now().UnixNano(), count)
|
|
}
|
|
if ev.Timestamp.IsZero() {
|
|
ev.Timestamp = time.Now()
|
|
}
|
|
|
|
s.mu.RLock()
|
|
listeners := make([]Listener, len(s.listeners))
|
|
copy(listeners, s.listeners)
|
|
s.mu.RUnlock()
|
|
|
|
for _, l := range listeners {
|
|
if l != nil {
|
|
l(ev)
|
|
}
|
|
}
|
|
}
|
|
|
|
// EmitPhase 发送阶段性任务状态跃迁事件
|
|
func (s *Streamer) EmitPhase(phaseID, message, role string, stepIndex, totalSteps, progress int) {
|
|
if s == nil {
|
|
return
|
|
}
|
|
s.Emit(StreamEvent{
|
|
Type: EventPhase,
|
|
Phase: &PhasePayload{
|
|
PhaseID: phaseID,
|
|
Message: message,
|
|
Role: role,
|
|
StepIndex: stepIndex,
|
|
TotalSteps: totalSteps,
|
|
Progress: progress,
|
|
},
|
|
})
|
|
}
|
|
|
|
// EmitStatus 发送意图与状态机事件
|
|
func (s *Streamer) EmitStatus(intent, branch, currentStage, message string, patches map[string]string) {
|
|
if s == nil {
|
|
return
|
|
}
|
|
s.Emit(StreamEvent{
|
|
Type: EventStatus,
|
|
Status: &StatusPayload{
|
|
Intent: intent,
|
|
Branch: branch,
|
|
CurrentStage: currentStage,
|
|
Patches: patches,
|
|
Message: message,
|
|
},
|
|
})
|
|
}
|
|
|
|
// EmitSummary 发送核心进展摘要事件 (低认知负荷)
|
|
func (s *Streamer) EmitSummary(title, content string, maturityScore int, maturityLevel string) {
|
|
if s == nil {
|
|
return
|
|
}
|
|
s.Emit(StreamEvent{
|
|
Type: EventSummary,
|
|
Summary: &SummaryPayload{
|
|
Title: title,
|
|
Content: content,
|
|
MaturityScore: maturityScore,
|
|
MaturityLevel: maturityLevel,
|
|
},
|
|
})
|
|
}
|
|
|
|
// EmitBriefing 发送 B 轨业务参谋决策锦囊
|
|
func (s *Streamer) EmitBriefing(brief any) {
|
|
if s == nil || brief == nil {
|
|
return
|
|
}
|
|
s.Emit(StreamEvent{
|
|
Type: EventBriefing,
|
|
Briefing: brief,
|
|
})
|
|
}
|
|
|
|
// EmitDetail 发送详细推演草稿或思考链 (按需折叠)
|
|
func (s *Streamer) EmitDetail(category, stepID, role, title, content string, collapsible bool) {
|
|
if s == nil {
|
|
return
|
|
}
|
|
s.Emit(StreamEvent{
|
|
Type: EventDetail,
|
|
Detail: &DetailPayload{
|
|
Category: category,
|
|
StepID: stepID,
|
|
Role: role,
|
|
Title: title,
|
|
Content: content,
|
|
Collapsible: collapsible,
|
|
},
|
|
})
|
|
}
|
|
|
|
// EmitDelta 发送对话自然语言流式文本
|
|
func (s *Streamer) EmitDelta(delta string) {
|
|
if s == nil || delta == "" {
|
|
return
|
|
}
|
|
s.Emit(StreamEvent{
|
|
Type: EventDelta,
|
|
Delta: delta,
|
|
})
|
|
}
|
|
|
|
// EmitDeliverable 发送 A 轨合规文档交付事件
|
|
func (s *Streamer) EmitDeliverable(taskKey, title, filePath string, wordCount int, score float64) {
|
|
if s == nil {
|
|
return
|
|
}
|
|
s.Emit(StreamEvent{
|
|
Type: EventDeliverable,
|
|
Deliverable: &DeliverablePayload{
|
|
TaskKey: taskKey,
|
|
Title: title,
|
|
FilePath: filePath,
|
|
WordCount: wordCount,
|
|
QualityScore: score,
|
|
},
|
|
})
|
|
}
|
|
|
|
// EmitGovernance 发送文档治理生命周期与认领变更拦截事件
|
|
func (s *Streamer) EmitGovernance(payload GovernancePayload) {
|
|
if s == nil {
|
|
return
|
|
}
|
|
s.Emit(StreamEvent{
|
|
Type: EventGovernance,
|
|
Governance: &payload,
|
|
})
|
|
}
|
|
|
|
// EmitErrorInfo 发送结构化错误警报
|
|
func (s *Streamer) EmitErrorInfo(info ErrorInfo) {
|
|
if s == nil {
|
|
return
|
|
}
|
|
cp := info
|
|
s.Emit(StreamEvent{
|
|
Type: EventError,
|
|
Error: info.UserMessage,
|
|
ErrorInfo: &cp,
|
|
})
|
|
}
|
|
|
|
// EmitError 发送错误警报(自动分类为结构化 ErrorInfo)
|
|
func (s *Streamer) EmitError(err error) {
|
|
if s == nil || err == nil {
|
|
return
|
|
}
|
|
s.EmitErrorInfo(ClassifyError(err, "", "", "", ""))
|
|
}
|
|
|
|
// EmitDone 发送流结束事件
|
|
func (s *Streamer) EmitDone(sessionID string, duration time.Duration) {
|
|
if s == nil {
|
|
return
|
|
}
|
|
s.Emit(StreamEvent{
|
|
Type: EventDone,
|
|
Done: &DonePayload{
|
|
SessionID: sessionID,
|
|
DurationMs: duration.Milliseconds(),
|
|
TotalEvents: int(atomic.LoadInt64(&s.eventCount)),
|
|
},
|
|
})
|
|
}
|
|
|
|
// EmitDoneWithWorkspace 发送流结束事件并携带任务工作区目录(P2:供前端「继续任务」续跑)
|
|
func (s *Streamer) EmitDoneWithWorkspace(sessionID, workspaceID string, duration time.Duration) {
|
|
if s == nil {
|
|
return
|
|
}
|
|
s.Emit(StreamEvent{
|
|
Type: EventDone,
|
|
Done: &DonePayload{
|
|
SessionID: sessionID,
|
|
WorkspaceID: workspaceID,
|
|
DurationMs: duration.Milliseconds(),
|
|
TotalEvents: int(atomic.LoadInt64(&s.eventCount)),
|
|
},
|
|
})
|
|
}
|
|
|
|
// EmitTurnAck 发送回合确认事件(用户消息落库后立即回传)
|
|
func (s *Streamer) EmitTurnAck(clientTurnID string, userMessageID int64, status string, duplicate bool) {
|
|
if s == nil {
|
|
return
|
|
}
|
|
s.Emit(StreamEvent{
|
|
Type: EventTurnAck,
|
|
Turn: &TurnAckPayload{
|
|
ClientTurnID: clientTurnID,
|
|
UserMessageID: userMessageID,
|
|
Status: status,
|
|
Duplicate: duplicate,
|
|
},
|
|
})
|
|
}
|
|
|
|
// EmitDoneWithExport 发送流结束事件并携带落库消息 ID 与权威导出能力
|
|
func (s *Streamer) EmitDoneWithExport(sessionID string, duration time.Duration, userMessageID, messageID int64, revision int, sha256 string, capability any) {
|
|
s.EmitDoneWithTurnExport(sessionID, "", duration, userMessageID, messageID, revision, sha256, capability)
|
|
}
|
|
|
|
// EmitDoneWithTurnExport 发送流结束事件并携带回合与用户/助手消息 ID
|
|
func (s *Streamer) EmitDoneWithTurnExport(sessionID, clientTurnID string, duration time.Duration, userMessageID, messageID int64, revision int, sha256 string, capability any) {
|
|
if s == nil {
|
|
return
|
|
}
|
|
s.Emit(StreamEvent{
|
|
Type: EventDone,
|
|
Done: &DonePayload{
|
|
SessionID: sessionID,
|
|
ClientTurnID: clientTurnID,
|
|
DurationMs: duration.Milliseconds(),
|
|
TotalEvents: int(atomic.LoadInt64(&s.eventCount)),
|
|
UserMessageID: userMessageID,
|
|
MessageID: messageID,
|
|
Status: "completed",
|
|
ContentRevision: revision,
|
|
ContentSha256: sha256,
|
|
ExportCapability: capability,
|
|
},
|
|
})
|
|
}
|
|
|
|
// EventCount 获取已发射事件总数
|
|
func (s *Streamer) EventCount() int {
|
|
if s == nil {
|
|
return 0
|
|
}
|
|
return int(atomic.LoadInt64(&s.eventCount))
|
|
}
|
|
|
|
// WithStreamer 将 Streamer 注入 Context
|
|
func WithStreamer(ctx context.Context, s *Streamer) context.Context {
|
|
return context.WithValue(ctx, streamerCtxKey, s)
|
|
}
|
|
|
|
// WithListener 便捷方法:基于 Listener 创建 Streamer 并注入 Context
|
|
func WithListener(ctx context.Context, includeDetails bool, l Listener) (context.Context, *Streamer) {
|
|
s := NewStreamer(includeDetails, l)
|
|
return WithStreamer(ctx, s), s
|
|
}
|
|
|
|
// FromContext 从 Context 提取 Streamer,若未设置则返回安全空对象,避免空指针崩溃
|
|
func FromContext(ctx context.Context) *Streamer {
|
|
if ctx == nil {
|
|
return nil
|
|
}
|
|
if s, ok := ctx.Value(streamerCtxKey).(*Streamer); ok && s != nil {
|
|
return s
|
|
}
|
|
return nil
|
|
}
|