Files
CyberGril/web/src/domain/model.ts
T

104 lines
3.2 KiB
TypeScript

export type Theme = 'moon' | 'fog';
export type Relation = '朋友' | '恋人';
export interface Identity {
id: string;
name: string;
birth: string;
hour: string;
relation: Relation;
nickname: string;
boundary: string;
}
export interface Settings {
city: string;
appearance: string;
reduced: boolean;
services: Record<string, string>;
}
export interface LifeEvent {
id: string;
title: string;
text: string;
time: string;
place: string;
kind: 'done' | 'now' | 'plan';
shared: boolean;
// When and how she told you ("19:34 告诉你", "你们一起经历的"); only ever present on records she has shared.
told?: string;
memoryIds: string[];
// Real records only: the (already server-trimmed) companion messages in which she told it.
messageIds?: string[];
}
export interface Memory {
id: string;
title: string;
text: string;
group: string;
source: string;
lifeIds: string[];
}
export interface Message {
id: string;
role: 'user' | 'companion';
text: string;
lifeIds: string[];
turnId?: string;
status?: 'pending' | 'failed' | 'sent' | 'cancelled';
proactive?: boolean;
}
export interface DemoState {
version: 1;
identity: Identity;
settings: Settings;
events: LifeEvent[];
memories: Memory[];
messages: Message[];
lastVisit: number;
}
export interface SceneDesc {
mode: 'daily' | 'talk';
activity: string;
time_band: string;
weather: string;
outfit: string;
}
export type ServerEvent = {
t: 'chat.error'; epoch: number; turn_id: string; phi: string; retryable: boolean;
} | {
t: 'chat.delta';
epoch: number;
turn_id: string;
delta: string;
} | {
t: 'chat.final';
epoch: number;
turn_id: string;
utterance: {
text: string;
speech_tags: string[];
};
// Only on a successful real final while the R2 backend is enabled; demo and older servers omit it.
receipt?: Receipt | null;
};
export interface Receipt {
message_id: number;
turn_id: string;
// The epoch of the turn that produced the message; on a replay it can differ from the frame's current epoch.
epoch: number;
token: string;
text_hash: string;
expires_at: number;
}
export const defaultSettings: Settings = { city: '杭州', appearance: 'reference-v1', reduced: false, services: { '对话模型': '', 'ASR': '', 'TTS': '', '视觉观察': '' } };
export function validateCreation(name: string, birth: string, relation: string, now = new Date()): string {
if (!name.trim() || name.trim().length > 20 || (/[<>]/u.test(name) || [...name].some(c => c.charCodeAt(0) < 32)))
return '请为她起一个 1–20 字的名字,不含控制字符或尖括号。';
const d = new Date(birth + 'T00:00:00');
const limit = new Date(now.getFullYear() - 18, now.getMonth(), now.getDate());
if (!/^\d{4}-\d{2}-\d{2}$/.test(birth) || !Number.isFinite(d.getTime()) || d > limit || d.getFullYear() < 1900)
return '请填写有效的出生日期,且已满 18 岁。';
if (!['朋友', '恋人'].includes(relation))
return '请亲自选择你们的关系。';
return '';
}