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
+144
View File
@@ -0,0 +1,144 @@
/**
* Jev 服务层 —— TypeSafe AI 的 System One 模型。
* REST: POST https://api.typesafe.ai/v1/systemone
* Header: Authorization: Bearer <TYPESAFE_API_KEY>
* 请求: { model: "jev-latest", state: <文本或JSON>, questions: { <id>: Question } }
* 响应: answers[<id>] 含分数/概率分布/confidence(0-1)
*/
/** 是非概率问题 */
export interface NoulQuestion {
type: 'noul';
description: string;
}
/** 选择题(≤255 选项),criteria 为 map */
export interface ChoiceQuestion {
type: 'choice';
description: string;
options: string[];
criteria?: Record<string, string>;
}
/** 打分题,criteria 为 2-10 级有序数组(从低到高) */
export interface ScoreQuestion {
type: 'score';
description: string;
criteria: string[];
}
export type JevQuestion = NoulQuestion | ChoiceQuestion | ScoreQuestion;
export interface JevRequest {
model: 'jev-latest';
state: unknown;
questions: Record<string, JevQuestion>;
}
export type JevAnswer =
| { type: 'noul'; probability: number; confidence: number }
| { type: 'choice'; selected: string; distribution: Record<string, number>; confidence: number }
| { type: 'score'; score: number; min: number; max: number; confidence: number };
export interface JevClient {
ask(state: unknown, questions: Record<string, JevQuestion>): Promise<Record<string, JevAnswer>>;
}
const API_URL = 'https://api.typesafe.ai/v1/systemone';
/** 直连 REST 的实现。浏览器调用可能遇到 CORS,正式环境需走后端代理(见 README)。 */
export class HttpJevClient implements JevClient {
private apiKey: string;
constructor(apiKey: string) {
this.apiKey = apiKey;
}
async ask(state: unknown, questions: Record<string, JevQuestion>): Promise<Record<string, JevAnswer>> {
const res = await fetch(API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${this.apiKey}`,
},
body: JSON.stringify({ model: 'jev-latest', state, questions } satisfies JevRequest),
});
if (!res.ok) {
throw new Error(`Jev API ${res.status}: ${await res.text()}`);
}
const body = (await res.json()) as { answers: Record<string, JevAnswer> };
return body.answers;
}
}
/** 简单确定性 hash(FNV-1a),Mock 用它让同一 (state, question) 永远得到同一答案。 */
function hash(input: string): number {
let h = 0x811c9dc5;
for (let i = 0; i < input.length; i++) {
h ^= input.charCodeAt(i);
h = Math.imul(h, 0x01000193);
}
return h >>> 0;
}
/** 由 hash 生成 [0,1) 的确定性伪随机序列 */
function prng(seed: number, n: number): number[] {
const out: number[] = [];
let s = seed;
for (let i = 0; i < n; i++) {
s = hash(String(s) + ':' + i);
out.push(s / 0xffffffff);
}
return out;
}
/** 无 key 开发用:确定性伪随机返分(同一输入永远同一输出,便于联调)。 */
export class MockJevClient implements JevClient {
async ask(state: unknown, questions: Record<string, JevQuestion>): Promise<Record<string, JevAnswer>> {
const stateKey = JSON.stringify(state) ?? '';
const answers: Record<string, JevAnswer> = {};
for (const [id, q] of Object.entries(questions)) {
const seed = hash(stateKey + '|' + id);
const confidence = Math.round((0.35 + prng(seed, 1)[0] * 0.6) * 100) / 100;
if (q.type === 'noul') {
answers[id] = { type: 'noul', probability: Math.round(prng(seed, 2)[1] * 100) / 100, confidence };
} else if (q.type === 'choice') {
const r = prng(seed, q.options.length + 1);
const weights = r.slice(0, q.options.length);
const total = weights.reduce((a, b) => a + b, 0) || 1;
const distribution: Record<string, number> = {};
let selected = q.options[0];
let best = -1;
q.options.forEach((opt, i) => {
distribution[opt] = Math.round((weights[i] / total) * 100) / 100;
if (weights[i] > best) {
best = weights[i];
selected = opt;
}
});
answers[id] = { type: 'choice', selected, distribution, confidence };
} else {
const min = 1;
const max = q.criteria.length;
const score = min + Math.floor(prng(seed, 1)[0] * max);
answers[id] = { type: 'score', score, min, max, confidence };
}
}
return answers;
}
}
let singleton: JevClient | null = null;
/** key 缺失时自动降级 Mock 并提示。 */
export function getJevClient(): JevClient {
if (singleton) return singleton;
const key = import.meta.env.VITE_TYPESAFE_API_KEY as string | undefined;
if (key) {
singleton = new HttpJevClient(key);
} else {
console.warn('[jev] 未配置 VITE_TYPESAFE_API_KEY,降级为 MockJevClient(确定性伪随机返分)');
singleton = new MockJevClient();
}
return singleton;
}