feat(service): 建立多层级流式事件发射模型与Context穿透引擎
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
committed by
weichen
co-authored by
Cursor
parent
68d9c65317
commit
1bdea54022
@@ -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 提交入库。
|
||||
@@ -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` | **“当前在干嘛、进行到哪了”**<br>消除黑盒焦虑,建立确定性预期 | **高频刷新、单行即时动态指示**<br>例如:“正在组建智能体小队(法规专员、撰写专员、审核主管)...”<br>“法规专家正在进行法规筛选与标准研判...”<br>“审核主管正在进行质量门禁与引用核验...” |
|
||||
| **L2: 摘要与结论信息 (Summary & Briefing)** | `summary`, `briefing`, `delta` | **“核心发现了什么、该怎么决策”**<br>低认知负荷,快速掌握局面 | **精炼成文、突出核心结论**<br>• **摘要 (Summary)**:1-3 句话概括事实与核心研判<br>• **决策锦囊 (Briefing)**:B 轨成熟度等级、基准假设、风险雷达、选项 [1]/[2]、沟通话术<br>• **对话回答 (Delta)**:参谋自然语言解答流式逐句吐出 |
|
||||
| **L3: 底层执行与推演细节 (Detail / Raw Trace)** | `detail`, `deliverable` | **“具体推演依据、技术证据留底”**<br>满足专业审查,支持随时深挖 | **按需展开、默认折叠 (Collapsible)**<br>• 法规专员的细分研判草稿<br>• 审核专员的修改痕迹与门禁打分明细<br>• 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 思考过程与各专员步骤草稿收拢在 `<details><summary>查看专家推演与合规论证过程 (3 步)</summary>...</details>`;
|
||||
- 仅当用户主动点击时才展开,彻底避免初次打开时的信息过载。
|
||||
|
||||
### 6.2 CLI 终端界面
|
||||
- 默认开启流式进度模式,使用动态转轮或单行覆盖更新当前阶段(`⏳ [组建小队] ...`);
|
||||
- 完成后以清爽排版输出【核心摘要】与【决策锦囊】;
|
||||
- 增加 `--verbose` 开关,仅在用户显式指定时打印每步专员的完整中间推演。
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
Reference in New Issue
Block a user