feat(記憶與情緒快取): 記憶與情緒狀態加入 Redis 快取層,連不到時自動退回原行為
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user