feat(記憶與情緒快取): 記憶與情緒狀態加入 Redis 快取層,連不到時自動退回原行為

This commit is contained in:
Jeffery
2026-08-14 12:44:41 +08:00
parent 080ac390d3
commit 6ce7b2e800
5 changed files with 140 additions and 35 deletions
+38
View File
@@ -5,6 +5,9 @@ import { RuleBasedEmotionTagger } from "./emotion-tagger.js";
import { nextEmotionState, type TransitionContext } from "./emotion-state-machine.js";
import { toResponseStyle, type ResponseStyle } from "./response-style.js";
import { getArchetypeParams } from "../personality/archetype-params.js";
import { getRedisClient } from "../cache/redis-client.js";
const REDIS_KEY_PREFIX = "kokorone:emotion-state:";
// D-2 時間衰減:預設半衰期 2 小時,實際速度由角色參數(G 群組)覆寫。
const DEFAULT_HALF_LIFE_MS = 2 * 60 * 60 * 1000;
@@ -133,11 +136,45 @@ export class EmotionService {
return { state: toDomain(characterId, updated, now), style: toResponseStyle(next) };
}
// R-2:情緒狀態機的即時讀寫改走 Redis 當熱路徑快取,Prisma/SQLite(未來 Postgres)
// 仍是唯一的持久真實來源——快取只省掉命中時的資料庫往返,Redis 不可用或未命中都直接退回原本
// 讀 Prisma 的行為,因此「重啟 api 後情緒狀態不遺失」這件事完全不受快取層是否存在影響。
private async readCached(characterId: string): Promise<{ dims: Dimensions; updatedAt: Date } | null> {
try {
const raw = await getRedisClient().get(REDIS_KEY_PREFIX + characterId);
if (!raw) return null;
const parsed = JSON.parse(raw) as Dimensions & { updatedAt: string };
const { updatedAt, ...dims } = parsed;
return { dims, updatedAt: new Date(updatedAt) };
} catch {
return null;
}
}
private async writeCache(characterId: string, dims: Dimensions, updatedAt: Date): Promise<void> {
try {
await getRedisClient().set(
REDIS_KEY_PREFIX + characterId,
JSON.stringify({ ...dims, updatedAt: updatedAt.toISOString() }),
"EX",
60 * 60 * 24,
);
} catch {
// Redis 不可用時,Prisma 仍是持久真實來源,快取寫入失敗不影響正確性。
}
}
private async decayedDimensions(
characterId: string,
now: Date,
halfLifeMs = DEFAULT_HALF_LIFE_MS,
): Promise<{ dims: Dimensions; updatedAt: Date }> {
const cached = await this.readCached(characterId);
if (cached) {
const elapsedMs = now.getTime() - cached.updatedAt.getTime();
return { dims: decayDimensions(cached.dims, elapsedMs, halfLifeMs), updatedAt: cached.updatedAt };
}
const row = await this.prisma.client.emotionState.upsert({
where: { characterId },
update: {},
@@ -154,6 +191,7 @@ export class EmotionService {
where: { characterId },
data: { ...dims, updatedAt: now },
});
await this.writeCache(characterId, dims, now);
}
}