fix: 首启同意门(consent 事件存证)+ 条件渲染系统(flags 推导 27 变量)+ 10 类内容-界面不一致修复

This commit is contained in:
Jayway Liu
2026-10-03 18:21:09 +08:00
parent d2a31ab38c
commit b0c7ef2daf
11 changed files with 501 additions and 29 deletions
+62
View File
@@ -3,6 +3,11 @@ import { FOLLOW_UP_DIMENSIONS, FOLLOW_UP_PRIORITY } from './types';
import type { EventStore, StoryEvent } from './eventStore';
import { makeEvent } from './eventStore';
import type { JevClient, JevQuestion } from '../services/jev';
import type { Flags } from './flags';
import { choiceFlags, narrationFlags, resolveConditionalText } from './flags';
/** 告知文案版本:文案变动时升版,consent 事件存证所同意的版本(ADR-0004) */
export const CONSENT_VERSION = 'act1-consent-v1';
export interface EngineState {
sessionId: string;
@@ -12,6 +17,8 @@ export interface EngineState {
activeVariant: SceneVariant | null;
/** 追问补全:讲述后检测到缺失维度时挂起,等用户回答或"记不清了" */
pendingFollowUp: PendingFollowUp | null;
/** 分支变量(附录 A):从本轨迹的选择/讲述事件增量推导,驱动条件文本 */
flags: Flags;
/** 全部三章完成后为 true → 进入画像报告 */
finished: boolean;
}
@@ -78,8 +85,10 @@ export class StoryEngine {
nodeId: start.startNodeId,
activeVariant: null,
pendingFollowUp: null,
flags: {},
finished: false,
};
this.skipHiddenNodes();
}
currentChapter(): Chapter {
@@ -110,6 +119,29 @@ export class StoryEngine {
} else {
throw new Error(`advance() 不适用于 ${node.type} 节点`);
}
this.skipHiddenNodes();
}
/**
* 条件节点跳过:节点级【若 …】条件不满足的 dialogue / cutscene 对玩家不可见,
* 直接走到 next(条件分支不产生死路)。每次状态迁移后调用。
*/
private skipHiddenNodes(): void {
for (let guard = 0; guard < 100; guard++) {
const node = this.currentNode();
if (node.type !== 'dialogue' && node.type !== 'cutscene') return;
const text = node.type === 'dialogue' ? node.text : node.caption;
if (resolveConditionalText(text, this.state.flags) !== null) return;
this.state.nodeId = node.next;
}
}
/**
* 渲染文本:求值文案中的全部【若 …】条件标记(节点级 + 内联段),
* 返回玩家可见文本。条件标记本身永不渲染给玩家。
*/
renderText(text: string): string {
return resolveConditionalText(text, this.state.flags) ?? '';
}
/** B 类选择:落事件并按选项跳转 */
@@ -129,7 +161,9 @@ export class StoryEngine {
optionLabel: option.label,
}),
);
Object.assign(this.state.flags, choiceFlags(node.pointId, option.id));
this.state.nodeId = option.next;
this.skipHiddenNodes();
}
/**
@@ -235,8 +269,10 @@ export class StoryEngine {
transcript,
});
await this.store.append(event);
Object.assign(this.state.flags, narrationFlags(node.pointId, variant.variantId, transcript));
this.state.activeVariant = variant;
this.state.nodeId = node.next;
this.skipHiddenNodes();
return { variant, event };
}
@@ -370,6 +406,30 @@ export class StoryEngine {
}),
);
this.state.nodeId = node.next;
this.skipHiddenNodes();
}
/**
* 同意存证(ADR-0004):告知与同意的 UX 由 ConsentGate 承担,
* 同意动作在这里 append 一条 consent 事件(含版本与时间戳)。
* 事件存在于任意轨迹即视为已同意——同意门全剧只出现一次。
*/
async hasConsent(): Promise<boolean> {
return (await this.store.list()).some((e) => e.kind === 'consent');
}
async grantConsent(): Promise<void> {
if (await this.hasConsent()) return;
await this.store.append(
makeEvent({
sessionId: this.state.sessionId,
chapterId: this.state.chapterId,
kind: 'consent',
pointId: 'CONSENT',
dimensions: [],
consentVersion: CONSENT_VERSION,
}),
);
}
/**
@@ -384,6 +444,8 @@ export class StoryEngine {
this.state.nodeId = chapter.startNodeId;
this.state.activeVariant = null;
this.state.pendingFollowUp = null;
this.state.flags = {};
this.state.finished = false;
this.skipHiddenNodes();
}
}