diff --git a/docs/superpowers/plans/2026-09-08-streaming-events-and-digestible-ux.md b/docs/superpowers/plans/2026-09-08-streaming-events-and-digestible-ux.md new file mode 100644 index 00000000..b6d30c94 --- /dev/null +++ b/docs/superpowers/plans/2026-09-08-streaming-events-and-digestible-ux.md @@ -0,0 +1,111 @@ +# 流式传输服务与按需呈现交互实现计划 (Streaming Events & Digestible UX Implementation Plan) + +本文档制定“方向三:HTTP API 与 Server-Sent Events (SSE) 流式对话服务”的详细工程实施计划。重点落实阶段性任务状态更新、三层信息分级(状态、摘要、结论)与底层推演按需折叠机制。 + +## 一、 总体技术架构与流转路径 + +``` +[前端 Web / CLI 终端 / API 客户端] + │ + │ HTTP / POST /api/v1/chat/stream (SSE 长连接, include_details=false/true) + ▼ + ┌───────────────────────────────────────────────────────────────┐ + │ internal/server: HTTP RESTful API & SSE 流式长连接引擎 │ + └───────┬───────────────────────────────────────────────────────┘ + │ 携带 Streamer Context + ▼ + ┌───────────────────────────────────────────────────────────────┐ + │ internal/chat & coordinator: 协同对话层与意图流式识别 │ + │ • 发射 status: 意图识别分支、参数槽位、当前阶段 │ + └───────┬───────────────────────────────────────────────────────┘ + │ 任务执行 / 阶段跃迁推进 + ▼ + ┌───────────────────────────────────────────────────────────────┐ + │ internal/bizdev & squad: 智能体小队与知识检索调度流 │ + │ • 发射 phase: "squad_assembly" (组建小队) │ + │ • 发射 phase: "knowledge_retrieval" (法规专家筛选条款) │ + │ • 发射 phase: "step_executing" (撰写专员初稿中) │ + │ • 发射 phase: "quality_gate" (审核主管门禁把关中) │ + │ • 发射 detail: 步骤推演详细草稿 (标记 collapsible: true) │ + └───────┬───────────────────────────────────────────────────────┘ + │ 汇总交付与 B 轨提炼 + ▼ + ┌───────────────────────────────────────────────────────────────┐ + │ 结论提炼与分层交付 │ + │ • 发射 summary: 核心进展摘要 (1-3 句话低认知负荷) │ + │ • 发射 briefing: B 轨参谋决策锦囊 (评级、假设、风险、选项) │ + │ • 发射 deliverable: A 轨正式合规文档生成通知 │ + │ • 发射 done: 流正常结束 │ + └───────────────────────────────────────────────────────────────┘ +``` + +--- + +## 二、 任务拆分与渐进式提交规划 (Progressive Commits) + +### Task 1: 结构化流式事件体系与上下文穿透器 (internal/stream) (feat(service)) +- **目标**:建立统一的流式事件模型(`StreamEvent`)、事件类型(`phase`, `status`, `summary`, `briefing`, `detail`, `delta`, `deliverable`, `done`, `error`)与 Context 注入提取工具。 +- **涉及文件**: + - 新建: `internal/stream/types.go` + - 新建: `internal/stream/stream.go` + - 新建: `internal/stream/stream_test.go` +- **渐进提交**:`feat(service): 建立多层级流式事件发射模型与Context穿透引擎` + +### Task 2: 任务全流程节点流式事件插桩打点 (feat(agent)) +- **目标**:在智能体小队编排、知识库检索、专员单步执行、质量门禁把关及 B 轨参谋提炼中,实时发射阶段性状态(组建小队、法规筛选、初稿撰写、质量核验)及按需折叠的详细推演。 +- **涉及文件**: + - 修改: `internal/squad/models.go` / `workflow.go` / `service.go` + - 修改: `internal/bizdev/types.go` / `service.go` + - 修改: `internal/coordinator/engine.go` + - 修改: `internal/chat/session.go` +- **渐进提交**:`feat(agent): 在小队编排与任务生命周期中打通阶段性状态与摘要流式通知` + +### Task 3: HTTP RESTful API 服务与 SSE 流式端点实现 (feat(api)) +- **目标**:基于 Go 标准库 `net/http` 构建开发环境 8080 服务,实现 `/api/v1/health`、`/api/v1/projects/:id`、`/api/v1/chat` 及核心 `/api/v1/chat/stream`,支持 `include_details` 按需控制参数。 +- **涉及文件**: + - 新建: `internal/server/server.go` + - 新建: `internal/server/handlers.go` + - 新建: `internal/server/sse.go` + - 新建: `internal/server/server_test.go` + - 新建: `cmd/server/main.go` + - 修改: `internal/config/config.go` +- **渐进提交**:`feat(api): 实现HTTP服务骨架与Server-Sent Events流式交互接口` + +### Task 4: CLI 终端流式体验与分层交互升级 (feat(cli)) +- **目标**:升级 `cmd/bizdev-cli`,在控制台默认展现清爽的阶段性转轮/状态提示(如 `[组建小队] -> [法规筛选中] -> [撰写初稿中]`),收敛中间海量日志,并呈现清晰的【核心摘要】与【决策锦囊】,支持 `--verbose` 按需展开底层专员推演。 +- **涉及文件**: + - 修改: `cmd/bizdev-cli/main.go` +- **渐进提交**:`feat(cli): 终端支持动态阶段状态流式输出与低认知负荷摘要呈现` + +### Task 5: 全链路集成验证、真实 Agent 实测与测试报告归档 (test & docs) +- **目标**: + 1. 运行全量 `go test ./...` 确保单测与集成测试 100% 通过; + 2. 使用真实 `pi-agent` + `MiniMax-M3` 容器后端运行真实流式对话,取样并验证 SSE 流中的阶段状态、摘要、结论与按需折叠细节; + 3. 按照标准规范生成测试报告归档至 `.doc/test-reports/2026-09-08-streaming-api-and-digestible-ux-test-report.md`。 +- **涉及文件**: + - 新建: `internal/server/e2e_test.go` + - 新建: `.doc/test-reports/2026-09-08-streaming-api-and-digestible-ux-test-report.md` +- **渐进提交**: + - `test(api): 补充SSE流式长连接端到端自动化测试套件` + - `docs(test): 归档流式传输服务与按需呈现交互规范测试报告` + +--- + +## 三、 详细步骤与执行清单 + +- [ ] **Step 1**: 实现 `internal/stream` 核心包,支持事件订阅、Context 注入、过滤与多接收端分发; +- [ ] **Step 2**: 完善 `internal/stream/stream_test.go` 保证并发与边界测试通过; +- [ ] **Step 3**: 在 `squad.WorkflowDriver` 与 `Service` 中插桩: + - 发射 `phase: squad_assembly`(组建小队中) + - 发射 `phase: knowledge_retrieval`(法规专家正在筛选条款) + - 发射 `phase: step_executing`(单步角色、目标与进度百分比) + - 发射 `phase: quality_gate`(审核主管门禁校验中) + - 发射 `detail`(专员推演草稿,携带 `collapsible: true`) + - 发射 `summary` 与 `briefing`(B轨参谋锦囊) +- [ ] **Step 4**: 在 `internal/chat/session.go` 中提供 `SendMessageStream(ctx, input, listener)`; +- [ ] **Step 5**: 实现 `internal/server` HTTP 服务与 SSE 协议转换处理器; +- [ ] **Step 6**: 实现 `cmd/server/main.go`,绑定配置中端口(默认 8080); +- [ ] **Step 7**: 升级 `cmd/bizdev-cli/main.go` 提供动态流式终端交互; +- [ ] **Step 8**: 运行全量测试套件并执行真实 Agent SSE 端到端实测; +- [ ] **Step 9**: 按照 `.cursor/rules/test-reports-standard.mdc` 规范撰写并归档测试报告; +- [ ] **Step 10**: 执行阶段性 Git 提交入库。 diff --git a/docs/superpowers/specs/2026-09-08-streaming-events-and-digestible-ux-design.md b/docs/superpowers/specs/2026-09-08-streaming-events-and-digestible-ux-design.md new file mode 100644 index 00000000..20a4b1a5 --- /dev/null +++ b/docs/superpowers/specs/2026-09-08-streaming-events-and-digestible-ux-design.md @@ -0,0 +1,158 @@ +# 流式传输协议与按需呈现交互规范设计 (Streaming API & Digestible UX Design) + +## 一、 背景与业务痛点 (Background & Pain Points) + +在商务与技术协同推进体系中,智能体小队在真实 `pi-agent`(MiniMax-M3)驱动下,针对医疗器械开展需求接入(`intake`)、注册可行性研判(`feasibility_reg`)与初步方案设计(`prelim_design`)等任务,通常需要经历: +1. 法规标准知识库定向检索与条文初筛; +2. 跨学科专员角色(如法规专员、撰写专员、审核主管)自主多阶段推演; +3. 自动化质量门禁(QualityGate)合规核验与自我修复; +4. 交付物汇总与业务参谋决策锦囊提炼。 + +全流程在真实多智能体环境下执行耗时通常在数分钟(如 3~6 分钟)。当前系统面临以下关键用户体验与交互瓶颈: +1. **黑盒漫长等待与确定性预期缺失**:用户发出指令后,控制台或客户端无中间状态回传,用户不知道当前系统在检索法规、撰写初稿还是在做质量审核,极易产生“系统是否卡死挂起”的焦虑; +2. **信息洪流造成的认知过载 (Cognitive Overload)**:若将 Agent 的长文本思考链、工具调用明细、数百行技术草稿一股脑倾泻给用户,用户在极短时间内无法提取核心要点,产生强烈的认知疲劳; +3. **缺乏清晰的信息分层**:用户最关心的是“当前进度如何(状态)”、“达成了什么结论(摘要)”以及“后续该怎么走(参谋建议)”,而详细的技术报告与专员论证记录应作为可追溯的支撑材料“按需查看”,不应喧宾夺主。 + +--- + +## 二、 设计目标与核心原则 (Design Goals & Core Principles) + +### 2.1 核心目标 +1. **建立确定性心智预期**:通过毫秒级实时流式传输(Server-Sent Events),向用户清晰同步阶段性任务状态(如:正在组建小队、法规专家正在筛选条款、撰写专员正在生成初稿、审核主管正在进行门禁校验); +2. **分层按需呈现 (Digestible on Demand)**: + - 默认呈现:**状态信息 (Status)** + **核心摘要 (Summary)** + **参谋决策建议 (Briefing)**; + - 按需呈现:**详细推演过程 (Detail)** 默认折叠,用户可随时一键展开深入核查; +3. **标准化 RESTful HTTP API 与 SSE 对话服务**:基于 Go 标准库 `net/http` 构建企业级服务端,支持开发环境 `8080` 端口常驻; +4. **双轨交付与贴身参谋无缝融合**:流式推送不仅服务于对话回复(Token Delta),更原生承载 A 轨合规交付物与 B 轨业务参谋决策锦囊(成熟度、预设假设、风险雷达、决策选项、汇报话术)。 + +### 2.2 核心原则 +- **不打扰原则 (Non-Intrusive)**:不给用户输出过多的未经提炼的原始日志,保证用户在 10 秒内能看懂核心结论; +- **透明可追溯原则 (Transparent & Traceable)**:详细的思考与草稿不能丢失,全部打标为 `collapsible` 细节按需提供; +- **全链路非阻塞流转 (End-to-End Streaming)**:从 L1 意图层、L2 协调层到 L3 真实小队执行层,事件穿透并实时推送。 + +--- + +## 三、 信息架构三层分层模型 (Three-Tier Information Architecture) + +| 信息层级 | 对应流式事件 | 面向用户的心智价值 | 展现形态与认知成本 | +| :--- | :--- | :--- | :--- | +| **L1: 阶段性任务状态 (Phase / Status)** | `phase`, `status` | **“当前在干嘛、进行到哪了”**
消除黑盒焦虑,建立确定性预期 | **高频刷新、单行即时动态指示**
例如:“正在组建智能体小队(法规专员、撰写专员、审核主管)...”
“法规专家正在进行法规筛选与标准研判...”
“审核主管正在进行质量门禁与引用核验...” | +| **L2: 摘要与结论信息 (Summary & Briefing)** | `summary`, `briefing`, `delta` | **“核心发现了什么、该怎么决策”**
低认知负荷,快速掌握局面 | **精炼成文、突出核心结论**
• **摘要 (Summary)**:1-3 句话概括事实与核心研判
• **决策锦囊 (Briefing)**:B 轨成熟度等级、基准假设、风险雷达、选项 [1]/[2]、沟通话术
• **对话回答 (Delta)**:参谋自然语言解答流式逐句吐出 | +| **L3: 底层执行与推演细节 (Detail / Raw Trace)** | `detail`, `deliverable` | **“具体推演依据、技术证据留底”**
满足专业审查,支持随时深挖 | **按需展开、默认折叠 (Collapsible)**
• 法规专员的细分研判草稿
• 审核专员的修改痕迹与门禁打分明细
• A 轨长达数百行的正式 Word/Markdown 报告文档路径与下载 | + +--- + +## 四、 协议与事件规范 (SSE Protocol Specification) + +### 4.1 SSE 通信协议 +- 媒体类型:`Content-Type: text/event-stream` +- 缓存控制:`Cache-Control: no-cache` +- 连接机制:`Connection: keep-alive` +- 字符编码:`UTF-8` + +### 4.2 事件类型定义 + +```go +type EventType string + +const ( + EventPhase EventType = "phase" // 阶段性状态跃迁(组建小队、法规筛选、初稿撰写、门禁审核) + EventStatus EventType = "status" // 意图识别结果、成熟度状态机更新 + EventSummary EventType = "summary" // 提炼的核心进展摘要(低认知负荷) + EventBriefing EventType = "briefing" // 贴身业务参谋决策锦囊(B轨赋能) + EventDelta EventType = "delta" // 自然语言逐字/逐句流式吐出 + EventDetail EventType = "detail" // 详细推演草稿或思考链(按需显示,默认折叠) + EventDeliverable EventType = "deliverable" // A轨正式合规文档生成通知 + EventError EventType = "error" // 运行异常警报 + EventDone EventType = "done" // 会话流完成 +) +``` + +### 4.3 典型数据载荷示例 + +#### 1. 阶段状态事件 (`event: phase`) +```json +{ + "phase_id": "knowledge_retrieval", + "message": "法规专家正在检索权威法规知识库并筛选适用条款(匹配 GB 9706.1-2020、YY 0505)...", + "role": "regulatory", + "step_index": 1, + "total_steps": 3, + "progress": 25, + "timestamp": "2026-09-08T09:15:00Z" +} +``` + +#### 2. 详细过程事件 (`event: detail`,按需折叠) +```json +{ + "category": "step_output", + "step_id": "tasks/01_regulatory", + "title": "法规专员研判纪要", + "content": "### 01 法规专员研判纪要\n依据 2024 年分类界定结果第(十二)条,该设备结构归属于 06-15,建议管理类别判定为第二类医疗器械...", + "collapsible": true +} +``` + +#### 3. 核心摘要事件 (`event: summary`) +```json +{ + "title": "方案成型阶段进展摘要", + "content": "已完成医用电子内窥镜图像处理系统的初步架构与法规路径分析,推荐按 II 类注册(编码 06-15)走同品种比对主线。", + "maturity_score": 80 +} +``` + +#### 4. 业务参谋决策锦囊 (`event: briefing`) +```json +{ + "advisor_brief": { + "maturity_score": 80, + "maturity_level": "V0.5", + "maturity_headline": "「医用电子内窥镜图像处理系统」的设计方案框架与法规路径已成型,已具备进行详细 WBS 拆解与报价的条件", + "baseline_assumptions": [ + "系统架构采用模块化分层设计,软硬件接口保持独立兼容", + "电气安全与电磁兼容暂按 GB 9706.1-2020 / YY 0505 标准预留检测整改预算" + ], + "preemptive_risks": [ + "关键元器件交付周期可能受供应链波动影响,建议首期备料周期预留 4 周冗余" + ], + "next_best_actions": [ + "[1] 审阅初步系统架构图与法规分类论证说明", + "[2] 进入 V1.0 商业锁定阶段,生成明细 CDO 报价单与 Excel 核算模型" + ], + "communication_kit": "您好!关于「医用电子内窥镜图像处理系统」,我们已完成了技术路线比选与法规路径分析..." + } +} +``` + +--- + +## 五、 HTTP RESTful API 端点清单 + +| 端点 | 方法 | 说明 | 关键参数 | +| :--- | :--- | :--- | :--- | +| `/api/v1/health` | GET | 服务健康检查与基础设施状态(包含知识库数据库连接) | 无 | +| `/api/v1/projects/:id` | GET | 查询项目当前上下文、黑板事实、挂起预期与参谋简报 | `id` | +| `/api/v1/chat` | POST | 基础非流式对话交互(返回完整 JSON) | `session_id`, `message`, `user_id` | +| `/api/v1/chat/stream` | POST/GET | **核心 SSE 流式对话长连接服务** | `session_id`, `message`, `include_details` (bool, 默认 false) | +| `/api/v1/tasks/run` | POST | 独立触发执行指定任务或成熟度阶段 | `task_key`, `project_name`, `device_type`, `auto_repair` | +| `/api/v1/tasks/run/stream`| POST/GET | **任务执行全流程 SSE 监控流** | 任务参数 + `include_details` | + +--- + +## 六、 客户端按需呈现策略 (Client Presentation Strategy) + +### 6.1 Web / UI 界面 +- **主对话视窗**: + - 顶部/卡片内渲染动态进度条与当前阶段(如 `[正在组建小队] -> [法规筛选中] -> [撰写初稿中] -> [门禁校验中]`); + - 参谋自然语言回复逐句流畅显示; + - 底部浮现高亮【💡 参谋决策锦囊】卡片,包含成熟度徽章、掌舵选项快捷按钮; +- **抽屉与折叠区**: + - 中间的 Agent 思考过程与各专员步骤草稿收拢在 `
查看专家推演与合规论证过程 (3 步)...
`; + - 仅当用户主动点击时才展开,彻底避免初次打开时的信息过载。 + +### 6.2 CLI 终端界面 +- 默认开启流式进度模式,使用动态转轮或单行覆盖更新当前阶段(`⏳ [组建小队] ...`); +- 完成后以清爽排版输出【核心摘要】与【决策锦囊】; +- 增加 `--verbose` 开关,仅在用户显式指定时打印每步专员的完整中间推演。 diff --git a/internal/stream/stream.go b/internal/stream/stream.go new file mode 100644 index 00000000..f193c175 --- /dev/null +++ b/internal/stream/stream.go @@ -0,0 +1,238 @@ +package stream + +import ( + "context" + "fmt" + "sync" + "sync/atomic" + "time" + + "chat-agent-system/internal/squad" +) + +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 *squad.AdvisorBriefing) { + 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, + }, + }) +} + +// EmitError 发送错误警报 +func (s *Streamer) EmitError(err error) { + if s == nil || err == nil { + return + } + s.Emit(StreamEvent{ + Type: EventError, + Error: err.Error(), + }) +} + +// 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)), + }, + }) +} + +// 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 +} diff --git a/internal/stream/stream_test.go b/internal/stream/stream_test.go new file mode 100644 index 00000000..ab117062 --- /dev/null +++ b/internal/stream/stream_test.go @@ -0,0 +1,142 @@ +package stream + +import ( + "context" + "errors" + "sync" + "testing" + "time" + + "chat-agent-system/internal/squad" +) + +func TestStreamer_BasicEvents(t *testing.T) { + var received []StreamEvent + var mu sync.Mutex + + listener := func(ev StreamEvent) { + mu.Lock() + defer mu.Unlock() + received = append(received, ev) + } + + streamer := NewStreamer(true, listener) + + streamer.EmitPhase("squad_assembly", "正在组建小队...", "leader", 1, 3, 33) + streamer.EmitStatus("ADVANCE_STAGE", "BranchExpectationHit", "V0.5_feasibility", "推进阶段", map[string]string{"market": "CE"}) + streamer.EmitSummary("初审结论", "产品符合第二类医疗器械判定", 80, "V0.5") + streamer.EmitBriefing(&squad.AdvisorBriefing{MaturityScore: 80, MaturityLevel: "V0.5"}) + streamer.EmitDetail("step_output", "tasks/01_regulatory", "regulatory", "条款梳理", "GB 9706.1 适用", true) + streamer.EmitDelta("您好,已为您完成推进。") + streamer.EmitDeliverable("feasibility_reg", "注册可行性评估报告", "sandbox/output.md", 2500, 100.0) + streamer.EmitError(errors.New("模拟告警")) + streamer.EmitDone("sess_123", 500*time.Millisecond) + + mu.Lock() + count := len(received) + mu.Unlock() + + if count != 9 { + t.Fatalf("expected 9 events, got %d", count) + } + + // 校验事件类型 + expectedTypes := []EventType{ + EventPhase, EventStatus, EventSummary, EventBriefing, + EventDetail, EventDelta, EventDeliverable, EventError, EventDone, + } + + for i, exp := range expectedTypes { + if received[i].Type != exp { + t.Errorf("event %d: expected type %s, got %s", i, exp, received[i].Type) + } + } +} + +func TestStreamer_FilterDetails(t *testing.T) { + var received []StreamEvent + + listener := func(ev StreamEvent) { + received = append(received, ev) + } + + // includeDetails = false: 必须过滤 Detail 事件 + streamer := NewStreamer(false, listener) + + streamer.EmitPhase("step_executing", "法规专家研判中...", "regulatory", 1, 2, 50) + streamer.EmitDetail("step_output", "tasks/01", "regulatory", "庞大推演草稿", "长篇草稿内容...", true) + streamer.EmitSummary("精炼结论", "完成研判", 85, "V0.5") + + if len(received) != 2 { + t.Fatalf("expected 2 events (Detail filtered out), got %d", len(received)) + } + + if received[0].Type != EventPhase || received[1].Type != EventSummary { + t.Errorf("unexpected event types: %v, %v", received[0].Type, received[1].Type) + } +} + +func TestStreamer_ContextPropagation(t *testing.T) { + ctx := context.Background() + + // 1. 未注入时 FromContext 返回 nil,测试 nil-safe 调用 + nilStreamer := FromContext(ctx) + if nilStreamer != nil { + t.Fatalf("expected nil streamer from empty context") + } + // nil 调用不崩溃 + nilStreamer.EmitPhase("test", "msg", "role", 1, 1, 100) + nilStreamer.EmitDelta("test") + + // 2. 注入后正常提取 + var events []StreamEvent + ctxWithS, s := WithListener(ctx, true, func(ev StreamEvent) { + events = append(events, ev) + }) + + extracted := FromContext(ctxWithS) + if extracted != s { + t.Fatalf("extracted streamer did not match original") + } + + extracted.EmitPhase("squad_assembly", "组建小队", "leader", 1, 3, 33) + if len(events) != 1 { + t.Fatalf("expected 1 event, got %d", len(events)) + } +} + +func TestStreamer_Concurrency(t *testing.T) { + var count int64 + var mu sync.Mutex + + s := NewStreamer(true, func(ev StreamEvent) { + mu.Lock() + count++ + mu.Unlock() + }) + + var wg sync.WaitGroup + workers := 10 + iterations := 50 + + for i := 0; i < workers; i++ { + wg.Add(1) + go func(wID int) { + defer wg.Done() + for j := 0; j < iterations; j++ { + s.EmitPhase("running", "executing step", "worker", j, iterations, 50) + } + }(i) + } + + wg.Wait() + + mu.Lock() + total := count + mu.Unlock() + + expected := int64(workers * iterations) + if total != expected { + t.Fatalf("expected %d events, got %d", expected, total) + } +} diff --git a/internal/stream/types.go b/internal/stream/types.go new file mode 100644 index 00000000..43788a15 --- /dev/null +++ b/internal/stream/types.go @@ -0,0 +1,92 @@ +package stream + +import ( + "time" + + "chat-agent-system/internal/squad" +) + +// EventType 定义流式事件类型 +type EventType string + +const ( + EventPhase EventType = "phase" // 阶段性状态跃迁(组建小队、法规筛选、初稿撰写、门禁审核) + EventStatus EventType = "status" // 意图识别结果、成熟度状态机更新 + EventSummary EventType = "summary" // 提炼的核心进展摘要(低认知负荷) + EventBriefing EventType = "briefing" // 贴身业务参谋决策锦囊(B轨赋能) + EventDelta EventType = "delta" // 对话自然语言逐字/逐句流式吐出 + EventDetail EventType = "detail" // 详细推演草稿或思考链(按需显示,默认折叠) + EventDeliverable EventType = "deliverable" // A轨正式合规文档生成通知 + EventError EventType = "error" // 运行异常警报 + EventDone EventType = "done" // 会话流完成 +) + +// PhasePayload 阶段性任务状态更新载荷 +type PhasePayload struct { + PhaseID string `json:"phase_id"` // 如 squad_assembly, knowledge_retrieval, step_executing, quality_gate + Message string `json:"message"` // 用户友好的自然语言进度说明 + Role string `json:"role,omitempty"` // 角色 (如 regulatory, writer, leader) + StepIndex int `json:"step_index,omitempty"` // 当前步骤索引 (如 1) + TotalSteps int `json:"total_steps,omitempty"` // 总步骤数 (如 3) + Progress int `json:"progress,omitempty"` // 百分比估算 (0-100) +} + +// StatusPayload 意图与状态机更新载荷 +type StatusPayload struct { + Intent string `json:"intent,omitempty"` + Branch string `json:"branch,omitempty"` + CurrentStage string `json:"current_stage,omitempty"` + Patches map[string]string `json:"patches,omitempty"` + Message string `json:"message,omitempty"` +} + +// SummaryPayload 核心进展摘要载荷 (低认知负荷) +type SummaryPayload struct { + Title string `json:"title"` + Content string `json:"content"` + MaturityScore int `json:"maturity_score,omitempty"` + MaturityLevel string `json:"maturity_level,omitempty"` +} + +// DetailPayload 详细执行与推演过程载荷 (按需显示,默认折叠) +type DetailPayload struct { + Category string `json:"category"` // agent_thought, step_output, tool_call, log + StepID string `json:"step_id,omitempty"` // 步骤标识 (如 tasks/01_regulatory) + Role string `json:"role,omitempty"` // 对应角色 + Title string `json:"title"` // 详情标题 + Content string `json:"content"` // 完整推演正文或技术草稿 + Collapsible bool `json:"collapsible"` // 客户端是否应当默认折叠显示 +} + +// DeliverablePayload A轨正式合规文档交付载荷 +type DeliverablePayload struct { + TaskKey string `json:"task_key"` + Title string `json:"title"` + FilePath string `json:"file_path"` + WordCount int `json:"word_count,omitempty"` + QualityScore float64 `json:"quality_score,omitempty"` +} + +// DonePayload 流结束载荷 +type DonePayload struct { + SessionID string `json:"session_id,omitempty"` + DurationMs int64 `json:"duration_ms"` + TotalEvents int `json:"total_events"` +} + +// StreamEvent 统一多层级流式事件 +type StreamEvent struct { + ID string `json:"id"` + Type EventType `json:"type"` + Timestamp time.Time `json:"timestamp"` + + Phase *PhasePayload `json:"phase,omitempty"` + Status *StatusPayload `json:"status,omitempty"` + Summary *SummaryPayload `json:"summary,omitempty"` + Briefing *squad.AdvisorBriefing `json:"briefing,omitempty"` + Detail *DetailPayload `json:"detail,omitempty"` + Delta string `json:"delta,omitempty"` + Deliverable *DeliverablePayload `json:"deliverable,omitempty"` + Error string `json:"error,omitempty"` + Done *DonePayload `json:"done,omitempty"` +}