feat: MVP 工程脚手架——剧情引擎、Act 1 三章内容、Jev 服务层、画像报告与分享卡、Tauri 壳

This commit is contained in:
Jayway Liu
2026-10-03 10:05:10 +08:00
parent f57eabe12c
commit bf9d7dad5b
54 changed files with 8040 additions and 0 deletions
+70
View File
@@ -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「已知缺口」。
*/