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

19 KiB
Raw Blame History

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(); 之后)添加:

log.info("[ShortNovel SSE] 开始读取上游响应: path={}, userId={}", path, currentUserId);
  • Step 2: 将 readUtf8Line() 改为 readUtf8LineStrict()

在第 145 行,将:

String line = source.readUtf8Line();

改为:

String line = source.readUtf8LineStrict();
  • Step 3: 在读取行后添加调试日志

在第 146 行(if (line == null) break; 之后)添加:

log.debug("[ShortNovel SSE] 读取到行: length={}, timestamp={}", 
          line.length(), System.currentTimeMillis());
  • Step 4: 在事件解析后添加时间戳日志

在第 160 行(String type = event.getString("type"); 之后)添加:

long eventTimestamp = System.currentTimeMillis();
log.info("[ShortNovel SSE] 处理事件: type={}, timestamp={}", type, eventTimestamp);
  • Step 5: 在 novel_done 事件处理中添加详细日志

在第 165 行(if ("novel_done".equals(type)) { 之后)添加:

JSONObject payload = event.getJSONObject("payload");
log.info("[ShortNovel SSE] novel_done 事件: originalQuery={}, payload={}", 
        originalQuery, payload != null);
  • Step 6: 在保存前添加 originalQuery 空字符串检查

将第 165 行的条件:

if (payload != null && originalQuery != null) {

改为:

if (payload != null && originalQuery != null && !originalQuery.trim().isEmpty()) {
  • Step 7: 在保存小说前添加日志

在第 172 行(Map<String, String> saveResult = epicScriptDialogueServiceImpl.saveNovelResult( 之前)添加:

log.info("[ShortNovel SSE] 开始保存小说: userId={}, queryLength={}, textLength={}", 
        currentUserId, originalQuery.length(), fullText.length());
  • Step 8: 在保存成功后添加日志

在第 176 行(Map<String, String> saveResult = ... 之后)添加:

log.info("[ShortNovel SSE] 小说保存成功: scriptId={}", saveResult.get("scriptId"));
  • Step 9: 在保存失败时添加警告日志

在第 183 行(} else { 之后)添加:

log.warn("[ShortNovel SSE] novel_done 事件缺少 full_text");
  • Step 10: 在跳过保存时添加警告日志

在第 185 行(} else { 之后)添加:

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())); 之后)添加:

emitter.send(SseEmitter.event().comment(""));  // 触发 flush
  • Step 12: 在循环结束后添加完成日志

在第 192 行(} 之前)添加:

log.info("[ShortNovel SSE] 完成读取上游响应");
  • Step 13: 编译后端验证
cd server
mvn clean install -DskipTests

预期输出:BUILD SUCCESS

  • Step 14: 提交后端修改
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 登录服务器

ssh root@101.200.208.45
  • Step 2: 备份当前 nginx 配置
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 配置文件
sudo nano /etc/nginx/sites-enabled/lifescript.happylifeos.com.conf

找到 location /api/shortNovel/ { 块(应该在第 50-65 行左右),将其修改为:

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 配置语法

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 配置
sudo systemctl reload nginx
  • Step 6: 验证 nginx 服务状态
sudo systemctl status nginx

预期输出:active (running)

  • Step 7: 提交 nginx 配置变更(到 git)
# 在本地创建 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) => { 之后)修改为:

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': { 之后)修改为:

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': { 之后)修改为:

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 之后)添加:

firstQuery.value = text  // 记住首次心愿,用于后续 followup 保存剧本
console.log('[ScriptView] 设置 firstQuery:', { text, length: text.length })
  • Step 5: 在 submitClarification 函数中添加日志

在第 1768 行(currentStreamTask.value = followupStream({ 之前)添加:

console.log('[ScriptView] submitClarification:', { 
  sessionId: novelSessionId.value,
  originalQuery: firstQuery.value,
  originalQueryLength: firstQuery.value?.length
})
  • Step 6: 在 confirmOutline 函数中添加日志

在第 1786 行(currentStreamTask.value = followupStream({ 之前)添加:

console.log('[ScriptView] confirmOutline:', { 
  sessionId: novelSessionId.value,
  originalQuery: firstQuery.value,
  originalQueryLength: firstQuery.value?.length
})
  • Step 7: 在 modifyOutline 函数中添加日志

在第 1802 行(currentStreamTask.value = followupStream({ 之前)添加:

console.log('[ScriptView] modifyOutline:', { 
  sessionId: novelSessionId.value,
  originalQuery: firstQuery.value,
  originalQueryLength: firstQuery.value?.length
})
  • Step 8: 在 resumeSession 函数中添加日志

在第 1826 行(currentStreamTask.value = followupStream({ 之前)添加:

console.log('[ScriptView] resumeSession:', { 
  sessionId: novelSessionId.value,
  originalQuery: firstQuery.value,
  originalQueryLength: firstQuery.value?.length
})
  • Step 9: 构建小程序验证
cd mini-program
npm run build:mp-weixin

预期输出:Build complete.

  • Step 10: 提交前端修改
git add mini-program/src/pages/main/ScriptView.vue
git commit -m "feat: 前端添加 SSE 事件诊断日志,确认 originalQuery 传递"

Task 4: 部署与验证

Files:

  • 无文件修改,纯部署和验证任务

  • Step 1: 上传后端 JAR 到服务器

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: 重启后端服务
ssh root@101.200.208.45 "cd /data/programs/emotion-museum && ./deploy-server.sh test"

预期输出:服务启动成功!

  • Step 3: 验证后端日志 - 检查事件逐条到达
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: 验证数据库记录
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 事件的时间戳都在同一毫秒,说明上游服务没有真正流式发送:

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: 提交部署验证文档
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: 记录测试结果
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,考虑更换为 ResponseBodyEmitterStreamingResponseBody