fix: preserve local usage state on backup import
This commit is contained in:
@@ -181,6 +181,167 @@ describe('backupService', () => {
|
|||||||
expect(restoredChannel?.sourceModel).toBe('gpt-4o');
|
expect(restoredChannel?.sourceModel).toBe('gpt-4o');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('preserves local logs and runtime stats when importing account backups', async () => {
|
||||||
|
const exportedAt = '2026-03-20T09:00:00.000Z';
|
||||||
|
const localRuntimeAt = '2026-03-21T10:30:00.000Z';
|
||||||
|
const site = await db.insert(schema.sites).values({
|
||||||
|
name: 'backup-site',
|
||||||
|
url: 'https://preserve.example.com',
|
||||||
|
platform: 'new-api',
|
||||||
|
status: 'active',
|
||||||
|
createdAt: exportedAt,
|
||||||
|
updatedAt: exportedAt,
|
||||||
|
}).returning().get();
|
||||||
|
|
||||||
|
const account = await db.insert(schema.accounts).values({
|
||||||
|
siteId: site.id,
|
||||||
|
username: 'preserve-user',
|
||||||
|
accessToken: 'session-token',
|
||||||
|
apiToken: 'api-token',
|
||||||
|
balance: 20,
|
||||||
|
balanceUsed: 3,
|
||||||
|
quota: 100,
|
||||||
|
status: 'active',
|
||||||
|
checkinEnabled: true,
|
||||||
|
createdAt: exportedAt,
|
||||||
|
updatedAt: exportedAt,
|
||||||
|
}).returning().get();
|
||||||
|
|
||||||
|
const accountToken = await db.insert(schema.accountTokens).values({
|
||||||
|
accountId: account.id,
|
||||||
|
name: 'default',
|
||||||
|
token: 'sk-preserve-token',
|
||||||
|
source: 'manual',
|
||||||
|
enabled: true,
|
||||||
|
isDefault: true,
|
||||||
|
createdAt: exportedAt,
|
||||||
|
updatedAt: exportedAt,
|
||||||
|
}).returning().get();
|
||||||
|
|
||||||
|
const route = await db.insert(schema.tokenRoutes).values({
|
||||||
|
modelPattern: 'gpt-preserve-*',
|
||||||
|
displayName: 'backup-route',
|
||||||
|
modelMapping: JSON.stringify({ to: 'gpt-4o-mini' }),
|
||||||
|
routeMode: 'pattern',
|
||||||
|
routingStrategy: 'weighted',
|
||||||
|
enabled: true,
|
||||||
|
createdAt: exportedAt,
|
||||||
|
updatedAt: exportedAt,
|
||||||
|
}).returning().get();
|
||||||
|
|
||||||
|
await db.insert(schema.routeChannels).values({
|
||||||
|
routeId: route.id,
|
||||||
|
accountId: account.id,
|
||||||
|
tokenId: accountToken.id,
|
||||||
|
sourceModel: 'gpt-4o',
|
||||||
|
priority: 1,
|
||||||
|
weight: 10,
|
||||||
|
enabled: true,
|
||||||
|
manualOverride: false,
|
||||||
|
successCount: 1,
|
||||||
|
failCount: 0,
|
||||||
|
totalLatencyMs: 200,
|
||||||
|
totalCost: 0.5,
|
||||||
|
lastUsedAt: exportedAt,
|
||||||
|
lastSelectedAt: exportedAt,
|
||||||
|
lastFailAt: null,
|
||||||
|
consecutiveFailCount: 0,
|
||||||
|
cooldownLevel: 0,
|
||||||
|
cooldownUntil: null,
|
||||||
|
}).run();
|
||||||
|
|
||||||
|
const insertedChannel = await db.select()
|
||||||
|
.from(schema.routeChannels)
|
||||||
|
.where(eq(schema.routeChannels.routeId, route.id))
|
||||||
|
.get();
|
||||||
|
|
||||||
|
expect(insertedChannel).toBeTruthy();
|
||||||
|
|
||||||
|
const exported = await backupService.exportBackup('all');
|
||||||
|
|
||||||
|
await db.update(schema.sites).set({
|
||||||
|
name: 'mutated-local-site',
|
||||||
|
updatedAt: localRuntimeAt,
|
||||||
|
}).where(eq(schema.sites.id, site.id)).run();
|
||||||
|
|
||||||
|
await db.update(schema.tokenRoutes).set({
|
||||||
|
displayName: 'mutated-local-route',
|
||||||
|
updatedAt: localRuntimeAt,
|
||||||
|
}).where(eq(schema.tokenRoutes.id, route.id)).run();
|
||||||
|
|
||||||
|
await db.update(schema.accounts).set({
|
||||||
|
balanceUsed: 88,
|
||||||
|
updatedAt: localRuntimeAt,
|
||||||
|
}).where(eq(schema.accounts.id, account.id)).run();
|
||||||
|
|
||||||
|
await db.update(schema.routeChannels).set({
|
||||||
|
successCount: 77,
|
||||||
|
failCount: 9,
|
||||||
|
totalLatencyMs: 4321,
|
||||||
|
totalCost: 7.89,
|
||||||
|
lastUsedAt: localRuntimeAt,
|
||||||
|
lastSelectedAt: localRuntimeAt,
|
||||||
|
lastFailAt: localRuntimeAt,
|
||||||
|
consecutiveFailCount: 4,
|
||||||
|
cooldownLevel: 2,
|
||||||
|
cooldownUntil: localRuntimeAt,
|
||||||
|
}).where(eq(schema.routeChannels.id, insertedChannel!.id)).run();
|
||||||
|
|
||||||
|
await db.insert(schema.checkinLogs).values({
|
||||||
|
accountId: account.id,
|
||||||
|
status: 'success',
|
||||||
|
message: 'local-checkin',
|
||||||
|
reward: '1.5',
|
||||||
|
createdAt: localRuntimeAt,
|
||||||
|
}).run();
|
||||||
|
|
||||||
|
await db.insert(schema.proxyLogs).values({
|
||||||
|
routeId: route.id,
|
||||||
|
channelId: insertedChannel!.id,
|
||||||
|
accountId: account.id,
|
||||||
|
modelRequested: 'gpt-4o',
|
||||||
|
modelActual: 'gpt-4o',
|
||||||
|
status: 'success',
|
||||||
|
totalTokens: 321,
|
||||||
|
estimatedCost: 0.123,
|
||||||
|
createdAt: localRuntimeAt,
|
||||||
|
}).run();
|
||||||
|
|
||||||
|
const result = await backupService.importBackup(exported as unknown as Record<string, unknown>);
|
||||||
|
|
||||||
|
expect(result.allImported).toBe(true);
|
||||||
|
expect(result.sections.accounts).toBe(true);
|
||||||
|
|
||||||
|
const restoredSite = await db.select().from(schema.sites).where(eq(schema.sites.id, site.id)).get();
|
||||||
|
const restoredAccount = await db.select().from(schema.accounts).where(eq(schema.accounts.id, account.id)).get();
|
||||||
|
const restoredRoute = await db.select().from(schema.tokenRoutes).where(eq(schema.tokenRoutes.id, route.id)).get();
|
||||||
|
const restoredChannel = await db.select().from(schema.routeChannels).where(eq(schema.routeChannels.id, insertedChannel!.id)).get();
|
||||||
|
const restoredProxyLogs = await db.select().from(schema.proxyLogs).all();
|
||||||
|
const restoredCheckinLogs = await db.select().from(schema.checkinLogs).all();
|
||||||
|
|
||||||
|
expect(restoredSite?.name).toBe('backup-site');
|
||||||
|
expect(restoredRoute?.displayName).toBe('backup-route');
|
||||||
|
expect(restoredAccount?.balanceUsed).toBe(88);
|
||||||
|
expect(restoredChannel?.successCount).toBe(77);
|
||||||
|
expect(restoredChannel?.failCount).toBe(9);
|
||||||
|
expect(restoredChannel?.totalLatencyMs).toBe(4321);
|
||||||
|
expect(restoredChannel?.totalCost).toBe(7.89);
|
||||||
|
expect(restoredChannel?.lastUsedAt).toBe(localRuntimeAt);
|
||||||
|
expect(restoredChannel?.lastSelectedAt).toBe(localRuntimeAt);
|
||||||
|
expect(restoredChannel?.lastFailAt).toBe(localRuntimeAt);
|
||||||
|
expect(restoredChannel?.consecutiveFailCount).toBe(4);
|
||||||
|
expect(restoredChannel?.cooldownLevel).toBe(2);
|
||||||
|
expect(restoredChannel?.cooldownUntil).toBe(localRuntimeAt);
|
||||||
|
expect(restoredProxyLogs).toHaveLength(1);
|
||||||
|
expect(restoredProxyLogs[0]?.accountId).toBe(account.id);
|
||||||
|
expect(restoredProxyLogs[0]?.routeId).toBe(route.id);
|
||||||
|
expect(restoredProxyLogs[0]?.channelId).toBe(insertedChannel!.id);
|
||||||
|
expect(restoredProxyLogs[0]?.totalTokens).toBe(321);
|
||||||
|
expect(restoredCheckinLogs).toHaveLength(1);
|
||||||
|
expect(restoredCheckinLogs[0]?.accountId).toBe(account.id);
|
||||||
|
expect(restoredCheckinLogs[0]?.message).toBe('local-checkin');
|
||||||
|
});
|
||||||
|
|
||||||
it('imports ALL-API-Hub style payload with accounts and preferences', async () => {
|
it('imports ALL-API-Hub style payload with accounts and preferences', async () => {
|
||||||
const payload = {
|
const payload = {
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ type AccountTokenRow = typeof schema.accountTokens.$inferSelect;
|
|||||||
type TokenRouteRow = typeof schema.tokenRoutes.$inferSelect;
|
type TokenRouteRow = typeof schema.tokenRoutes.$inferSelect;
|
||||||
type RouteChannelRow = typeof schema.routeChannels.$inferSelect;
|
type RouteChannelRow = typeof schema.routeChannels.$inferSelect;
|
||||||
type RouteGroupSourceRow = typeof schema.routeGroupSources.$inferSelect;
|
type RouteGroupSourceRow = typeof schema.routeGroupSources.$inferSelect;
|
||||||
|
type ProxyLogRow = typeof schema.proxyLogs.$inferSelect;
|
||||||
|
type CheckinLogRow = typeof schema.checkinLogs.$inferSelect;
|
||||||
|
|
||||||
interface AccountsBackupSection {
|
interface AccountsBackupSection {
|
||||||
sites: SiteRow[];
|
sites: SiteRow[];
|
||||||
@@ -80,6 +82,53 @@ type BackupV2 = BackupFullV2 | BackupAccountsPartialV2 | BackupPreferencesPartia
|
|||||||
|
|
||||||
type RawBackupData = Record<string, unknown>;
|
type RawBackupData = Record<string, unknown>;
|
||||||
|
|
||||||
|
type AccountRuntimeSnapshot = {
|
||||||
|
balanceUsed: number | null;
|
||||||
|
lastCheckinAt: string | null;
|
||||||
|
lastBalanceRefresh: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type RouteChannelRuntimeSnapshot = Pick<RouteChannelRow,
|
||||||
|
'successCount'
|
||||||
|
| 'failCount'
|
||||||
|
| 'totalLatencyMs'
|
||||||
|
| 'totalCost'
|
||||||
|
| 'lastUsedAt'
|
||||||
|
| 'lastSelectedAt'
|
||||||
|
| 'lastFailAt'
|
||||||
|
| 'consecutiveFailCount'
|
||||||
|
| 'cooldownLevel'
|
||||||
|
| 'cooldownUntil'
|
||||||
|
>;
|
||||||
|
|
||||||
|
type ProxyLogSnapshot = ProxyLogRow & {
|
||||||
|
accountKey: string | null;
|
||||||
|
routeKey: string | null;
|
||||||
|
channelKey: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CheckinLogSnapshot = CheckinLogRow & {
|
||||||
|
accountKey: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface RuntimeIdentityIndexes {
|
||||||
|
accountKeyById: Map<number, string>;
|
||||||
|
accountIdByKey: Map<string, number>;
|
||||||
|
tokenKeyById: Map<number, string>;
|
||||||
|
tokenIdByKey: Map<string, number>;
|
||||||
|
routeKeyById: Map<number, string>;
|
||||||
|
routeIdByKey: Map<string, number>;
|
||||||
|
channelKeyById: Map<number, string>;
|
||||||
|
channelIdByKey: Map<string, number>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RuntimeStateSnapshot {
|
||||||
|
accountRuntimeByKey: Map<string, AccountRuntimeSnapshot>;
|
||||||
|
routeChannelRuntimeByKey: Map<string, RouteChannelRuntimeSnapshot>;
|
||||||
|
proxyLogs: ProxyLogSnapshot[];
|
||||||
|
checkinLogs: CheckinLogSnapshot[];
|
||||||
|
}
|
||||||
|
|
||||||
interface BackupImportResult {
|
interface BackupImportResult {
|
||||||
allImported: boolean;
|
allImported: boolean;
|
||||||
sections: {
|
sections: {
|
||||||
@@ -202,6 +251,234 @@ function resolveImportedOauthColumns(row: Pick<AccountRow, 'oauthProvider' | 'oa
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildSiteIdentityKey(row: Pick<SiteRow, 'platform' | 'url'>): string {
|
||||||
|
return `${asString(row.platform).toLowerCase()}::${normalizeOriginUrl(asString(row.url))}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildAccountIdentityKey(input: {
|
||||||
|
siteKey: string;
|
||||||
|
username?: string | null;
|
||||||
|
accessToken?: string | null;
|
||||||
|
apiToken?: string | null;
|
||||||
|
oauthProvider?: string | null;
|
||||||
|
oauthAccountKey?: string | null;
|
||||||
|
oauthProjectId?: string | null;
|
||||||
|
}): string {
|
||||||
|
const oauthProvider = asString(input.oauthProvider).toLowerCase();
|
||||||
|
const oauthAccountKey = asString(input.oauthAccountKey);
|
||||||
|
const oauthProjectId = asString(input.oauthProjectId);
|
||||||
|
if (oauthProvider || oauthAccountKey || oauthProjectId) {
|
||||||
|
return `oauth::${input.siteKey}::${oauthProvider}::${oauthAccountKey}::${oauthProjectId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiToken = asString(input.apiToken);
|
||||||
|
if (apiToken) {
|
||||||
|
return `api::${input.siteKey}::${apiToken}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const accessToken = asString(input.accessToken);
|
||||||
|
if (accessToken) {
|
||||||
|
return `session::${input.siteKey}::${accessToken}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `user::${input.siteKey}::${asString(input.username)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTokenIdentityKey(row: Pick<AccountTokenRow, 'name' | 'token' | 'tokenGroup' | 'source' | 'isDefault'>, accountKey: string): string {
|
||||||
|
const token = asString(row.token);
|
||||||
|
if (token) {
|
||||||
|
return `token::${accountKey}::${token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'token-meta',
|
||||||
|
accountKey,
|
||||||
|
asString(row.name),
|
||||||
|
asString(row.tokenGroup),
|
||||||
|
asString(row.source),
|
||||||
|
row.isDefault ? '1' : '0',
|
||||||
|
].join('::');
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildRouteIdentityKey(row: Pick<TokenRouteRow, 'modelPattern' | 'routeMode'>): string {
|
||||||
|
return [
|
||||||
|
asString(row.modelPattern),
|
||||||
|
asString(row.routeMode),
|
||||||
|
].join('::');
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildRouteChannelIdentityKey(
|
||||||
|
row: Pick<RouteChannelRow, 'routeId' | 'accountId' | 'tokenId' | 'sourceModel'>,
|
||||||
|
indexes: Pick<RuntimeIdentityIndexes, 'accountKeyById' | 'tokenKeyById' | 'routeKeyById'>,
|
||||||
|
): string | null {
|
||||||
|
const routeKey = indexes.routeKeyById.get(row.routeId);
|
||||||
|
const accountKey = indexes.accountKeyById.get(row.accountId);
|
||||||
|
if (!routeKey || !accountKey) return null;
|
||||||
|
|
||||||
|
const tokenKey = row.tokenId ? (indexes.tokenKeyById.get(row.tokenId) || '') : '';
|
||||||
|
return [routeKey, accountKey, tokenKey, asString(row.sourceModel)].join('::');
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildRuntimeIdentityIndexesFromSection(section: AccountsBackupSection): RuntimeIdentityIndexes {
|
||||||
|
const siteKeyById = new Map<number, string>();
|
||||||
|
const accountKeyById = new Map<number, string>();
|
||||||
|
const accountIdByKey = new Map<string, number>();
|
||||||
|
const tokenKeyById = new Map<number, string>();
|
||||||
|
const tokenIdByKey = new Map<string, number>();
|
||||||
|
const routeKeyById = new Map<number, string>();
|
||||||
|
const routeIdByKey = new Map<string, number>();
|
||||||
|
const channelKeyById = new Map<number, string>();
|
||||||
|
const channelIdByKey = new Map<string, number>();
|
||||||
|
|
||||||
|
for (const row of section.sites) {
|
||||||
|
siteKeyById.set(row.id, buildSiteIdentityKey(row));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const row of section.accounts) {
|
||||||
|
const siteKey = siteKeyById.get(row.siteId);
|
||||||
|
if (!siteKey) continue;
|
||||||
|
const oauthColumns = resolveImportedOauthColumns(row);
|
||||||
|
const accountKey = buildAccountIdentityKey({
|
||||||
|
siteKey,
|
||||||
|
username: row.username,
|
||||||
|
accessToken: row.accessToken,
|
||||||
|
apiToken: row.apiToken,
|
||||||
|
oauthProvider: oauthColumns.oauthProvider,
|
||||||
|
oauthAccountKey: oauthColumns.oauthAccountKey,
|
||||||
|
oauthProjectId: oauthColumns.oauthProjectId,
|
||||||
|
});
|
||||||
|
accountKeyById.set(row.id, accountKey);
|
||||||
|
accountIdByKey.set(accountKey, row.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const row of section.accountTokens) {
|
||||||
|
const accountKey = accountKeyById.get(row.accountId);
|
||||||
|
if (!accountKey) continue;
|
||||||
|
const tokenKey = buildTokenIdentityKey(row, accountKey);
|
||||||
|
tokenKeyById.set(row.id, tokenKey);
|
||||||
|
tokenIdByKey.set(tokenKey, row.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const row of section.tokenRoutes) {
|
||||||
|
const routeKey = buildRouteIdentityKey(row);
|
||||||
|
routeKeyById.set(row.id, routeKey);
|
||||||
|
routeIdByKey.set(routeKey, row.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const row of section.routeChannels) {
|
||||||
|
const channelKey = buildRouteChannelIdentityKey(row, {
|
||||||
|
accountKeyById,
|
||||||
|
tokenKeyById,
|
||||||
|
routeKeyById,
|
||||||
|
});
|
||||||
|
if (!channelKey) continue;
|
||||||
|
channelKeyById.set(row.id, channelKey);
|
||||||
|
channelIdByKey.set(channelKey, row.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
accountKeyById,
|
||||||
|
accountIdByKey,
|
||||||
|
tokenKeyById,
|
||||||
|
tokenIdByKey,
|
||||||
|
routeKeyById,
|
||||||
|
routeIdByKey,
|
||||||
|
channelKeyById,
|
||||||
|
channelIdByKey,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function collectCurrentRuntimeStateSnapshot(): Promise<RuntimeStateSnapshot> {
|
||||||
|
const [sites, accounts, accountTokens, tokenRoutes, routeChannels, proxyLogs, checkinLogs] = await Promise.all([
|
||||||
|
db.select().from(schema.sites).all(),
|
||||||
|
db.select().from(schema.accounts).all(),
|
||||||
|
db.select().from(schema.accountTokens).all(),
|
||||||
|
db.select().from(schema.tokenRoutes).all(),
|
||||||
|
db.select().from(schema.routeChannels).all(),
|
||||||
|
db.select().from(schema.proxyLogs).all(),
|
||||||
|
db.select().from(schema.checkinLogs).all(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const siteKeyById = new Map<number, string>();
|
||||||
|
for (const row of sites) {
|
||||||
|
siteKeyById.set(row.id, buildSiteIdentityKey(row));
|
||||||
|
}
|
||||||
|
|
||||||
|
const accountKeyById = new Map<number, string>();
|
||||||
|
const accountRuntimeByKey = new Map<string, AccountRuntimeSnapshot>();
|
||||||
|
for (const row of accounts) {
|
||||||
|
const siteKey = siteKeyById.get(row.siteId);
|
||||||
|
if (!siteKey) continue;
|
||||||
|
const oauthColumns = resolveImportedOauthColumns(row);
|
||||||
|
const accountKey = buildAccountIdentityKey({
|
||||||
|
siteKey,
|
||||||
|
username: row.username,
|
||||||
|
accessToken: row.accessToken,
|
||||||
|
apiToken: row.apiToken,
|
||||||
|
oauthProvider: oauthColumns.oauthProvider,
|
||||||
|
oauthAccountKey: oauthColumns.oauthAccountKey,
|
||||||
|
oauthProjectId: oauthColumns.oauthProjectId,
|
||||||
|
});
|
||||||
|
accountKeyById.set(row.id, accountKey);
|
||||||
|
accountRuntimeByKey.set(accountKey, {
|
||||||
|
balanceUsed: row.balanceUsed ?? 0,
|
||||||
|
lastCheckinAt: row.lastCheckinAt ?? null,
|
||||||
|
lastBalanceRefresh: row.lastBalanceRefresh ?? null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenKeyById = new Map<number, string>();
|
||||||
|
for (const row of accountTokens) {
|
||||||
|
const accountKey = accountKeyById.get(row.accountId);
|
||||||
|
if (!accountKey) continue;
|
||||||
|
tokenKeyById.set(row.id, buildTokenIdentityKey(row, accountKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
const routeKeyById = new Map<number, string>();
|
||||||
|
for (const row of tokenRoutes) {
|
||||||
|
routeKeyById.set(row.id, buildRouteIdentityKey(row));
|
||||||
|
}
|
||||||
|
|
||||||
|
const channelKeyById = new Map<number, string>();
|
||||||
|
const routeChannelRuntimeByKey = new Map<string, RouteChannelRuntimeSnapshot>();
|
||||||
|
for (const row of routeChannels) {
|
||||||
|
const channelKey = buildRouteChannelIdentityKey(row, {
|
||||||
|
accountKeyById,
|
||||||
|
tokenKeyById,
|
||||||
|
routeKeyById,
|
||||||
|
});
|
||||||
|
if (!channelKey) continue;
|
||||||
|
channelKeyById.set(row.id, channelKey);
|
||||||
|
routeChannelRuntimeByKey.set(channelKey, {
|
||||||
|
successCount: row.successCount,
|
||||||
|
failCount: row.failCount,
|
||||||
|
totalLatencyMs: row.totalLatencyMs,
|
||||||
|
totalCost: row.totalCost,
|
||||||
|
lastUsedAt: row.lastUsedAt ?? null,
|
||||||
|
lastSelectedAt: row.lastSelectedAt ?? null,
|
||||||
|
lastFailAt: row.lastFailAt ?? null,
|
||||||
|
consecutiveFailCount: row.consecutiveFailCount ?? 0,
|
||||||
|
cooldownLevel: row.cooldownLevel ?? 0,
|
||||||
|
cooldownUntil: row.cooldownUntil ?? null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
accountRuntimeByKey,
|
||||||
|
routeChannelRuntimeByKey,
|
||||||
|
proxyLogs: proxyLogs.map((row) => ({
|
||||||
|
...row,
|
||||||
|
accountKey: row.accountId ? (accountKeyById.get(row.accountId) || null) : null,
|
||||||
|
routeKey: row.routeId ? (routeKeyById.get(row.routeId) || null) : null,
|
||||||
|
channelKey: row.channelId ? (channelKeyById.get(row.channelId) || null) : null,
|
||||||
|
})),
|
||||||
|
checkinLogs: checkinLogs.map((row) => ({
|
||||||
|
...row,
|
||||||
|
accountKey: accountKeyById.get(row.accountId) || null,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeLegacyPlatform(raw: string): string {
|
function normalizeLegacyPlatform(raw: string): string {
|
||||||
const value = raw.trim().toLowerCase();
|
const value = raw.trim().toLowerCase();
|
||||||
if (!value) return 'new-api';
|
if (!value) return 'new-api';
|
||||||
@@ -1068,14 +1345,16 @@ function detectImportMetadata(data: RawBackupData): {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function importAccountsSection(section: AccountsBackupSection): Promise<void> {
|
async function importAccountsSection(section: AccountsBackupSection): Promise<void> {
|
||||||
|
const runtimeState = await collectCurrentRuntimeStateSnapshot();
|
||||||
|
const importedIndexes = buildRuntimeIdentityIndexesFromSection(section);
|
||||||
|
|
||||||
await db.transaction(async (tx) => {
|
await db.transaction(async (tx) => {
|
||||||
|
await tx.delete(schema.proxyLogs).run();
|
||||||
await tx.delete(schema.routeChannels).run();
|
await tx.delete(schema.routeChannels).run();
|
||||||
await tx.delete(schema.routeGroupSources).run();
|
await tx.delete(schema.routeGroupSources).run();
|
||||||
await tx.delete(schema.tokenRoutes).run();
|
await tx.delete(schema.tokenRoutes).run();
|
||||||
await tx.delete(schema.tokenModelAvailability).run();
|
await tx.delete(schema.tokenModelAvailability).run();
|
||||||
await tx.delete(schema.modelAvailability).run();
|
await tx.delete(schema.modelAvailability).run();
|
||||||
await tx.delete(schema.proxyLogs).run();
|
|
||||||
await tx.delete(schema.checkinLogs).run();
|
|
||||||
await tx.delete(schema.accountTokens).run();
|
await tx.delete(schema.accountTokens).run();
|
||||||
await tx.delete(schema.accounts).run();
|
await tx.delete(schema.accounts).run();
|
||||||
await tx.delete(schema.sites).run();
|
await tx.delete(schema.sites).run();
|
||||||
@@ -1102,6 +1381,8 @@ async function importAccountsSection(section: AccountsBackupSection): Promise<vo
|
|||||||
|
|
||||||
for (const row of section.accounts) {
|
for (const row of section.accounts) {
|
||||||
const oauthColumns = resolveImportedOauthColumns(row);
|
const oauthColumns = resolveImportedOauthColumns(row);
|
||||||
|
const accountKey = importedIndexes.accountKeyById.get(row.id);
|
||||||
|
const runtimeAccount = accountKey ? runtimeState.accountRuntimeByKey.get(accountKey) : undefined;
|
||||||
await tx.insert(schema.accounts).values({
|
await tx.insert(schema.accounts).values({
|
||||||
id: row.id,
|
id: row.id,
|
||||||
siteId: row.siteId,
|
siteId: row.siteId,
|
||||||
@@ -1112,7 +1393,7 @@ async function importAccountsSection(section: AccountsBackupSection): Promise<vo
|
|||||||
oauthAccountKey: oauthColumns.oauthAccountKey,
|
oauthAccountKey: oauthColumns.oauthAccountKey,
|
||||||
oauthProjectId: oauthColumns.oauthProjectId,
|
oauthProjectId: oauthColumns.oauthProjectId,
|
||||||
balance: row.balance,
|
balance: row.balance,
|
||||||
balanceUsed: row.balanceUsed,
|
balanceUsed: runtimeAccount?.balanceUsed ?? row.balanceUsed,
|
||||||
quota: row.quota,
|
quota: row.quota,
|
||||||
unitCost: row.unitCost,
|
unitCost: row.unitCost,
|
||||||
valueScore: row.valueScore,
|
valueScore: row.valueScore,
|
||||||
@@ -1120,8 +1401,8 @@ async function importAccountsSection(section: AccountsBackupSection): Promise<vo
|
|||||||
isPinned: row.isPinned ?? false,
|
isPinned: row.isPinned ?? false,
|
||||||
sortOrder: row.sortOrder ?? 0,
|
sortOrder: row.sortOrder ?? 0,
|
||||||
checkinEnabled: row.checkinEnabled,
|
checkinEnabled: row.checkinEnabled,
|
||||||
lastCheckinAt: row.lastCheckinAt,
|
lastCheckinAt: runtimeAccount?.lastCheckinAt ?? row.lastCheckinAt,
|
||||||
lastBalanceRefresh: row.lastBalanceRefresh,
|
lastBalanceRefresh: runtimeAccount?.lastBalanceRefresh ?? row.lastBalanceRefresh,
|
||||||
extraConfig: row.extraConfig,
|
extraConfig: row.extraConfig,
|
||||||
createdAt: row.createdAt,
|
createdAt: row.createdAt,
|
||||||
updatedAt: row.updatedAt,
|
updatedAt: row.updatedAt,
|
||||||
@@ -1170,6 +1451,8 @@ async function importAccountsSection(section: AccountsBackupSection): Promise<vo
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const row of section.routeChannels) {
|
for (const row of section.routeChannels) {
|
||||||
|
const channelKey = importedIndexes.channelKeyById.get(row.id);
|
||||||
|
const runtimeChannel = channelKey ? runtimeState.routeChannelRuntimeByKey.get(channelKey) : undefined;
|
||||||
await tx.insert(schema.routeChannels).values({
|
await tx.insert(schema.routeChannels).values({
|
||||||
id: row.id,
|
id: row.id,
|
||||||
routeId: row.routeId,
|
routeId: row.routeId,
|
||||||
@@ -1180,13 +1463,61 @@ async function importAccountsSection(section: AccountsBackupSection): Promise<vo
|
|||||||
weight: row.weight,
|
weight: row.weight,
|
||||||
enabled: row.enabled,
|
enabled: row.enabled,
|
||||||
manualOverride: row.manualOverride,
|
manualOverride: row.manualOverride,
|
||||||
successCount: row.successCount,
|
successCount: runtimeChannel?.successCount ?? row.successCount,
|
||||||
failCount: row.failCount,
|
failCount: runtimeChannel?.failCount ?? row.failCount,
|
||||||
totalLatencyMs: row.totalLatencyMs,
|
totalLatencyMs: runtimeChannel?.totalLatencyMs ?? row.totalLatencyMs,
|
||||||
totalCost: row.totalCost,
|
totalCost: runtimeChannel?.totalCost ?? row.totalCost,
|
||||||
lastUsedAt: row.lastUsedAt,
|
lastUsedAt: runtimeChannel?.lastUsedAt ?? row.lastUsedAt,
|
||||||
lastFailAt: row.lastFailAt,
|
lastSelectedAt: runtimeChannel?.lastSelectedAt ?? row.lastSelectedAt ?? null,
|
||||||
cooldownUntil: row.cooldownUntil,
|
lastFailAt: runtimeChannel?.lastFailAt ?? row.lastFailAt,
|
||||||
|
consecutiveFailCount: runtimeChannel?.consecutiveFailCount ?? row.consecutiveFailCount ?? 0,
|
||||||
|
cooldownLevel: runtimeChannel?.cooldownLevel ?? row.cooldownLevel ?? 0,
|
||||||
|
cooldownUntil: runtimeChannel?.cooldownUntil ?? row.cooldownUntil,
|
||||||
|
}).run();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const row of runtimeState.proxyLogs) {
|
||||||
|
const accountId = row.accountKey ? (importedIndexes.accountIdByKey.get(row.accountKey) ?? null) : null;
|
||||||
|
const routeId = row.routeKey ? (importedIndexes.routeIdByKey.get(row.routeKey) ?? null) : null;
|
||||||
|
const channelId = row.channelKey ? (importedIndexes.channelIdByKey.get(row.channelKey) ?? null) : null;
|
||||||
|
|
||||||
|
await tx.insert(schema.proxyLogs).values({
|
||||||
|
id: row.id,
|
||||||
|
routeId,
|
||||||
|
channelId,
|
||||||
|
accountId,
|
||||||
|
downstreamApiKeyId: row.downstreamApiKeyId ?? null,
|
||||||
|
modelRequested: row.modelRequested ?? null,
|
||||||
|
modelActual: row.modelActual ?? null,
|
||||||
|
status: row.status ?? null,
|
||||||
|
httpStatus: row.httpStatus ?? null,
|
||||||
|
latencyMs: row.latencyMs ?? null,
|
||||||
|
promptTokens: row.promptTokens ?? null,
|
||||||
|
completionTokens: row.completionTokens ?? null,
|
||||||
|
totalTokens: row.totalTokens ?? null,
|
||||||
|
estimatedCost: row.estimatedCost ?? null,
|
||||||
|
billingDetails: row.billingDetails ?? null,
|
||||||
|
clientFamily: row.clientFamily ?? null,
|
||||||
|
clientAppId: row.clientAppId ?? null,
|
||||||
|
clientAppName: row.clientAppName ?? null,
|
||||||
|
clientConfidence: row.clientConfidence ?? null,
|
||||||
|
errorMessage: row.errorMessage ?? null,
|
||||||
|
retryCount: row.retryCount ?? 0,
|
||||||
|
createdAt: row.createdAt,
|
||||||
|
}).run();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const row of runtimeState.checkinLogs) {
|
||||||
|
const accountId = row.accountKey ? importedIndexes.accountIdByKey.get(row.accountKey) : undefined;
|
||||||
|
if (!accountId) continue;
|
||||||
|
|
||||||
|
await tx.insert(schema.checkinLogs).values({
|
||||||
|
id: row.id,
|
||||||
|
accountId,
|
||||||
|
status: row.status,
|
||||||
|
message: row.message ?? null,
|
||||||
|
reward: row.reward ?? null,
|
||||||
|
createdAt: row.createdAt,
|
||||||
}).run();
|
}).run();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user