Refactor proxy-core surfaces and transformer boundaries (#163)

* refactor: add canonical request contracts

* test: harden transformer boundary contracts

* refactor: adapt protocol transformers to canonical contract

* refactor: add cli profile registry

* refactor: extract provider profiles

* refactor: add conductor and provider executors

* refactor: add models surface

* refactor: migrate responses surface

* refactor: migrate chat surface

* refactor: migrate gemini surface

* refactor: migrate file surfaces

* test: align runtime executor undici mock

* fix: preserve compact SSE closeout payloads

* feat: support all-api-hub v2 backup imports

* fix: aggregate codex oauth SSE final payloads

* fix: harden responses SSE failure handling
This commit is contained in:
Cita
2026-03-19 16:07:56 +08:00
committed by GitHub
parent c0c993f448
commit 24483c28eb
75 changed files with 8333 additions and 3574 deletions
+285 -1
View File
@@ -1,4 +1,4 @@
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
import { mkdtempSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
@@ -235,6 +235,290 @@ describe('backupService', () => {
expect(settings.some((row) => row.key === 'legacy_preferences_ref_v2')).toBe(true);
});
it('imports ALL-API-Hub V2 backups into native offline connections and summaries', async () => {
const fetchSpy = vi.spyOn(globalThis, 'fetch');
fetchSpy.mockImplementation(async () => {
throw new Error('network access should not happen during offline import');
});
try {
const payload = {
version: '2.0',
timestamp: Date.now(),
accounts: {
accounts: [
{
id: 'managed-account',
site_url: 'https://newapi.example.com',
site_type: 'new-api',
site_name: 'Managed Site',
authType: 'access_token',
account_info: {
id: 7788,
username: 'managed-user',
access_token: 'managed-session-token',
quota: 100000,
today_quota_consumption: 50000,
},
checkIn: {
autoCheckInEnabled: true,
},
created_at: '2026-02-01T00:00:00.000Z',
updated_at: '2026-02-02T00:00:00.000Z',
},
{
id: 'cookie-account',
site_url: 'https://onehub.example.com',
site_type: 'one-hub',
site_name: 'Cookie Site',
username: 'cookie-user',
authType: 'cookie',
cookieAuth: {
sessionCookie: 'sid=cookie-session',
},
checkIn: {
autoCheckInEnabled: false,
},
created_at: '2026-02-03T00:00:00.000Z',
updated_at: '2026-02-04T00:00:00.000Z',
},
{
id: 'direct-openai-account',
site_url: 'https://api.openai.com',
site_type: 'openai',
site_name: 'OpenAI Direct',
username: 'openai-account',
authType: 'access_token',
account_info: {
username: 'openai-account',
access_token: 'sk-openai-account',
},
created_at: '2026-02-05T00:00:00.000Z',
updated_at: '2026-02-06T00:00:00.000Z',
},
{
id: 'sub2api-account',
site_url: 'https://sub2api.example.com',
site_type: 'sub2api',
site_name: 'Sub2API',
authType: 'access_token',
account_info: {
id: 99,
username: 'sub2-user',
access_token: 'sub2-session-token',
},
sub2apiAuth: {
refreshToken: 'sub2-refresh-token',
tokenExpiresAt: 1735689600000,
},
checkIn: {
autoCheckInEnabled: true,
},
created_at: '2026-02-07T00:00:00.000Z',
updated_at: '2026-02-08T00:00:00.000Z',
},
{
id: 'skipped-none-account',
site_url: 'https://skip-none.example.com',
site_type: 'new-api',
site_name: 'Skip None',
authType: 'none',
username: 'skip-none-user',
created_at: '2026-02-09T00:00:00.000Z',
updated_at: '2026-02-10T00:00:00.000Z',
},
{
id: 'skipped-empty-account',
site_url: 'https://skip-empty.example.com',
site_type: 'new-api',
site_name: 'Skip Empty',
authType: 'access_token',
account_info: {
username: 'skip-empty-user',
},
created_at: '2026-02-11T00:00:00.000Z',
updated_at: '2026-02-12T00:00:00.000Z',
},
],
bookmarks: [
{
id: 'bookmark-1',
name: 'Ignored Bookmark',
url: 'https://bookmark.example.com',
},
],
pinnedAccountIds: ['direct-openai-account'],
orderedAccountIds: ['managed-account', 'cookie-account', 'direct-openai-account'],
last_updated: 1735689600000,
},
preferences: {
language: 'zh-CN',
},
channelConfigs: {
bySite: {
demo: { enabled: true },
},
},
tagStore: {
version: 1,
tagsById: {},
},
apiCredentialProfiles: {
version: 2,
profiles: [
{
id: 'profile-openai',
name: 'OpenAI Profile',
apiType: 'openai',
baseUrl: 'https://api.openai.com/v1',
apiKey: 'sk-profile-openai',
tagIds: [],
notes: '',
createdAt: 1735689601000,
updatedAt: 1735689602000,
},
{
id: 'profile-anthropic',
name: 'Claude Profile',
apiType: 'anthropic',
baseUrl: 'https://api.anthropic.com/v1',
apiKey: 'sk-profile-claude',
tagIds: [],
notes: '',
createdAt: 1735689603000,
updatedAt: 1735689604000,
},
{
id: 'profile-gemini',
name: 'Gemini Profile',
apiType: 'google',
baseUrl: 'https://generativelanguage.googleapis.com/v1beta',
apiKey: 'gemini-profile-key',
tagIds: [],
notes: '',
createdAt: 1735689605000,
updatedAt: 1735689606000,
},
{
id: 'profile-compat-fallback',
name: 'Compat Profile',
apiType: 'openai-compatible',
baseUrl: 'https://compat.example.com/v1',
apiKey: 'sk-compat-profile',
tagIds: [],
notes: '',
createdAt: 1735689607000,
updatedAt: 1735689608000,
},
],
lastUpdated: 1735689609000,
},
} as Record<string, unknown>;
const result = await backupService.importBackup(payload);
const summary = (result as any).summary;
const warnings = (result as any).warnings;
expect(result.allImported).toBe(true);
expect(result.sections.accounts).toBe(true);
expect(result.sections.preferences).toBe(true);
expect(summary).toMatchObject({
importedAccounts: 4,
importedProfiles: 4,
importedApiKeyConnections: 5,
importedSites: 7,
skippedAccounts: 2,
});
expect(summary.ignoredSections).toEqual(
expect.arrayContaining(['accounts.bookmarks', 'channelConfigs', 'tagStore']),
);
expect(warnings).toEqual(
expect.arrayContaining([
expect.stringContaining('skipped-none-account'),
expect.stringContaining('skipped-empty-account'),
]),
);
const sites = await db.select().from(schema.sites).all();
const accounts = await db.select().from(schema.accounts).all();
const accountTokens = await db.select().from(schema.accountTokens).all();
const settings = await db.select().from(schema.settings).all();
expect(fetchSpy).not.toHaveBeenCalled();
expect(sites).toHaveLength(7);
expect(accounts).toHaveLength(8);
expect(accountTokens).toHaveLength(5);
expect(settings.some((row) => row.key === 'legacy_preferences_ref_v2')).toBe(true);
expect(settings.some((row) => row.key === 'legacy_channel_configs_ref_v2')).toBe(true);
expect(settings.some((row) => row.key === 'legacy_tag_store_ref_v2')).toBe(true);
expect(settings.some((row) => row.key === 'legacy_api_credential_profiles_ref_v2')).toBe(false);
const managedAccount = accounts.find((row) => row.username === 'managed-user');
const cookieAccount = accounts.find((row) => row.username === 'cookie-user');
const openAiAccount = accounts.find((row) => row.username === 'openai-account');
const sub2apiAccount = accounts.find((row) => row.username === 'sub2-user');
const openAiProfileAccount = accounts.find((row) => row.username === 'OpenAI Profile');
const claudeProfileAccount = accounts.find((row) => row.username === 'Claude Profile');
const geminiProfileAccount = accounts.find((row) => row.username === 'Gemini Profile');
const compatProfileAccount = accounts.find((row) => row.username === 'Compat Profile');
expect(managedAccount?.accessToken).toBe('managed-session-token');
expect(managedAccount?.apiToken).toBeNull();
expect(managedAccount?.checkinEnabled).toBe(true);
expect(JSON.parse(managedAccount?.extraConfig || '{}')).toMatchObject({
credentialMode: 'session',
platformUserId: 7788,
});
expect(cookieAccount?.accessToken).toBe('sid=cookie-session');
expect(cookieAccount?.checkinEnabled).toBe(false);
expect(JSON.parse(cookieAccount?.extraConfig || '{}')).toMatchObject({
credentialMode: 'session',
});
expect(openAiAccount?.accessToken).toBe('');
expect(openAiAccount?.apiToken).toBe('sk-openai-account');
expect(openAiAccount?.checkinEnabled).toBe(false);
expect(JSON.parse(openAiAccount?.extraConfig || '{}')).toMatchObject({
credentialMode: 'apikey',
});
expect(sub2apiAccount?.accessToken).toBe('sub2-session-token');
expect(JSON.parse(sub2apiAccount?.extraConfig || '{}')).toMatchObject({
credentialMode: 'session',
platformUserId: 99,
sub2apiAuth: {
refreshToken: 'sub2-refresh-token',
tokenExpiresAt: 1735689600000,
},
});
expect(openAiProfileAccount?.accessToken).toBe('');
expect(openAiProfileAccount?.apiToken).toBe('sk-profile-openai');
expect(JSON.parse(openAiProfileAccount?.extraConfig || '{}')).toMatchObject({
credentialMode: 'apikey',
});
expect(claudeProfileAccount?.apiToken).toBe('sk-profile-claude');
expect(geminiProfileAccount?.apiToken).toBe('gemini-profile-key');
expect(compatProfileAccount?.apiToken).toBe('sk-compat-profile');
const openAiSite = sites.find((row) => row.platform === 'openai' && row.url === 'https://api.openai.com');
expect(openAiSite).toBeTruthy();
expect(accounts.filter((row) => row.siteId === openAiSite?.id)).toHaveLength(2);
expect(accountTokens.map((row) => row.token).sort()).toEqual([
'gemini-profile-key',
'sk-compat-profile',
'sk-openai-account',
'sk-profile-claude',
'sk-profile-openai',
]);
expect(accountTokens.every((row) => row.name === 'default' && row.isDefault && row.source === 'legacy')).toBe(true);
} finally {
fetchSpy.mockRestore();
}
});
it('backfills oauth columns from extraConfig when importing older backups', async () => {
const payload = {
timestamp: Date.now(),
+418 -3
View File
@@ -2,6 +2,7 @@ import { asc } from 'drizzle-orm';
import cron from 'node-cron';
import { db, schema } from '../db/index.js';
import { upsertSetting } from '../db/upsertSetting.js';
import { mergeAccountExtraConfig } from './accountExtraConfig.js';
import { getOauthInfoFromExtraConfig } from './oauth/oauthAccount.js';
const BACKUP_VERSION = '2.0';
@@ -60,6 +61,15 @@ interface BackupImportResult {
preferences: boolean;
};
appliedSettings: Array<{ key: string; value: unknown }>;
summary?: {
importedSites: number;
importedAccounts: number;
importedProfiles: number;
importedApiKeyConnections: number;
skippedAccounts: number;
ignoredSections: string[];
};
warnings?: string[];
}
const EXCLUDED_SETTING_KEYS = new Set<string>([
@@ -67,6 +77,48 @@ const EXCLUDED_SETTING_KEYS = new Set<string>([
'auth_token',
]);
const DIRECT_API_PLATFORMS = new Set([
'openai',
'claude',
'gemini',
'cliproxyapi',
'codex',
'gemini-cli',
'antigravity',
]);
const IMPORT_PLATFORM_ALIASES: Record<string, string> = {
anyrouter: 'anyrouter',
'wong-gongyi': 'new-api',
'vo-api': 'new-api',
'super-api': 'new-api',
'rix-api': 'new-api',
'neo-api': 'new-api',
newapi: 'new-api',
'new api': 'new-api',
'new-api': 'new-api',
oneapi: 'one-api',
'one api': 'one-api',
'one-api': 'one-api',
onehub: 'one-hub',
'one-hub': 'one-hub',
donehub: 'done-hub',
'done-hub': 'done-hub',
veloera: 'veloera',
sub2api: 'sub2api',
openai: 'openai',
anthropic: 'claude',
claude: 'claude',
google: 'gemini',
gemini: 'gemini',
cliproxyapi: 'cliproxyapi',
cpa: 'cliproxyapi',
'cli-proxy-api': 'cliproxyapi',
codex: 'codex',
'gemini-cli': 'gemini-cli',
antigravity: 'antigravity',
};
function isRecord(value: unknown): value is Record<string, unknown> {
return !!value && typeof value === 'object' && !Array.isArray(value);
}
@@ -141,6 +193,354 @@ function normalizeLegacyPlatform(raw: string): string {
return 'new-api';
}
function normalizeOriginUrl(raw: string): string {
const trimmed = raw.trim();
if (!trimmed) return '';
try {
return new URL(trimmed).origin;
} catch {
return trimmed.replace(/\/+$/, '');
}
}
function detectLocalPlatformByUrlHint(url: string): string | undefined {
const normalized = url.trim().toLowerCase();
if (!normalized) return undefined;
if (normalized.includes('api.openai.com')) return 'openai';
if (normalized.includes('chatgpt.com/backend-api/codex')) return 'codex';
if (normalized.includes('api.anthropic.com') || normalized.includes('anthropic.com/v1')) return 'claude';
if (
normalized.includes('generativelanguage.googleapis.com')
|| normalized.includes('googleapis.com/v1beta/openai')
|| normalized.includes('gemini.google.com')
) {
return 'gemini';
}
if (normalized.includes('cloudcode-pa.googleapis.com')) return 'gemini-cli';
if (normalized.includes('anyrouter')) return 'anyrouter';
if (normalized.includes('donehub') || normalized.includes('done-hub')) return 'done-hub';
if (normalized.includes('onehub') || normalized.includes('one-hub')) return 'one-hub';
if (normalized.includes('veloera')) return 'veloera';
if (normalized.includes('sub2api')) return 'sub2api';
if (normalized.includes('127.0.0.1:8317') || normalized.includes('localhost:8317')) return 'cliproxyapi';
return undefined;
}
function resolveImportedPlatform(rawPlatform: unknown, rawUrl: string): string {
const normalizedPlatform = typeof rawPlatform === 'string'
? IMPORT_PLATFORM_ALIASES[rawPlatform.trim().toLowerCase()]
: undefined;
if (normalizedPlatform) return normalizedPlatform;
const urlHint = detectLocalPlatformByUrlHint(rawUrl);
if (urlHint) return urlHint;
return normalizeLegacyPlatform(asString(rawPlatform));
}
function resolveImportedProfilePlatform(apiType: unknown, baseUrl: string): string {
const normalizedType = asString(apiType).toLowerCase();
if (normalizedType === 'openai') return 'openai';
if (normalizedType === 'anthropic') return 'claude';
if (normalizedType === 'google') return 'gemini';
if (normalizedType === 'openai-compatible') {
return detectLocalPlatformByUrlHint(baseUrl) || 'openai';
}
return detectLocalPlatformByUrlHint(baseUrl) || 'openai';
}
function pushDefaultImportedToken(
rows: AccountTokenRow[],
nextId: () => number,
accountId: number,
token: string | null,
createdAt: string,
updatedAt: string,
) {
if (!token) return;
rows.push({
id: nextId(),
accountId,
name: 'default',
token,
tokenGroup: 'default',
valueStatus: 'ready',
source: 'legacy',
enabled: true,
isDefault: true,
createdAt,
updatedAt,
});
}
function buildAllApiHubV2AccountsSection(data: RawBackupData): {
section: AccountsBackupSection;
summary: NonNullable<BackupImportResult['summary']>;
warnings: string[];
} | null {
const accountsContainer = isRecord(data.accounts) ? data.accounts : null;
if (!accountsContainer || !Array.isArray(accountsContainer.accounts)) return null;
const looksLikeV2 =
(typeof data.version === 'string' && data.version.startsWith('2'))
|| Object.prototype.hasOwnProperty.call(accountsContainer, 'last_updated')
|| Array.isArray(accountsContainer.bookmarks)
|| Array.isArray(accountsContainer.pinnedAccountIds)
|| Array.isArray(accountsContainer.orderedAccountIds)
|| (isRecord(data.apiCredentialProfiles) && Array.isArray(data.apiCredentialProfiles.profiles));
if (!looksLikeV2) return null;
const section: AccountsBackupSection = {
sites: [],
accounts: [],
accountTokens: [],
tokenRoutes: [],
routeChannels: [],
routeGroupSources: [],
};
const siteIdByKey = new Map<string, number>();
let nextSiteId = 1;
let nextAccountId = 1;
let nextTokenId = 1;
const warnings: string[] = [];
const ignoredSections: string[] = [];
let importedAccounts = 0;
let importedProfiles = 0;
let importedApiKeyConnections = 0;
let skippedAccounts = 0;
const nextToken = () => nextTokenId++;
const ensureSite = (input: {
platform: string;
url: string;
name?: string;
createdAt: string;
updatedAt: string;
}) => {
const normalizedUrl = normalizeOriginUrl(input.url);
if (!normalizedUrl) return null;
const key = `${input.platform}::${normalizedUrl}`;
const existingId = siteIdByKey.get(key);
if (existingId) return existingId;
const siteId = nextSiteId++;
siteIdByKey.set(key, siteId);
section.sites.push({
id: siteId,
name: asString(input.name) || normalizedUrl,
url: normalizedUrl,
externalCheckinUrl: null,
platform: input.platform,
proxyUrl: null,
useSystemProxy: false,
customHeaders: null,
status: 'active',
isPinned: false,
sortOrder: section.sites.length,
globalWeight: 1,
apiKey: null,
createdAt: input.createdAt,
updatedAt: input.updatedAt,
});
return siteId;
};
const addIgnoredSection = (name: string, active: boolean) => {
if (active && !ignoredSections.includes(name)) ignoredSections.push(name);
};
addIgnoredSection('accounts.bookmarks', Array.isArray(accountsContainer.bookmarks) && accountsContainer.bookmarks.length > 0);
addIgnoredSection('channelConfigs', isRecord(data.channelConfigs));
addIgnoredSection('tagStore', isRecord(data.tagStore));
for (const row of accountsContainer.accounts) {
if (!isRecord(row)) continue;
const createdAt = toIsoString(row.created_at);
const updatedAt = toIsoString(row.updated_at);
const siteUrl = normalizeOriginUrl(asString(row.site_url));
const siteName = asString(row.site_name) || siteUrl;
const platform = resolveImportedPlatform(row.site_type, siteUrl);
const authType = asString(row.authType).toLowerCase();
const accountInfo = isRecord(row.account_info) ? row.account_info : {};
const cookieAuth = isRecord(row.cookieAuth) ? row.cookieAuth : {};
const sub2apiAuth = isRecord(row.sub2apiAuth) ? row.sub2apiAuth : {};
const rawAccountId = asString(row.id) || asString(row.username) || siteName || `account-${nextAccountId}`;
const username = asString(accountInfo.username) || asString(row.username) || rawAccountId;
const platformUserId = asNumber(accountInfo.id, 0);
const checkin = isRecord(row.checkIn) ? row.checkIn : {};
const accessTokenCandidate = asString(accountInfo.access_token) || asString(row.access_token);
const cookieSession = asString(cookieAuth.sessionCookie);
const isDirectApiPlatform = DIRECT_API_PLATFORMS.has(platform);
let accessToken = '';
let apiToken: string | null = null;
let credentialMode: 'session' | 'apikey' | null = null;
if (authType === 'cookie') {
if (!cookieSession) {
skippedAccounts += 1;
warnings.push(`跳过 ALL-API-Hub 账号 ${rawAccountId}:cookieAuth.sessionCookie 缺失`);
continue;
}
accessToken = cookieSession;
credentialMode = 'session';
} else if (authType === 'access_token') {
if (!accessTokenCandidate) {
skippedAccounts += 1;
warnings.push(`跳过 ALL-API-Hub 账号 ${rawAccountId}:access_token 缺失`);
continue;
}
if (isDirectApiPlatform) {
accessToken = '';
apiToken = accessTokenCandidate;
credentialMode = 'apikey';
} else {
accessToken = accessTokenCandidate;
credentialMode = 'session';
}
} else {
skippedAccounts += 1;
warnings.push(`跳过 ALL-API-Hub 账号 ${rawAccountId}:authType=${authType || 'unknown'} 不支持离线迁移`);
continue;
}
const siteId = ensureSite({
platform,
url: siteUrl,
name: siteName,
createdAt,
updatedAt,
});
if (!siteId) {
skippedAccounts += 1;
warnings.push(`跳过 ALL-API-Hub 账号 ${rawAccountId}:site_url 无效`);
continue;
}
const importedBalance = normalizeLegacyQuota(accountInfo.quota);
const importedUsed = normalizeLegacyQuota(accountInfo.today_quota_consumption);
const importedQuota = importedBalance + importedUsed;
const extraConfigPatch: Record<string, unknown> = {
credentialMode,
source: 'all-api-hub',
};
if (platformUserId > 0) {
extraConfigPatch.platformUserId = platformUserId;
}
const refreshToken = asString(sub2apiAuth.refreshToken);
const tokenExpiresAt = asNumber(sub2apiAuth.tokenExpiresAt, 0);
if (refreshToken) {
extraConfigPatch.sub2apiAuth = tokenExpiresAt > 0
? { refreshToken, tokenExpiresAt }
: { refreshToken };
}
const accountId = nextAccountId++;
section.accounts.push({
id: accountId,
siteId,
username,
accessToken,
apiToken,
oauthProvider: null,
oauthAccountKey: null,
oauthProjectId: null,
balance: importedBalance,
balanceUsed: importedUsed,
quota: importedQuota > 0 ? importedQuota : importedBalance,
unitCost: null,
valueScore: 0,
status: asBoolean(row.disabled, false) ? 'disabled' : 'active',
isPinned: false,
sortOrder: section.accounts.length,
checkinEnabled: credentialMode === 'session' ? asBoolean(checkin.autoCheckInEnabled, true) : false,
lastCheckinAt: null,
lastBalanceRefresh: null,
extraConfig: mergeAccountExtraConfig(undefined, extraConfigPatch),
createdAt,
updatedAt,
});
pushDefaultImportedToken(section.accountTokens, nextToken, accountId, apiToken, createdAt, updatedAt);
if (credentialMode === 'apikey') importedApiKeyConnections += 1;
importedAccounts += 1;
}
const profilesContainer = isRecord(data.apiCredentialProfiles) ? data.apiCredentialProfiles : null;
const profiles = Array.isArray(profilesContainer?.profiles) ? profilesContainer.profiles : [];
for (const profile of profiles) {
if (!isRecord(profile)) continue;
const baseUrl = normalizeOriginUrl(asString(profile.baseUrl));
const apiKey = asString(profile.apiKey);
if (!baseUrl || !apiKey) {
warnings.push(`跳过 ALL-API-Hub API 凭据 ${asString(profile.id) || asString(profile.name) || 'unknown'}:baseUrl 或 apiKey 缺失`);
continue;
}
const createdAt = toIsoString(profile.createdAt);
const updatedAt = toIsoString(profile.updatedAt);
const platform = resolveImportedProfilePlatform(profile.apiType, asString(profile.baseUrl));
const siteId = ensureSite({
platform,
url: baseUrl,
name: baseUrl,
createdAt,
updatedAt,
});
if (!siteId) continue;
const accountId = nextAccountId++;
section.accounts.push({
id: accountId,
siteId,
username: asString(profile.name) || asString(profile.id) || baseUrl,
accessToken: '',
apiToken: apiKey,
oauthProvider: null,
oauthAccountKey: null,
oauthProjectId: null,
balance: 0,
balanceUsed: 0,
quota: 0,
unitCost: null,
valueScore: 0,
status: 'active',
isPinned: false,
sortOrder: section.accounts.length,
checkinEnabled: false,
lastCheckinAt: null,
lastBalanceRefresh: null,
extraConfig: mergeAccountExtraConfig(undefined, {
credentialMode: 'apikey',
source: 'all-api-hub-profile',
importedProfileId: asString(profile.id) || undefined,
}),
createdAt,
updatedAt,
});
pushDefaultImportedToken(section.accountTokens, nextToken, accountId, apiKey, createdAt, updatedAt);
importedApiKeyConnections += 1;
importedProfiles += 1;
}
return {
section,
summary: {
importedSites: section.sites.length,
importedAccounts,
importedProfiles,
importedApiKeyConnections,
skippedAccounts,
ignoredSections,
},
warnings,
};
}
function buildAccountsSectionFromRefBackup(data: RawBackupData): AccountsBackupSection | null {
const accountsContainer = isRecord(data.accounts) ? data.accounts : null;
const rows = Array.isArray(accountsContainer?.accounts) ? accountsContainer.accounts : null;
@@ -285,9 +685,6 @@ function buildPreferencesSectionFromRefBackup(data: RawBackupData): PreferencesB
if (isRecord(data.channelConfigs)) {
settings.push({ key: 'legacy_channel_configs_ref_v2', value: data.channelConfigs });
}
if (isRecord(data.apiCredentialProfiles)) {
settings.push({ key: 'legacy_api_credential_profiles_ref_v2', value: data.apiCredentialProfiles });
}
if (isRecord(data.tagStore)) {
settings.push({ key: 'legacy_tag_store_ref_v2', value: data.tagStore });
}
@@ -457,6 +854,9 @@ function detectAccountsSection(data: RawBackupData): AccountsBackupSection | nul
if (legacyNested) return legacyNested;
}
const allApiHubV2 = buildAllApiHubV2AccountsSection(data);
if (allApiHubV2) return allApiHubV2.section;
const refFormat = buildAccountsSectionFromRefBackup(data);
if (refFormat) return refFormat;
@@ -483,6 +883,18 @@ function detectPreferencesSection(data: RawBackupData): PreferencesBackupSection
return null;
}
function detectImportMetadata(data: RawBackupData): {
summary?: BackupImportResult['summary'];
warnings?: string[];
} {
const allApiHubV2 = buildAllApiHubV2AccountsSection(data);
if (!allApiHubV2) return {};
return {
summary: allApiHubV2.summary,
warnings: allApiHubV2.warnings.length > 0 ? allApiHubV2.warnings : undefined,
};
}
async function importAccountsSection(section: AccountsBackupSection): Promise<void> {
await db.transaction(async (tx) => {
await tx.delete(schema.routeChannels).run();
@@ -634,6 +1046,7 @@ export async function importBackup(data: RawBackupData): Promise<BackupImportRes
const accountsSection = detectAccountsSection(data);
const preferencesSection = detectPreferencesSection(data);
const importMetadata = detectImportMetadata(data);
const type = typeof data.type === 'string' ? data.type : '';
const accountsRequested = type === 'accounts' || !!accountsSection;
@@ -670,5 +1083,7 @@ export async function importBackup(data: RawBackupData): Promise<BackupImportRes
preferences: preferencesImported,
},
appliedSettings,
summary: importMetadata.summary,
warnings: importMetadata.warnings,
};
}
+32 -3
View File
@@ -1,5 +1,7 @@
const DAY_MS = 24 * 60 * 60 * 1000;
export type StoredUtcDateTimeInput = string | number | Date | null | undefined;
function pad2(value: number): string {
return String(value).padStart(2, '0');
}
@@ -20,11 +22,38 @@ export function formatUtcSqlDateTime(value: Date): string {
return `${value.getUTCFullYear()}-${pad2(value.getUTCMonth() + 1)}-${pad2(value.getUTCDate())} ${pad2(value.getUTCHours())}:${pad2(value.getUTCMinutes())}:${pad2(value.getUTCSeconds())}`;
}
export function parseStoredUtcDateTime(raw: string | null | undefined): Date | null {
if (!raw) return null;
function parseEpochDateTime(raw: number): Date | null {
if (!Number.isFinite(raw)) return null;
if (raw > 1_000_000_000_000) {
const parsed = new Date(Math.round(raw));
return Number.isNaN(parsed.getTime()) ? null : parsed;
}
if (raw > 1_000_000_000) {
const parsed = new Date(Math.round(raw * 1000));
return Number.isNaN(parsed.getTime()) ? null : parsed;
}
return null;
}
export function parseStoredUtcDateTime(raw: StoredUtcDateTimeInput): Date | null {
if (raw == null) return null;
if (raw instanceof Date) {
return Number.isNaN(raw.getTime()) ? null : new Date(raw.getTime());
}
if (typeof raw === 'number') {
return parseEpochDateTime(raw);
}
if (typeof raw !== 'string') return null;
const text = raw.trim();
if (!text) return null;
const numeric = Number(text);
if (Number.isFinite(numeric)) {
const parsedNumeric = parseEpochDateTime(numeric);
if (parsedNumeric) return parsedNumeric;
}
let parsed: Date;
if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(text)) {
parsed = new Date(`${text.replace(' ', 'T')}Z`);
@@ -36,7 +65,7 @@ export function parseStoredUtcDateTime(raw: string | null | undefined): Date | n
return parsed;
}
export function toLocalDayKeyFromStoredUtc(raw: string | null | undefined): string | null {
export function toLocalDayKeyFromStoredUtc(raw: StoredUtcDateTimeInput): string | null {
const parsed = parseStoredUtcDateTime(raw);
if (!parsed) return null;
return formatLocalDate(parsed);
@@ -148,4 +148,35 @@ describe('buildModelAnalysis', () => {
calls: 1,
});
});
it('accepts Date-backed createdAt values from external database drivers', () => {
const logs = [
{
createdAt: new Date('2026-02-24T12:30:00.000Z') as unknown as string,
modelActual: 'gpt-4o',
modelRequested: null,
status: 'success',
latencyMs: 250,
totalTokens: 800,
estimatedCost: 1.6,
},
];
const result = buildModelAnalysis(logs, {
now: new Date('2026-02-24T12:00:00.000Z'),
days: 1,
});
expect(result.totals).toEqual({
calls: 1,
tokens: 800,
spend: 1.6,
});
expect(result.callRanking[0]).toMatchObject({
model: 'gpt-4o',
calls: 1,
successRate: 100,
avgLatencyMs: 250,
});
});
});
+2 -2
View File
@@ -1,9 +1,9 @@
import { formatLocalDate, parseStoredUtcDateTime } from './localTimeService.js';
import { formatLocalDate, parseStoredUtcDateTime, type StoredUtcDateTimeInput } from './localTimeService.js';
const DAY_MS = 24 * 60 * 60 * 1000;
interface ProxyLogLike {
createdAt: string | null;
createdAt: StoredUtcDateTimeInput;
modelActual: string | null;
modelRequested: string | null;
status: string | null;