docs(sse): 前端/后端 SSE 契约反向对齐 sse.ts 真实实现
studio src/lib/sse.ts 已是真实实现(connectAIStream/connectEventStream 双函数 +
fetch/ReadableStream + 按 event: 行分发),但三处正式文档仍停留在虚构契约,故反向对齐(反假绿):
- 前端-01 v6→v7:AI 流改两步(POST /ai/tasks 创建→GET /ai/tasks/{taskId}/stream 建流);
事件流路径 /events/stream→/events;明确按 SSE event: 行分发。
- 后端-05 v8→v9:补 GET /ai/tasks/{taskId}/stream 端点 + SSE 事件契约表
(chunk/quality_check/done/error 及各 payload);记 done 的 taskId/suggestionId
后端 Long 序列化为 JSON 数字、与候选 uuid 字符串契约不一致(待后端统一,前端已在解析边界 String 归一)。
- dev-baseline/muse-studio/CLAUDE.md:SSE 章节由虚构 useAIStream/useEventStream hook
改写为真实双函数 + 线格式 + 约束(AI 流不重连、事件流指数退避 1/2/5/10s、AbortController 关闭、无凭证 fail-closed)。
- sse.ts:onDone 注释补 WHY string(Long→JSON number→String 归一)。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9634842f2f
commit
cb57b88866
@ -1,7 +1,7 @@
|
||||
# 前端-01:工程结构与核心依赖
|
||||
|
||||
- 版本:v6
|
||||
- 更新日期:2026-05-24
|
||||
- 版本:v7
|
||||
- 更新日期:2026-06-14
|
||||
- 目标读者:前端 / 架构 / 产品 / 后端
|
||||
- 阅读时间:25-40 分钟
|
||||
- 边界说明:本文件只定义阶段 7 的前端工程基线、管理后台与用户端拆分、路由组织、状态分层和接口边界。写作台交互看 `前端-02`,元引擎看 `前端-03`,精确 API 看 `后端-05`。
|
||||
@ -192,15 +192,16 @@ muse-studio/
|
||||
|
||||
### 6.1 AI 生成 streaming 端点
|
||||
|
||||
- 路径:`POST /app-api/muse/ai/generate/stream`
|
||||
- 生命周期:per-request,请求发起时建立 SSE 连接,生成完成后连接关闭。
|
||||
- 端点:分两步——先 `POST /app-api/muse/ai/tasks` 创建任务拿到 `taskId`,再 `GET /app-api/muse/ai/tasks/{taskId}/stream` 建立 SSE 流(生成提示词等参数走创建任务的请求体,不进 URL)。
|
||||
- 生命周期:per-request,建流后流式推送,生成完成或失败后连接关闭。
|
||||
- 用途:AI 正文生成、候选生成等需要流式输出的场景。
|
||||
- 结束信号:服务端发送 `event: done`,payload 包含候选 ID。前端收到后用候选 ID 调用 REST 接口(`GET /app-api/muse/suggestions/{suggestionId}`)获取完整结构化结果(质量评分、来源标注等元数据)。
|
||||
- 事件分发:按 SSE `event:` 行分发,`data:` 行为该事件 payload JSON。事件类型:`chunk`(增量文本)、`quality_check`(质检)、`done`(结束,payload 含 `taskId` 与候选 `suggestionId`)、`error`(已脱敏错误)。
|
||||
- 结束信号:服务端发送 `event: done`,payload 含候选 ID。前端收到后用候选 ID 调用 REST 接口(`GET /app-api/muse/suggestions/{suggestionId}`)获取完整结构化结果(质量评分、来源标注等元数据)。
|
||||
- 错误处理:服务端发送 `event: error` 时,前端展示错误原因并关闭连接。
|
||||
|
||||
### 6.2 统一事件 stream
|
||||
|
||||
- 路径:`GET /app-api/muse/events/stream`
|
||||
- 路径:`GET /app-api/muse/events`
|
||||
- 生命周期:长连接,用户登录后建立,页面关闭时断开。
|
||||
- 用途:接收服务端推送的异步事件通知,前端按 event type 分发到对应处理器。
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# 后端-05:统一 API(接口) 契约-v1
|
||||
|
||||
- 版本:v8
|
||||
- 更新日期:2026-05-24
|
||||
- 版本:v9
|
||||
- 更新日期:2026-06-14
|
||||
- 目标读者:前端 / 后端 / 架构 / 测试
|
||||
- 阅读时间:35-55 分钟
|
||||
- 边界说明:本文件只定义 Muse 在 Yudao Cloud fork 上的 API 分组、资源语义、关键命令、错误模型与异步交互。底层表结构看 `后端-04`,状态机看 `架构-04`,关键流程看 `后端-03`。
|
||||
@ -449,11 +449,23 @@ Planning Candidate 是 Shadow 对象;确认时目标 owner 必须重验 work
|
||||
|---|---|---|
|
||||
| POST | `/app-api/muse/ai/tasks` | 创建生成、续写、扩写、润色、检测、规划任务 |
|
||||
| GET | `/app-api/muse/ai/tasks/{taskId}` | 查询任务 |
|
||||
| GET | `/app-api/muse/ai/tasks/{taskId}/stream` | AI 任务 SSE 流式事件(生成增量、质检、结束、错误) |
|
||||
| GET | `/app-api/muse/works/{workId}/suggestions` | 当前作品候选列表 |
|
||||
| GET | `/app-api/muse/suggestions/{suggestionId}` | 候选详情 |
|
||||
| POST | `/app-api/muse/suggestions/{suggestionId}/accept` | 接受候选 |
|
||||
| POST | `/app-api/muse/suggestions/{suggestionId}/reject` | 丢弃候选 |
|
||||
|
||||
AI 任务流(`GET .../ai/tasks/{taskId}/stream`)按 SSE 规范发送:`event:` 行为事件名,`data:` 行为该事件 payload JSON,`id:` 行为可续传的事件序号;无事件时发 SSE comment 作为 keepalive。事件契约:
|
||||
|
||||
| event | data 字段 | 说明 |
|
||||
|---|---|---|
|
||||
| `chunk` | `content`、`sequenceNo` | Token 级增量文本 |
|
||||
| `quality_check` | `dimension`、`score`、`passed` | 影子层质检结果 |
|
||||
| `done` | `taskId`、`suggestionId`(可选 `summary`) | 终态:生成完成;`suggestionId` 缺失则不发 `done` |
|
||||
| `error` | `code`、`message` | 终态:已脱敏错误 |
|
||||
|
||||
> 待确认(类型不一致):当前后端实现把 `done` 的 `taskId` / `suggestionId` 按 Long 序列化为 JSON **数字**,与本契约中候选/任务 ID 采用的 `uuid` 字符串(见候选详情等资源)不一致。需后端统一为 uuid 字符串或全局改判;统一前,前端已在 SSE 解析边界将其归一为字符串以保证类型稳定。
|
||||
|
||||
Accept 请求语义:
|
||||
|
||||
| 字段 | 要求 |
|
||||
|
||||
@ -203,45 +203,34 @@ export const useEditorUIStore = create<EditorUIStore>((set) => ({
|
||||
|
||||
### 双通道设计
|
||||
|
||||
1. **AI Stream 通道**:AI 生成内容的流式传输(独立连接,按请求生命周期)
|
||||
2. **事件 Stream 通道**:系统事件的统一推送(长连接,应用生命周期)
|
||||
studio 的 SSE 能力集中在 `src/lib/sse.ts`,对外暴露两个**函数**(既不是 React Hook,也不用浏览器原生 `EventSource`):
|
||||
|
||||
### AI Stream
|
||||
1. **AI Stream 通道**:`connectAIStream(url, handlers)` —— AI 生成内容的流式传输(独立连接,单次请求生命周期)。
|
||||
2. **事件 Stream 通道**:`connectEventStream(lastEventId?)` —— 系统事件的统一推送(应用级长连接)。
|
||||
|
||||
```tsx
|
||||
/**
|
||||
* AI 流式生成 Hook
|
||||
* 管理单次 AI 请求的 SSE 连接生命周期
|
||||
*/
|
||||
export function useAIStream(options: AIStreamOptions) {
|
||||
// 每次请求创建独立 EventSource
|
||||
// 支持中断(AbortController)
|
||||
// 支持 token 级增量渲染
|
||||
// 错误时自动重试 1 次
|
||||
}
|
||||
```
|
||||
> WHY 不用 `EventSource`:两条通道都要带自定义请求头(`X-API-Version`,事件流还要 `Authorization: Bearer`),而 `EventSource` 无法设置请求头,故统一用 `fetch` + `ReadableStream` 读流。
|
||||
|
||||
### 事件 Stream
|
||||
### 线格式与事件分发(务必对齐后端)
|
||||
|
||||
```tsx
|
||||
/**
|
||||
* 全局事件流 Hook
|
||||
* 统一管理系统级实时事件推送
|
||||
*/
|
||||
export function useEventStream() {
|
||||
// 应用启动时建立连接
|
||||
// 支持 lastEventId 断线续传
|
||||
// 心跳检测 + 自动重连(指数退避,最大 30s)
|
||||
// 事件类型:任务完成、协作通知、系统消息
|
||||
}
|
||||
```
|
||||
后端用 Spring `SseEmitter.event().name(<事件名>).data(<JSON>)` 发送:**事件类型在 SSE `event:` 行,`data:` 行只是该事件的 payload JSON(不含 `type` 字段),`id:` 行承载可续传序号**。因此前端必须**按 `event:` 行分发**,禁止按 `data` 里的 `type` 字段分发。两条通道共用同一套「按 `event:` 行解析 + `id` / comment / 多行 `data` 拼接」的 parser。
|
||||
|
||||
AI Stream 的四类事件与 payload:
|
||||
|
||||
| event | data payload | 说明 |
|
||||
|---|---|---|
|
||||
| `chunk` | `{ content, sequenceNo }` | Token 级增量文本 |
|
||||
| `quality_check` | `{ dimension, score, passed }` | 影子层质检结果 |
|
||||
| `done` | `{ taskId, suggestionId }` | 生成结束;`suggestionId` 是后续采纳写入 Canonical 的唯一入参 |
|
||||
| `error` | `{ code, message }` | 已脱敏错误,前端展示并关闭连接 |
|
||||
|
||||
> WHY `done` 的 ID 归一为 string:后端 `taskId`/`suggestionId` 实为 Long、会序列化为 JSON **数字**,而前端全链路(采纳请求体、生成的 OpenAPI 类型)按 **string** 处理;故在 `done` 解析边界统一 `String()` 归一后再回调,避免数字流入采纳请求体。
|
||||
|
||||
### 关键约束
|
||||
|
||||
- `lastEventId` 必须持久化到 localStorage,确保断线续传
|
||||
- AI Stream 超时设置 120s,事件 Stream 无超时
|
||||
- 所有 SSE 连接必须在组件卸载时正确关闭
|
||||
- 网络恢复后自动重连,不丢失中间事件
|
||||
- **AI Stream**:每次请求建立独立连接,用 `AbortController` 支持中止;**不自动重试/重连**(生成是一次性请求,失败走 `onError` 由用户重发)。
|
||||
- **事件 Stream**:应用级长连接,断线按指数退避自动重连(`1s → 2s → 5s → 10s` 封顶),以收到的 `id` 作为 `lastEventId` cursor 续传;缺登录凭证时 fail-closed(不发空 Bearer)。
|
||||
- 所有 SSE 连接必须在组件卸载时通过 `AbortController` 正确关闭,避免连接泄漏。
|
||||
- 解析失败统一走 `onError`(`SSE_PARSE_ERROR`),不让异常吞掉整条流。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -12,6 +12,9 @@ export interface SSEEventHandler {
|
||||
* AI 生成任务全部结束。
|
||||
* WHY 字段名:后端 done 事件 data 携带 { taskId, suggestionId }(见 muse-module-ai 的 doneData),
|
||||
* suggestionId 是用户后续采纳写入 Canonical 的唯一合法入参,因此这里改为对齐后端真实契约。
|
||||
* WHY 类型为 string:后端 taskId/suggestionId 实为 Long、会序列化为 JSON 数字,而前端全链路(采纳请求体、
|
||||
* 生成的 OpenAPI 类型 src/types/*.ts)按 string 处理;故在 connectAIStream 的 done 分支用 String() 归一后再回调,
|
||||
* 此处类型即归一后的前端契约。注:OpenAPI 把 suggestionId 标注为 uuid 字符串,与后端 Long 实现存在不一致,待后端统一。
|
||||
*/
|
||||
onDone?: (data: { taskId: string; suggestionId: string }) => void;
|
||||
/** 连接或解析发生错误 */
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user