Files
happy-life-star/docs/superpowers/plans/2026-07-21-sse-streaming-and-history-save-fix.md
T

640 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SSE 流式传输与历史列表保存修复实施计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 修复小说生成页面的 SSE 流式传输(实现逐字输出)和历史列表保存问题
**Architecture:** 通过三个层面的修改实现真正的流式传输:后端 OkHttp 使用严格行读取 + 显式 flush、nginx 禁用代理缓冲、前端添加诊断日志确认 originalQuery 传递
**Tech Stack:** Spring Boot 2.7.18, OkHttp 4.12.0, nginx, Vue 3 (UniApp)
---
## 文件结构映射
**修改的文件:**
1. `server/src/main/java/com/emotion/service/impl/ShortNovelServiceImpl.java:140-192` - 后端 SSE 转发逻辑
2. `/etc/nginx/sites-enabled/lifescript.happylifeos.com.conf` - nginx 配置(服务器)
3. `mini-program/src/pages/main/ScriptView.vue:1650-1843` - 前端事件处理
**不创建新文件** - 所有修改都是在现有文件中添加日志和优化逻辑
---
## Task 1: 后端 OkHttp 读取优化
**Files:**
- Modify: `server/src/main/java/com/emotion/service/impl/ShortNovelServiceImpl.java:140-192`
- [ ] **Step 1: 在 forwardSse 方法开头添加开始日志**
打开 `server/src/main/java/com/emotion/service/impl/ShortNovelServiceImpl.java`,在第 140 行(`BufferedSource source = responseBody.source();` 之后)添加:
```java
log.info("[ShortNovel SSE] 开始读取上游响应: path={}, userId={}", path, currentUserId);
```
- [ ] **Step 2: 将 readUtf8Line() 改为 readUtf8LineStrict()**
在第 145 行,将:
```java
String line = source.readUtf8Line();
```
改为:
```java
String line = source.readUtf8LineStrict();
```
- [ ] **Step 3: 在读取行后添加调试日志**
在第 146 行(`if (line == null) break;` 之后)添加:
```java
log.debug("[ShortNovel SSE] 读取到行: length={}, timestamp={}",
line.length(), System.currentTimeMillis());
```
- [ ] **Step 4: 在事件解析后添加时间戳日志**
在第 160 行(`String type = event.getString("type");` 之后)添加:
```java
long eventTimestamp = System.currentTimeMillis();
log.info("[ShortNovel SSE] 处理事件: type={}, timestamp={}", type, eventTimestamp);
```
- [ ] **Step 5: 在 novel_done 事件处理中添加详细日志**
在第 165 行(`if ("novel_done".equals(type)) {` 之后)添加:
```java
JSONObject payload = event.getJSONObject("payload");
log.info("[ShortNovel SSE] novel_done 事件: originalQuery={}, payload={}",
originalQuery, payload != null);
```
- [ ] **Step 6: 在保存前添加 originalQuery 空字符串检查**
将第 165 行的条件:
```java
if (payload != null && originalQuery != null) {
```
改为:
```java
if (payload != null && originalQuery != null && !originalQuery.trim().isEmpty()) {
```
- [ ] **Step 7: 在保存小说前添加日志**
在第 172 行(`Map<String, String> saveResult = epicScriptDialogueServiceImpl.saveNovelResult(` 之前)添加:
```java
log.info("[ShortNovel SSE] 开始保存小说: userId={}, queryLength={}, textLength={}",
currentUserId, originalQuery.length(), fullText.length());
```
- [ ] **Step 8: 在保存成功后添加日志**
在第 176 行(`Map<String, String> saveResult = ...` 之后)添加:
```java
log.info("[ShortNovel SSE] 小说保存成功: scriptId={}", saveResult.get("scriptId"));
```
- [ ] **Step 9: 在保存失败时添加警告日志**
在第 183 行(`} else {` 之后)添加:
```java
log.warn("[ShortNovel SSE] novel_done 事件缺少 full_text");
```
- [ ] **Step 10: 在跳过保存时添加警告日志**
在第 185 行(`} else {` 之后)添加:
```java
log.warn("[ShortNovel SSE] novel_done 事件跳过保存: originalQuery={}, payload={}",
originalQuery, payload);
```
- [ ] **Step 11: 在 emitter.send() 后添加 flush 触发**
在第 187 行(`emitter.send(SseEmitter.event().name(type).data(event.toJSONString()));` 之后)添加:
```java
emitter.send(SseEmitter.event().comment("")); // 触发 flush
```
- [ ] **Step 12: 在循环结束后添加完成日志**
在第 192 行(`}` 之前)添加:
```java
log.info("[ShortNovel SSE] 完成读取上游响应");
```
- [ ] **Step 13: 编译后端验证**
```bash
cd server
mvn clean install -DskipTests
```
预期输出:`BUILD SUCCESS`
- [ ] **Step 14: 提交后端修改**
```bash
git add server/src/main/java/com/emotion/service/impl/ShortNovelServiceImpl.java
git commit -m "feat: 优化 SSE 转发逻辑,使用严格行读取和显式 flush"
```
---
## Task 2: nginx 配置优化
**Files:**
- Modify: `/etc/nginx/sites-enabled/lifescript.happylifeos.com.conf`(服务器)
- [ ] **Step 1: SSH 登录服务器**
```bash
ssh root@101.200.208.45
```
- [ ] **Step 2: 备份当前 nginx 配置**
```bash
sudo cp /etc/nginx/sites-enabled/lifescript.happylifeos.com.conf /etc/nginx/sites-enabled/lifescript.happylifeos.com.conf.backup.$(date +%Y%m%d_%H%M%S)
```
- [ ] **Step 3: 编辑 nginx 配置文件**
```bash
sudo nano /etc/nginx/sites-enabled/lifescript.happylifeos.com.conf
```
找到 `location /api/shortNovel/ {` 块(应该在第 50-65 行左右),将其修改为:
```nginx
location /api/shortNovel/ {
proxy_pass http://127.0.0.1:19089;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# 禁用代理缓冲,确保 SSE 事件立即转发
proxy_buffering off;
proxy_cache off;
chunked_transfer_encoding on;
proxy_set_header Connection '';
# 超时配置
proxy_connect_timeout 300s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
}
```
**关键新增行:**
- `proxy_buffering off;` - 禁用响应缓冲
- `proxy_cache off;` - 禁用缓存
- `chunked_transfer_encoding on;` - 启用分块传输
- `proxy_set_header Connection '';` - 清除 Connection 头
- [ ] **Step 4: 验证 nginx 配置语法**
```bash
sudo nginx -t
```
预期输出:
```
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
```
- [ ] **Step 5: 重新加载 nginx 配置**
```bash
sudo systemctl reload nginx
```
- [ ] **Step 6: 验证 nginx 服务状态**
```bash
sudo systemctl status nginx
```
预期输出:`active (running)`
- [ ] **Step 7: 提交 nginx 配置变更(到 git)**
```bash
# 在本地创建 nginx 配置文档记录变更
cat > docs/nginx-config-changes/2026-07-21-shortnovel-sse-optimization.md << 'EOF'
---
author: AI Assistant
created_at: 2026-07-21
purpose: 记录 nginx 配置优化,禁用 SSE 响应缓冲
---
# nginx 配置优化 - ShortNovel SSE
**文件:** `/etc/nginx/sites-enabled/lifescript.happylifeos.com.conf`
**变更内容:**
在 `location /api/shortNovel/` 块中添加了以下配置:
- `proxy_buffering off;` - 禁用代理缓冲
- `proxy_cache off;` - 禁用缓存
- `chunked_transfer_encoding on;` - 启用分块传输编码
- `proxy_set_header Connection '';` - 清除 Connection 头
**原因:**
- nginx 默认启用 `proxy_buffering on`,会缓冲后端响应
- 对于 SSEServer-Sent Events)长连接,缓冲会导致事件无法立即到达前端
- 禁用缓冲后,每个事件都能立即转发给客户端,实现真正的流式传输
**验证:**
- 使用 `curl -N https://lifescript.happylifeos.com/api/shortNovel/stream` 测试
- 应该能够逐条接收 SSE 事件,而不是等待所有事件完成后一次性接收
EOF
git add docs/nginx-config-changes/2026-07-21-shortnovel-sse-optimization.md
git commit -m "docs: 记录 nginx SSE 配置优化"
```
---
## Task 3: 前端事件处理优化
**Files:**
- Modify: `mini-program/src/pages/main/ScriptView.vue:1650-1843`
- [ ] **Step 1: 在 handleShortNovelEvent 函数开头添加日志**
打开 `mini-program/src/pages/main/ScriptView.vue`,在第 1691 行(`const handleShortNovelEvent = (event) => {` 之后)修改为:
```javascript
const handleShortNovelEvent = (event) => {
const { type, session_id, payload = {} } = event
const timestamp = Date.now()
console.log('[ScriptView] 收到事件:', { type, session_id, timestamp, payload })
if (session_id) novelSessionId.value = session_id
```
- [ ] **Step 2: 在 novel_delta case 中添加日志**
在第 1715 行(`case 'novel_delta': {` 之后)修改为:
```javascript
case 'novel_delta': {
const lastNovel = [...resultMessages.value].reverse().find(m => m.kind === 'novel' && m.pending)
if (lastNovel) {
const delta = payload.delta || ''
console.log('[ScriptView] novel_delta:', {
deltaLength: delta.length,
currentLength: lastNovel.content.length,
timestamp
})
lastNovel.content += delta
}
keepResultAtBottom()
break
}
```
- [ ] **Step 3: 在 novel_done case 中添加日志**
在第 1724 行(`case 'novel_done': {` 之后)修改为:
```javascript
case 'novel_done': {
console.log('[ScriptView] novel_done:', {
scriptId: payload.scriptId,
hasFullText: !!payload.full_text,
timestamp
})
// 标记最后一条 novel 消息完成
const lastNovel = [...resultMessages.value].reverse().find(m => m.kind === 'novel')
if (lastNovel) {
if (payload.full_text) lastNovel.content = payload.full_text
lastNovel.pending = false
}
scriptId.value = payload.scriptId || ''
conversationId.value = payload.conversationId || ''
currentVersionMessageId.value = payload.currentVersionMessageId || ''
generationPhase.value = 'done'
generationStatus.value = 'idle'
pendingNextResponse.value = false
persistResultMessages()
store.fetchScripts()
break
}
```
- [ ] **Step 4: 在 startNovelGeneration 函数中添加日志确认 firstQuery 设置**
在第 1650 行(`firstQuery.value = text` 之后)添加:
```javascript
firstQuery.value = text // 记住首次心愿,用于后续 followup 保存剧本
console.log('[ScriptView] 设置 firstQuery:', { text, length: text.length })
```
- [ ] **Step 5: 在 submitClarification 函数中添加日志**
在第 1768 行(`currentStreamTask.value = followupStream({` 之前)添加:
```javascript
console.log('[ScriptView] submitClarification:', {
sessionId: novelSessionId.value,
originalQuery: firstQuery.value,
originalQueryLength: firstQuery.value?.length
})
```
- [ ] **Step 6: 在 confirmOutline 函数中添加日志**
在第 1786 行(`currentStreamTask.value = followupStream({` 之前)添加:
```javascript
console.log('[ScriptView] confirmOutline:', {
sessionId: novelSessionId.value,
originalQuery: firstQuery.value,
originalQueryLength: firstQuery.value?.length
})
```
- [ ] **Step 7: 在 modifyOutline 函数中添加日志**
在第 1802 行(`currentStreamTask.value = followupStream({` 之前)添加:
```javascript
console.log('[ScriptView] modifyOutline:', {
sessionId: novelSessionId.value,
originalQuery: firstQuery.value,
originalQueryLength: firstQuery.value?.length
})
```
- [ ] **Step 8: 在 resumeSession 函数中添加日志**
在第 1826 行(`currentStreamTask.value = followupStream({` 之前)添加:
```javascript
console.log('[ScriptView] resumeSession:', {
sessionId: novelSessionId.value,
originalQuery: firstQuery.value,
originalQueryLength: firstQuery.value?.length
})
```
- [ ] **Step 9: 构建小程序验证**
```bash
cd mini-program
npm run build:mp-weixin
```
预期输出:`Build complete.`
- [ ] **Step 10: 提交前端修改**
```bash
git add mini-program/src/pages/main/ScriptView.vue
git commit -m "feat: 前端添加 SSE 事件诊断日志,确认 originalQuery 传递"
```
---
## Task 4: 部署与验证
**Files:**
- 无文件修改,纯部署和验证任务
- [ ] **Step 1: 上传后端 JAR 到服务器**
```bash
scp server/target/server-1.0.0.jar root@101.200.208.45:/data/programs/emotion-museum/emotion-single-1.0.0.jar
```
预期输出:`100%` 传输完成
- [ ] **Step 2: 重启后端服务**
```bash
ssh root@101.200.208.45 "cd /data/programs/emotion-museum && ./deploy-server.sh test"
```
预期输出:`服务启动成功!`
- [ ] **Step 3: 验证后端日志 - 检查事件逐条到达**
```bash
ssh root@101.200.208.45 "tail -f /data/logs/emotion-museum/emotion-single.log | grep 'ShortNovel SSE'"
```
然后在浏览器中触发一次小说生成,观察日志应该显示:
- `[ShortNovel SSE] 开始读取上游响应`
- 多个 `[ShortNovel SSE] 处理事件: type=novel_delta, timestamp=...`(时间戳应该逐条递增,间隔 > 100ms)
- `[ShortNovel SSE] novel_done 事件: originalQuery=..., payload=...`
- `[ShortNovel SSE] 开始保存小说: ...`
- `[ShortNovel SSE] 小说保存成功: scriptId=...`
- `[ShortNovel SSE] 完成读取上游响应`
- [ ] **Step 4: 验证前端日志 - 检查事件逐字显示**
在微信开发者工具或浏览器中打开小程序,进入小说生成页面:
1. 输入心愿文本并生成
2. 打开 Console 面板
3. 观察日志应该显示:
- `[ScriptView] 设置 firstQuery: {text: "...", length: ...}`
- `[ScriptView] 收到事件: {type: "novel_start", ...}`
- 多个 `[ScriptView] novel_delta: {deltaLength: 1-5, currentLength: ..., timestamp: ...}`(应该逐条出现)
- `[ScriptView] novel_done: {scriptId: "...", hasFullText: true, ...}`
4. **关键验证点:** 小说内容应该逐字显示,而不是一次性出现
- [ ] **Step 5: 验证历史列表保存**
1. 完成一次完整的小说生成流程
2. 返回主页,进入"历史"页面
3. 验证新生成的小说是否出现在列表中
4. 点击该小说,确认内容完整
- [ ] **Step 6: 验证数据库记录**
```bash
ssh root@101.200.208.45 "mysql -u root -pEmotionMuseum2025*# emotion_museum -e \"SELECT id, user_id, prompt, LEFT(content, 50) as content_preview, create_time FROM t_epic_script ORDER BY create_time DESC LIMIT 5;\""
```
预期输出:应该看到最新的记录,`create_time` 应该是刚才生成的时间
- [ ] **Step 7: 如果流式输出仍然不工作,检查上游服务**
如果日志显示所有 `novel_delta` 事件的时间戳都在同一毫秒,说明上游服务没有真正流式发送:
```bash
ssh root@101.200.208.45 "tail -100 /data/logs/emotion-museum/emotion-single.log | grep 'novel_delta' | awk '{print \$1, \$2}' | uniq -c"
```
如果输出显示所有事件都在同一秒,需要联系上游服务提供者确认是否支持真正的流式传输。
- [ ] **Step 8: 提交部署验证文档**
```bash
cat > docs/deployment-verification/2026-07-21-sse-streaming-fix.md << 'EOF'
---
author: AI Assistant
created_at: 2026-07-21
purpose: 记录 SSE 流式传输修复的部署验证过程
---
# SSE 流式传输修复 - 部署验证
**部署时间:** 2026-07-21
**修改内容:**
1. 后端:使用 `readUtf8LineStrict()` + 显式 flush
2. nginx:添加 `proxy_buffering off` 等配置
3. 前端:添加诊断日志
**验证结果:**
- [ ] 后端日志显示事件逐条到达(时间间隔 > 100ms)
- [ ] 前端 Console 显示 novel_delta 事件逐条接收
- [ ] 小说内容逐字显示
- [ ] 历史列表正确保存新生成的小说
- [ ] 数据库 t_epic_script 表有新记录
**问题与解决:**
(在此记录遇到的问题和解决方案)
EOF
git add docs/deployment-verification/2026-07-21-sse-streaming-fix.md
git commit -m "docs: 记录 SSE 流式传输修复的部署验证"
```
---
## Task 5: 测试用例执行
**Files:**
- 无文件修改,纯测试任务
- [ ] **Step 1: 执行测试用例 1 - 完整生成流程**
1. 输入心愿文本:"我想写一个关于时间旅行的故事"
2. 回答澄清问题(例如选择时代背景)
3. 确认大纲
4. 等待小说生成完成
5. **验证:**
- 前端逐字显示小说内容(观察 Console 日志)
- 后端日志显示事件逐条到达(`tail -f` 查看)
- 数据库中出现新记录
- 历史列表显示新生成的小说
- [ ] **Step 2: 执行测试用例 2 - 修改大纲后重新生成**
1. 输入心愿文本
2. 回答澄清问题
3. 点击"修改大纲",填写修改意见
4. 等待小说生成完成
5. **验证:** 同测试用例 1
- [ ] **Step 3: 执行测试用例 3 - 继续之前的创作**
1. 输入心愿文本
2. 回答澄清问题
3. 中断流程(关闭页面或刷新)
4. 重新进入页面,点击"继续创作"
5. 等待小说生成完成
6. **验证:** 同测试用例 1
- [ ] **Step 4: 记录测试结果**
```bash
cat > docs/test-results/2026-07-21-sse-streaming-test.md << 'EOF'
---
author: AI Assistant
created_at: 2026-07-21
purpose: 记录 SSE 流式传输修复的测试结果
---
# SSE 流式传输修复 - 测试结果
**测试时间:** 2026-07-21
## 测试用例 1:完整生成流程
- [ ] 前端逐字显示
- [ ] 后端日志显示事件逐条到达
- [ ] 数据库记录正确
- [ ] 历史列表显示
**测试结果:** PASS / FAIL
**问题描述:**(如果有)
## 测试用例 2:修改大纲后重新生成
- [ ] 前端逐字显示
- [ ] 后端日志显示事件逐条到达
- [ ] 数据库记录正确
- [ ] 历史列表显示
**测试结果:** PASS / FAIL
## 测试用例 3:继续之前的创作
- [ ] 前端逐字显示
- [ ] 后端日志显示事件逐条到达
- [ ] 数据库记录正确
- [ ] 历史列表显示
**测试结果:** PASS / FAIL
## 总结
(在此总结测试结果和发现的问题)
EOF
git add docs/test-results/2026-07-21-sse-streaming-test.md
git commit -m "docs: 记录 SSE 流式传输修复的测试结果"
```
---
## 完成标准
1. ✅ 所有后端日志显示事件逐条到达(时间间隔 > 100ms)
2. ✅ 前端 Console 显示 novel_delta 事件逐条接收
3. ✅ 小说内容在前端逐字显示,用户可以观察到文字逐个出现
4. ✅ 每次成功的小说生成都保存到数据库
5. ✅ 历史列表正确显示新生成的小说
6. ✅ 所有测试用例通过
---
## 风险评估与缓解
### 风险 1:上游服务不支持真正的流式传输
**检测方法:** 查看后端日志,如果所有 `novel_delta` 事件的时间戳都在同一毫秒,说明上游服务一次性发送了所有数据。
**缓解措施:** 联系上游服务提供者(`http://49.232.138.53:8010`),确认是否支持真正的流式传输。
### 风险 2nginx 配置不生效
**检测方法:** 使用 `curl -N` 测试 SSE 接口,观察是否逐条接收事件。
**缓解措施:** 检查 nginx error log,确认配置语法正确;检查是否有其他 location 块覆盖了 `/api/shortNovel/` 的配置。
### 风险 3SseEmitter flush 不工作
**检测方法:** 查看后端日志,确认事件处理时间戳和发送时间戳的差异。
**缓解措施:** 如果 `emitter.send(SseEmitter.event().comment(""))` 仍然不 flush,考虑更换为 `ResponseBodyEmitter``StreamingResponseBody`