feat: 对齐 Jev 真实 API schema、Web Speech 语音输入(优雅降级)、C1-01 语音化身份登记与真 key 冒烟脚本

This commit is contained in:
Jayway Liu
2026-10-03 12:43:41 +08:00
parent bf9d7dad5b
commit bed4d734ca
14 changed files with 623 additions and 95 deletions
+53 -27
View File
@@ -3,27 +3,38 @@
* 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)
*
* 以下 schema 按真实接口 curl 实测对齐(2026-10-03):
* - Question 的文本字段是 instructions;
* - choice 的 criteria 是 { 选项: 描述 } 的 map(选项数 ≤255);
* - score 的 criteria 是 2-10 级有序数组,索引从 0 开始(min=0, max=criteria.length-1);
* - score 答案是概率加权浮点值,可落在两级之间;legend 为 { 索引字符串: 等级描述 }。
*
* 实测响应样例:
* {"model":"jev-1.13.0","answers":{"risk":{"type":"score","score":1.46,"confidence":0.62,
* "legend":{"0":"非常保守",...},"probabilities":{"0":0.0,"1":0.55,...}},
* "hobby":{"type":"choice","choice":"outdoor","confidence":0.99,
* "probabilities":{"social":0.0,"outdoor":0.99,...}}},
* "usage":{"input_tokens":446,"output_tokens":57}}
*/
/** 是非概率问题 */
export interface NoulQuestion {
type: 'noul';
description: string;
instructions: string;
}
/** 选择题(≤255 选项),criteria 为 map */
/** 选择题(≤255 选项),criteria 为 { 选项: 描述 } 的 map */
export interface ChoiceQuestion {
type: 'choice';
description: string;
options: string[];
criteria?: Record<string, string>;
instructions: string;
criteria: Record<string, string>;
}
/** 打分题,criteria 为 2-10 级有序数组(从低到高) */
/** 打分题,criteria 为 2-10 级有序数组(索引 0 起,由低到高) */
export interface ScoreQuestion {
type: 'score';
description: string;
instructions: string;
criteria: string[];
}
@@ -37,8 +48,15 @@ export interface JevRequest {
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 };
| { type: 'choice'; choice: string; probabilities: Record<string, number>; confidence: number }
| {
type: 'score';
/** 概率加权浮点分数,范围 [0, criteria.length-1],可落在两级之间 */
score: number;
confidence: number;
legend: Record<string, string>;
probabilities: Record<string, number>;
};
export interface JevClient {
ask(state: unknown, questions: Record<string, JevQuestion>): Promise<Record<string, JevAnswer>>;
@@ -92,7 +110,7 @@ function prng(seed: number, n: number): number[] {
return out;
}
/** 无 key 开发用:确定性伪随机返分(同一输入永远同一输出,便于联调)。 */
/** 无 key 开发用:确定性伪随机返分(同一输入永远同一输出,输出形状与真实 API 对齐)。 */
export class MockJevClient implements JevClient {
async ask(state: unknown, questions: Record<string, JevQuestion>): Promise<Record<string, JevAnswer>> {
const stateKey = JSON.stringify(state) ?? '';
@@ -103,25 +121,33 @@ export class MockJevClient implements JevClient {
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];
const options = Object.keys(q.criteria);
const r = prng(seed, options.length);
const total = r.reduce((a, b) => a + b, 0) || 1;
const probabilities: Record<string, number> = {};
let choice = 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;
options.forEach((opt, i) => {
probabilities[opt] = Math.round((r[i] / total) * 100) / 100;
if (r[i] > best) {
best = r[i];
choice = opt;
}
});
answers[id] = { type: 'choice', selected, distribution, confidence };
answers[id] = { type: 'choice', choice, probabilities, 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 };
const r = prng(seed, q.criteria.length);
const total = r.reduce((a, b) => a + b, 0) || 1;
const probabilities: Record<string, number> = {};
const legend: Record<string, string> = {};
let score = 0;
q.criteria.forEach((level, i) => {
const p = r[i] / total;
probabilities[String(i)] = Math.round(p * 100) / 100;
legend[String(i)] = level;
score += i * p;
});
answers[id] = { type: 'score', score: Math.round(score * 100) / 100, confidence, legend, probabilities };
}
}
return answers;
@@ -133,7 +159,7 @@ 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;
const key = import.meta.env.VITE_TYPESAFE_API_KEY;
if (key) {
singleton = new HttpJevClient(key);
} else {