diff --git a/docs/superpowers/plans/2026-07-21-sse-streaming-and-history-save-fix.md b/docs/superpowers/plans/2026-07-21-sse-streaming-and-history-save-fix.md new file mode 100644 index 0000000..5b21a85 --- /dev/null +++ b/docs/superpowers/plans/2026-07-21-sse-streaming-and-history-save-fix.md @@ -0,0 +1,639 @@ +# 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 saveResult = epicScriptDialogueServiceImpl.saveNovelResult(` 之前)添加: + +```java +log.info("[ShortNovel SSE] 开始保存小说: userId={}, queryLength={}, textLength={}", + currentUserId, originalQuery.length(), fullText.length()); +``` + +- [ ] **Step 8: 在保存成功后添加日志** + +在第 176 行(`Map 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`,会缓冲后端响应 +- 对于 SSE(Server-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`),确认是否支持真正的流式传输。 + +### 风险 2:nginx 配置不生效 + +**检测方法:** 使用 `curl -N` 测试 SSE 接口,观察是否逐条接收事件。 + +**缓解措施:** 检查 nginx error log,确认配置语法正确;检查是否有其他 location 块覆盖了 `/api/shortNovel/` 的配置。 + +### 风险 3:SseEmitter flush 不工作 + +**检测方法:** 查看后端日志,确认事件处理时间戳和发送时间戳的差异。 + +**缓解措施:** 如果 `emitter.send(SseEmitter.event().comment(""))` 仍然不 flush,考虑更换为 `ResponseBodyEmitter` 或 `StreamingResponseBody`。