feat:新增 SESSION_STAGES_ACCUMULATOR 跨流中间步骤累积器
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user