Files
jev-match-game/src/engine/eventStore.ts
T

77 lines
2.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { DimensionTag, FollowUpDimension, IdentityProfile } from './types';
/**
* 选择/讲述事件存储(ADR-0004:历史不可涂改)。
* 接口只有 append 和 list —— 刻意不提供 update/delete,
* 让"不允许单独编辑历史选择"成为类型层面的约束,而非约定。
*/
export interface EventStore {
append(event: StoryEvent): Promise<void>;
/** 列出事件;传 sessionId 则只看该轨迹 */
list(sessionId?: string): Promise<StoryEvent[]>;
}
/** 一次重玩 = 一条新轨迹(新 sessionId),旧事件原样保留 */
export interface StoryEvent {
id: string;
sessionId: string;
ts: number;
chapterId: string;
kind: 'choice' | 'narration' | 'profile' | 'followup' | 'followup_skipped' | 'consent';
/** 选择点 ID,如 C1-03;consent 事件固定为 'CONSENT' */
pointId: string;
dimensions: DimensionTag[];
/** consent 事件:同意的告知文案版本(ADR-0004 存证) */
consentVersion?: string;
/** choice 事件:所选选项 */
optionId?: string;
optionLabel?: string;
/** narration 事件:选中的场景变体(情绪签名 → variantId) */
variantId?: string;
emotionSignature?: string;
/** followup / followup_skipped 事件:追问针对的缺失维度 */
followUpDimension?: FollowUpDimension;
/** 讲述/追问的转写文本(Web Speech API 只给文本,不留音频,符合 ADR-0004 即焚原则;沉默为 null) */
transcript?: string | null;
/** profile 事件:用户确认后的身份登记字段(匹配硬过滤字段) */
profile?: IdentityProfile;
}
const STORAGE_KEY = 'jev-dating-match.events.v1';
function newId(): string {
return typeof crypto !== 'undefined' && 'randomUUID' in crypto
? crypto.randomUUID()
: `e-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
}
export function makeEvent(partial: Omit<StoryEvent, 'id' | 'ts'>): StoryEvent {
return { ...partial, id: newId(), ts: Date.now() };
}
/** Web 端实现:localStorage。 */
export class LocalStorageEventStore implements EventStore {
async append(event: StoryEvent): Promise<void> {
const all = await this.list();
all.push(event);
localStorage.setItem(STORAGE_KEY, JSON.stringify(all));
}
async list(sessionId?: string): Promise<StoryEvent[]> {
let raw: string | null = null;
try {
raw = localStorage.getItem(STORAGE_KEY);
} catch {
return [];
}
const all: StoryEvent[] = raw ? (JSON.parse(raw) as StoryEvent[]) : [];
return sessionId ? all.filter((e) => e.sessionId === sessionId) : all;
}
}
/**
* Tauri 原生端实现:见 ./sqliteEventStore.ts(tauri-plugin-sql + SQLite,
* 同一 EventStore 接口,SQL 层同样只写 INSERT/SELECT)。
* 运行时选择在 App.tsx:window.__TAURI__ 存在用 SqliteEventStore,否则 localStorage。
*/