feat:新增 SESSION_STAGES_ACCUMULATOR 跨流中间步骤累积器

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-26 14:51:19 +08:00
parent fc0f5b88c5
commit 7de91dbbb4
2 changed files with 113 additions and 0 deletions
@@ -50,6 +50,14 @@ public class ShortNovelServiceImpl implements ShortNovelService {
private static final java.util.concurrent.ConcurrentHashMap<String, CachedSessionMeta> SESSION_ORIGINAL_QUERY_CACHE = new java.util.concurrent.ConcurrentHashMap<>();
private static final long SESSION_TTL_MS = 30 * 60 * 1000L;
/**
* sessionId -> 中间步骤累积列表(跨 SSE 流保存 clarification_question/answer/outline
* 用于在 novel_done 时统一创建 message,还原完整生成流程
* 会话超时 30 分钟(与 SESSION_ORIGINAL_QUERY_CACHE 对齐)
*/
private static final java.util.concurrent.ConcurrentHashMap<String, java.util.List<JSONObject>> SESSION_STAGES_ACCUMULATOR =
new java.util.concurrent.ConcurrentHashMap<>();
/**
* session 元数据(含 originalQuery 和缓存时间)
*/
@@ -99,6 +107,36 @@ public class ShortNovelServiceImpl implements ShortNovelService {
return meta.originalQuery;
}
/**
* 累积中间步骤到 session 缓存
* 包级可见,便于单元测试
*
* @param sessionId 会话ID
* @param stageEntry 步骤条目(含 kind 字段:clarification_question/clarification_answer/outline
*/
static void accumulateStage(String sessionId, JSONObject stageEntry) {
if (sessionId == null || sessionId.isEmpty() || stageEntry == null) {
return;
}
SESSION_STAGES_ACCUMULATOR.computeIfAbsent(sessionId,
k -> new java.util.concurrent.CopyOnWriteArrayList<>()).add(stageEntry);
}
/**
* 读取并清理 session 累积的中间步骤
* 包级可见,便于单元测试
*
* @param sessionId 会话ID
* @return 步骤列表(按累积顺序),无则返回空列表
*/
static java.util.List<JSONObject> drainStages(String sessionId) {
if (sessionId == null || sessionId.isEmpty()) {
return java.util.Collections.emptyList();
}
java.util.List<JSONObject> stages = SESSION_STAGES_ACCUMULATOR.remove(sessionId);
return stages != null ? stages : java.util.Collections.emptyList();
}
@Autowired
private ShortNovelConfig config;
@@ -0,0 +1,75 @@
package com.emotion.service.impl;
import com.alibaba.fastjson2.JSONObject;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
/**
* ShortNovelServiceImpl 静态缓存累积器单元测试
*
* @author huazhongmin
* @date 2026-07-26
*/
public class ShortNovelServiceImplTest {
@Test
public void testAccumulateStage_singleEntry() {
String sessionId = "test-session-1";
JSONObject entry = new JSONObject();
entry.put("kind", "clarification_question");
entry.put("card", new JSONObject().fluentPut("question", "你的咖啡馆特色?"));
ShortNovelServiceImpl.accumulateStage(sessionId, entry);
List<JSONObject> stages = ShortNovelServiceImpl.drainStages(sessionId);
assertEquals(1, stages.size());
assertEquals("clarification_question", stages.get(0).getString("kind"));
}
@Test
public void testAccumulateStage_multipleEntries_preservesOrder() {
String sessionId = "test-session-2";
ShortNovelServiceImpl.accumulateStage(sessionId,
new JSONObject().fluentPut("kind", "clarification_question").fluentPut("question", "Q1"));
ShortNovelServiceImpl.accumulateStage(sessionId,
new JSONObject().fluentPut("kind", "clarification_answer").fluentPut("answer", "A1"));
ShortNovelServiceImpl.accumulateStage(sessionId,
new JSONObject().fluentPut("kind", "outline").fluentPut("outline", new JSONObject()));
List<JSONObject> stages = ShortNovelServiceImpl.drainStages(sessionId);
assertEquals(3, stages.size());
assertEquals("clarification_question", stages.get(0).getString("kind"));
assertEquals("clarification_answer", stages.get(1).getString("kind"));
assertEquals("outline", stages.get(2).getString("kind"));
}
@Test
public void testDrainStages_clearsCache() {
String sessionId = "test-session-3";
ShortNovelServiceImpl.accumulateStage(sessionId,
new JSONObject().fluentPut("kind", "outline"));
List<JSONObject> first = ShortNovelServiceImpl.drainStages(sessionId);
assertEquals(1, first.size());
// 二次读取应为空(drain 后已清理)
List<JSONObject> second = ShortNovelServiceImpl.drainStages(sessionId);
assertTrue(second.isEmpty());
}
@Test
public void testAccumulateStage_nullSessionId_ignored() {
ShortNovelServiceImpl.accumulateStage(null,
new JSONObject().fluentPut("kind", "outline"));
assertTrue(ShortNovelServiceImpl.drainStages(null).isEmpty());
}
@Test
public void testDrainStages_unknownSession_returnsEmpty() {
List<JSONObject> stages = ShortNovelServiceImpl.drainStages("nonexistent-session");
assertTrue(stages.isEmpty());
}
}