feat: 对齐 Jev 真实 API schema、Web Speech 语音输入(优雅降级)、C1-01 语音化身份登记与真 key 冒烟脚本
This commit is contained in:
+53
-27
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user