feat: 自适应支线插入引擎(WIP:scoring/branching 抽取、挂点重定向、10 条支线 JSON、决策落库)

This commit is contained in:
Jayway Liu
2026-10-04 06:57:32 +08:00
parent 512651b973
commit ea9fa0fe56
37 changed files with 4311 additions and 817 deletions
+12 -57
View File
@@ -1,43 +1,18 @@
import { useEffect, useState } from 'react';
import type { EventStore, StoryEvent } from '../../engine/eventStore';
import type { EventStore } from '../../engine/eventStore';
import type { JevClient } from '../../services/jev';
import { BADGE_MAP, PROFILE_DIMENSIONS } from './badges';
import { BADGE_MAP } from './badges';
import { scoreProfile } from '../../engine/scoring';
import type { DimensionScore } from '../../engine/scoring';
export type { DimensionScore } from '../../engine/scoring';
import { ShareCard } from './ShareCard';
export interface DimensionScore {
id: string;
label: string;
score: number;
max: number;
confidence: number;
}
interface Props {
store: EventStore;
jev: JevClient;
onReplay: () => void;
}
/** 把 append-only 事件轨迹汇总为喂给 Jev 的 state 文本(profile 身份字段是硬过滤字段,不进画像) */
function buildState(events: StoryEvent[]): string {
const lines = events
.filter((e) => e.kind !== 'profile')
.map((e) => {
if (e.kind === 'choice') {
return `[${e.pointId}] 选择「${e.optionLabel}」(维度: ${e.dimensions.join('/') || '字段采集'})`;
}
if (e.kind === 'followup') {
return `[${e.pointId}] 追问(${e.followUpDimension})回答: ${e.transcript}`;
}
if (e.kind === 'followup_skipped') {
return `[${e.pointId}] 追问(${e.followUpDimension})回答: 记不清了(跳过)`;
}
const t = e.transcript === null ? '(沉默)' : `(讲述: ${e.transcript})`;
return `[${e.pointId}] 讲述${t} 情绪签名: ${e.emotionSignature ?? '未知'}`;
});
return `用户在互动剧情《苍梧疑冢》第一幕中的行为轨迹(按时间排序,含初刷与重玩轨迹):\n${lines.join('\n')}`;
}
/** Act 1 结束后的画像报告页:Jev 一次调用并行问 7 个维度 score */
export function ReportPage({ store, jev, onReplay }: Props) {
const [scores, setScores] = useState<DimensionScore[] | null>(null);
@@ -48,35 +23,14 @@ export function ReportPage({ store, jev, onReplay }: Props) {
useEffect(() => {
let cancelled = false;
(async () => {
const events = await store.list();
// 最近一次身份登记的称呼(旧数据缺 name 字段时回退空串 → 只显示编号)
const name = [...events].reverse().find((e) => e.kind === 'profile')?.profile?.name ?? '';
if (!cancelled) setPlayerName(name);
const state = buildState(events);
// 7 维 score 问题,criteria 为 10 级有序数组(索引 0-9 对应 1-10 级)
const criteria = Array.from({ length: 10 }, (_, i) => `${i + 1} 级`);
const questions = Object.fromEntries(
PROFILE_DIMENSIONS.map((d) => [
d.id,
{
type: 'score' as const,
instructions: `根据用户行为轨迹,评估其「${d.label}」维度水平:${d.description}`,
criteria,
},
]),
);
try {
const answers = await jev.ask(state, questions);
const events = await store.list();
// 最近一次身份登记的称呼(旧数据缺 name 字段时回退空串 → 只显示编号)
const name = [...events].reverse().find((e) => e.kind === 'profile')?.profile?.name ?? '';
if (!cancelled) setPlayerName(name);
const result = await scoreProfile(events, jev);
if (cancelled) return;
setScores(
PROFILE_DIMENSIONS.map((d) => {
const a = answers[d.id];
// API score 是 [0, criteria.length-1] 的概率加权浮点值,+1 映射到 1-10 级展示
return a?.type === 'score'
? { id: d.id, label: d.label, score: Math.round((a.score + 1) * 10) / 10, max: 10, confidence: a.confidence }
: { id: d.id, label: d.label, score: 0, max: 10, confidence: 0 };
}),
);
setScores(result);
} catch (e) {
if (!cancelled) setError(e instanceof Error ? e.message : String(e));
}
@@ -144,6 +98,7 @@ export function ReportPage({ store, jev, onReplay }: Props) {
</div>
<span className="dimension-value">
{s.score.toFixed(1)}/{s.max} · 置信度 {Math.round(s.confidence * 100)}%
{s.confidence < 0.6 && ' · 证据不足'}
</span>
</div>
))}