271 lines
9.3 KiB
TypeScript
271 lines
9.3 KiB
TypeScript
export type UserSyncTheme = 'light' | 'dark' | 'system';
|
|
|
|
export type UserSyncPreferences = {
|
|
theme?: UserSyncTheme;
|
|
language?: string;
|
|
defaultModel?: string;
|
|
defaultProviderAccountId?: string;
|
|
sidebarCollapsed?: boolean;
|
|
devModeUnlocked?: boolean;
|
|
selectedBundles?: string[];
|
|
enabledSkills?: string[];
|
|
disabledSkills?: string[];
|
|
};
|
|
|
|
export type UserSyncProviderAccount = {
|
|
id: string;
|
|
vendorId: string;
|
|
label: string;
|
|
authMode: string;
|
|
baseUrl?: string;
|
|
apiProtocol?: string;
|
|
model?: string;
|
|
fallbackModels?: string[];
|
|
fallbackAccountIds?: string[];
|
|
enabled: boolean;
|
|
isDefault: boolean;
|
|
metadata: Record<string, unknown>;
|
|
updatedAt?: string;
|
|
};
|
|
|
|
export type UserSyncPartner = {
|
|
id: string;
|
|
displayName: string;
|
|
templateRoleId: string;
|
|
description?: string;
|
|
avatarTone?: string;
|
|
avatarDataUrl?: string;
|
|
prompt?: string;
|
|
skillIds?: string[];
|
|
createdAt?: string;
|
|
updatedAt?: string;
|
|
};
|
|
|
|
export type UserSyncUsageAggregate = {
|
|
date: string;
|
|
model?: string;
|
|
providerAccountId?: string;
|
|
inputTokens: number;
|
|
outputTokens: number;
|
|
cacheReadTokens: number;
|
|
cacheWriteTokens: number;
|
|
estimatedCostUsd: number;
|
|
requestCount: number;
|
|
};
|
|
|
|
export type UserSyncBootstrap = {
|
|
preferences: UserSyncPreferences;
|
|
providerAccounts: UserSyncProviderAccount[];
|
|
partners: UserSyncPartner[];
|
|
usageAggregates: UserSyncUsageAggregate[];
|
|
};
|
|
|
|
type UnknownRecord = Record<string, unknown>;
|
|
|
|
function isRecord(value: unknown): value is UnknownRecord {
|
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
}
|
|
|
|
function text(value: unknown): string | undefined {
|
|
if (typeof value !== 'string') return undefined;
|
|
const trimmed = value.trim();
|
|
return trimmed || undefined;
|
|
}
|
|
|
|
function bool(value: unknown): boolean | undefined {
|
|
return typeof value === 'boolean' ? value : undefined;
|
|
}
|
|
|
|
function stringArray(value: unknown): string[] | undefined {
|
|
if (!Array.isArray(value)) return undefined;
|
|
const seen = new Set<string>();
|
|
const result: string[] = [];
|
|
for (const item of value) {
|
|
const clean = text(item);
|
|
if (clean && !seen.has(clean)) {
|
|
seen.add(clean);
|
|
result.push(clean);
|
|
}
|
|
}
|
|
return result.length ? result : undefined;
|
|
}
|
|
|
|
function plainMetadata(value: unknown): Record<string, unknown> {
|
|
if (!isRecord(value)) return {};
|
|
const result: Record<string, unknown> = {};
|
|
for (const [key, entry] of Object.entries(value)) {
|
|
if (entry === undefined || typeof entry === 'function') continue;
|
|
result[key] = entry;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function compact<T extends Record<string, unknown>>(value: T): Partial<T> {
|
|
return Object.fromEntries(
|
|
Object.entries(value).filter(([, entry]) => entry !== undefined),
|
|
) as Partial<T>;
|
|
}
|
|
|
|
function oneOfTheme(value: unknown): UserSyncTheme | undefined {
|
|
return value === 'light' || value === 'dark' || value === 'system' ? value : undefined;
|
|
}
|
|
|
|
function numberValue(value: unknown): number | undefined {
|
|
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
}
|
|
|
|
export function toUserSyncPreferences(
|
|
input: unknown,
|
|
defaultProviderAccountId?: string | null,
|
|
): UserSyncPreferences {
|
|
const record = isRecord(input) ? input : {};
|
|
return compact({
|
|
theme: oneOfTheme(record.theme),
|
|
language: text(record.language),
|
|
defaultModel: text(record.defaultModel ?? record.default_model),
|
|
defaultProviderAccountId: text(defaultProviderAccountId) ?? text(record.defaultProviderAccountId ?? record.default_provider_account_id),
|
|
sidebarCollapsed: bool(record.sidebarCollapsed ?? record.sidebar_collapsed),
|
|
devModeUnlocked: bool(record.devModeUnlocked ?? record.dev_mode_unlocked),
|
|
selectedBundles: stringArray(record.selectedBundles ?? record.selected_bundles),
|
|
enabledSkills: stringArray(record.enabledSkills ?? record.enabled_skills),
|
|
disabledSkills: stringArray(record.disabledSkills ?? record.disabled_skills),
|
|
});
|
|
}
|
|
|
|
export function toUserSyncProviderAccount(input: unknown): UserSyncProviderAccount {
|
|
const record = isRecord(input) ? input : {};
|
|
return compact({
|
|
id: text(record.id),
|
|
vendorId: text(record.vendorId ?? record.vendor_id),
|
|
label: text(record.label),
|
|
authMode: text(record.authMode ?? record.auth_mode),
|
|
baseUrl: text(record.baseUrl ?? record.base_url),
|
|
apiProtocol: text(record.apiProtocol ?? record.api_protocol),
|
|
model: text(record.model),
|
|
fallbackModels: stringArray(record.fallbackModels ?? record.fallback_models),
|
|
fallbackAccountIds: stringArray(record.fallbackAccountIds ?? record.fallback_account_ids),
|
|
enabled: bool(record.enabled) ?? true,
|
|
isDefault: bool(record.isDefault ?? record.is_default) ?? false,
|
|
metadata: plainMetadata(record.metadata),
|
|
updatedAt: text(record.updatedAt ?? record.updated_at),
|
|
}) as UserSyncProviderAccount;
|
|
}
|
|
|
|
export function toUserSyncPartner(input: unknown): UserSyncPartner {
|
|
const record = isRecord(input) ? input : {};
|
|
return compact({
|
|
id: text(record.id),
|
|
displayName: text(record.displayName ?? record.display_name),
|
|
// Keep the wire field for backend compatibility, but no longer preserve
|
|
// legacy role-template ids in the desktop state.
|
|
templateRoleId: 'custom',
|
|
description: text(record.description),
|
|
avatarTone: text(record.avatarTone ?? record.avatar_tone),
|
|
avatarDataUrl: text(record.avatarDataUrl ?? record.avatar_data_url),
|
|
prompt: text(record.prompt),
|
|
skillIds: stringArray(record.skillIds ?? record.skill_ids),
|
|
createdAt: text(record.createdAt ?? record.created_at),
|
|
updatedAt: text(record.updatedAt ?? record.updated_at),
|
|
}) as UserSyncPartner;
|
|
}
|
|
|
|
export function toServerUserSyncPreferences(input: unknown): UnknownRecord {
|
|
const preferences = toUserSyncPreferences(input);
|
|
return compact({
|
|
theme: preferences.theme,
|
|
language: preferences.language,
|
|
default_model: preferences.defaultModel,
|
|
default_provider_account_id: preferences.defaultProviderAccountId,
|
|
sidebar_collapsed: preferences.sidebarCollapsed,
|
|
dev_mode_unlocked: preferences.devModeUnlocked,
|
|
selected_bundles: preferences.selectedBundles,
|
|
enabled_skills: preferences.enabledSkills,
|
|
disabled_skills: preferences.disabledSkills,
|
|
});
|
|
}
|
|
|
|
export function toServerUserSyncProviderAccount(input: unknown): UnknownRecord {
|
|
const account = toUserSyncProviderAccount(input);
|
|
return compact({
|
|
id: account.id,
|
|
vendor_id: account.vendorId,
|
|
label: account.label,
|
|
auth_mode: account.authMode,
|
|
base_url: account.baseUrl,
|
|
api_protocol: account.apiProtocol,
|
|
model: account.model,
|
|
fallback_models: account.fallbackModels,
|
|
fallback_account_ids: account.fallbackAccountIds,
|
|
enabled: account.enabled,
|
|
is_default: account.isDefault,
|
|
metadata: account.metadata,
|
|
updated_at: account.updatedAt,
|
|
});
|
|
}
|
|
|
|
export function toServerUserSyncPartner(input: unknown): UnknownRecord {
|
|
const partner = toUserSyncPartner(input);
|
|
return compact({
|
|
id: partner.id,
|
|
display_name: partner.displayName,
|
|
template_role_id: partner.templateRoleId,
|
|
description: partner.description,
|
|
avatar_tone: partner.avatarTone,
|
|
avatar_data_url: partner.avatarDataUrl,
|
|
prompt: partner.prompt,
|
|
skill_ids: partner.skillIds,
|
|
created_at: partner.createdAt,
|
|
updated_at: partner.updatedAt,
|
|
});
|
|
}
|
|
|
|
export function normalizeUserSyncBootstrap(input: unknown): UserSyncBootstrap {
|
|
const record = isRecord(input) ? input : {};
|
|
const rawProviderAccounts = record.providerAccounts ?? record.provider_accounts;
|
|
const rawPartners = record.partners;
|
|
const rawUsageAggregates = record.usageAggregates ?? record.usage_aggregates;
|
|
const providerAccounts = Array.isArray(rawProviderAccounts)
|
|
? rawProviderAccounts.map(toUserSyncProviderAccount).filter(hasProviderIdentity)
|
|
: [];
|
|
const partners = Array.isArray(rawPartners)
|
|
? rawPartners.map(toUserSyncPartner).filter(hasPartnerIdentity)
|
|
: [];
|
|
const usageAggregates = Array.isArray(rawUsageAggregates)
|
|
? rawUsageAggregates.map(toUserSyncUsageAggregate).filter(hasUsageDate)
|
|
: [];
|
|
|
|
return {
|
|
preferences: toUserSyncPreferences(record.preferences),
|
|
providerAccounts,
|
|
partners,
|
|
usageAggregates,
|
|
};
|
|
}
|
|
|
|
function hasProviderIdentity(account: UserSyncProviderAccount): boolean {
|
|
return Boolean(account.id && account.vendorId && account.label && account.authMode);
|
|
}
|
|
|
|
function hasPartnerIdentity(partner: UserSyncPartner): boolean {
|
|
return Boolean(partner.id && partner.displayName);
|
|
}
|
|
|
|
function hasUsageDate(aggregate: UserSyncUsageAggregate): boolean {
|
|
return Boolean(aggregate.date);
|
|
}
|
|
|
|
function toUserSyncUsageAggregate(input: unknown): UserSyncUsageAggregate {
|
|
const record = isRecord(input) ? input : {};
|
|
return compact({
|
|
date: text(record.date),
|
|
model: text(record.model),
|
|
providerAccountId: text(record.providerAccountId ?? record.provider_account_id),
|
|
inputTokens: numberValue(record.inputTokens ?? record.input_tokens) ?? 0,
|
|
outputTokens: numberValue(record.outputTokens ?? record.output_tokens) ?? 0,
|
|
cacheReadTokens: numberValue(record.cacheReadTokens ?? record.cache_read_tokens) ?? 0,
|
|
cacheWriteTokens: numberValue(record.cacheWriteTokens ?? record.cache_write_tokens) ?? 0,
|
|
estimatedCostUsd: numberValue(record.estimatedCostUsd ?? record.estimated_cost_usd) ?? 0,
|
|
requestCount: numberValue(record.requestCount ?? record.request_count) ?? 0,
|
|
}) as UserSyncUsageAggregate;
|
|
}
|