feat: MVP 工程脚手架——剧情引擎、Act 1 三章内容、Jev 服务层、画像报告与分享卡、Tauri 壳
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
import type { DimensionTag } 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';
|
||||
/** 选择点 ID,如 C1-03 */
|
||||
pointId: string;
|
||||
dimensions: DimensionTag[];
|
||||
/** choice 事件:所选选项 */
|
||||
optionId?: string;
|
||||
optionLabel?: string;
|
||||
/** narration 事件:选中的场景变体(情绪签名 → variantId) */
|
||||
variantId?: string;
|
||||
emotionSignature?: string;
|
||||
/** 讲述转写文本占位(ASR 未接,MVP 为手输或占位文本;沉默为 null) */
|
||||
transcript?: string | null;
|
||||
}
|
||||
|
||||
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 原生端实现预留:后续用 tauri-plugin-sql(SQLite)实现同一接口,
|
||||
* 表结构即 StoryEvent 一行一条,只写 INSERT/SELECT,不建 UPDATE/DELETE 路径。
|
||||
* 见 README「已知缺口」。
|
||||
*/
|
||||
Reference in New Issue
Block a user