mirror of
https://github.com/cita-777/metapi.git
synced 2026-11-01 20:55:09 +08:00
653 lines
20 KiB
TypeScript
653 lines
20 KiB
TypeScript
import { asc } from 'drizzle-orm';
|
|
import cron from 'node-cron';
|
|
import { db, schema } from '../db/index.js';
|
|
import { upsertSetting } from '../db/upsertSetting.js';
|
|
import { getOauthInfoFromExtraConfig } from './oauth/oauthAccount.js';
|
|
|
|
const BACKUP_VERSION = '2.0';
|
|
|
|
export type BackupExportType = 'all' | 'accounts' | 'preferences';
|
|
|
|
type SiteRow = typeof schema.sites.$inferSelect;
|
|
type AccountRow = typeof schema.accounts.$inferSelect;
|
|
type AccountTokenRow = typeof schema.accountTokens.$inferSelect;
|
|
type TokenRouteRow = typeof schema.tokenRoutes.$inferSelect;
|
|
type RouteChannelRow = typeof schema.routeChannels.$inferSelect;
|
|
|
|
interface AccountsBackupSection {
|
|
sites: SiteRow[];
|
|
accounts: AccountRow[];
|
|
accountTokens: AccountTokenRow[];
|
|
tokenRoutes: TokenRouteRow[];
|
|
routeChannels: RouteChannelRow[];
|
|
}
|
|
|
|
interface PreferencesBackupSection {
|
|
settings: Array<{ key: string; value: unknown }>;
|
|
}
|
|
|
|
interface BackupFullV2 {
|
|
version: string;
|
|
timestamp: number;
|
|
accounts: AccountsBackupSection;
|
|
preferences: PreferencesBackupSection;
|
|
}
|
|
|
|
interface BackupAccountsPartialV2 {
|
|
version: string;
|
|
timestamp: number;
|
|
type: 'accounts';
|
|
accounts: AccountsBackupSection;
|
|
}
|
|
|
|
interface BackupPreferencesPartialV2 {
|
|
version: string;
|
|
timestamp: number;
|
|
type: 'preferences';
|
|
preferences: PreferencesBackupSection;
|
|
}
|
|
|
|
type BackupV2 = BackupFullV2 | BackupAccountsPartialV2 | BackupPreferencesPartialV2;
|
|
|
|
type RawBackupData = Record<string, unknown>;
|
|
|
|
interface BackupImportResult {
|
|
allImported: boolean;
|
|
sections: {
|
|
accounts: boolean;
|
|
preferences: boolean;
|
|
};
|
|
appliedSettings: Array<{ key: string; value: unknown }>;
|
|
}
|
|
|
|
const EXCLUDED_SETTING_KEYS = new Set<string>([
|
|
// Keep current admin login credential unchanged to avoid accidental lock-out.
|
|
'auth_token',
|
|
]);
|
|
|
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
}
|
|
|
|
function asString(value: unknown): string {
|
|
if (typeof value !== 'string') return '';
|
|
return value.trim();
|
|
}
|
|
|
|
function asBoolean(value: unknown, fallback = false): boolean {
|
|
if (typeof value === 'boolean') return value;
|
|
return fallback;
|
|
}
|
|
|
|
function asNumber(value: unknown, fallback = 0): number {
|
|
const n = Number(value);
|
|
return Number.isFinite(n) ? n : fallback;
|
|
}
|
|
|
|
function toIsoString(value: unknown): string {
|
|
if (typeof value === 'string' && value.trim()) {
|
|
const d = new Date(value);
|
|
if (!Number.isNaN(d.getTime())) return d.toISOString();
|
|
}
|
|
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
const d = new Date(value);
|
|
if (!Number.isNaN(d.getTime())) return d.toISOString();
|
|
}
|
|
return new Date().toISOString();
|
|
}
|
|
|
|
function normalizeLegacyQuota(raw: unknown): number {
|
|
const value = asNumber(raw, 0);
|
|
if (!Number.isFinite(value) || value <= 0) return 0;
|
|
// ref-all-api-hub stores quota in raw units for NewAPI-like sites.
|
|
// Convert obvious raw values to display currency units.
|
|
if (value >= 10_000) return value / 500_000;
|
|
return value;
|
|
}
|
|
|
|
function resolveImportedOauthColumns(row: Pick<AccountRow, 'oauthProvider' | 'oauthAccountKey' | 'oauthProjectId' | 'extraConfig'>) {
|
|
const oauth = getOauthInfoFromExtraConfig(row.extraConfig);
|
|
const oauthProvider = row.oauthProvider || oauth?.provider || null;
|
|
const oauthAccountKey = row.oauthAccountKey || oauth?.accountKey || oauth?.accountId || null;
|
|
const oauthProjectId = row.oauthProjectId || oauth?.projectId || null;
|
|
return {
|
|
oauthProvider,
|
|
oauthAccountKey,
|
|
oauthProjectId,
|
|
};
|
|
}
|
|
|
|
function normalizeLegacyPlatform(raw: string): string {
|
|
const value = raw.trim().toLowerCase();
|
|
if (!value) return 'new-api';
|
|
|
|
const supported = new Set([
|
|
'new-api',
|
|
'one-api',
|
|
'anyrouter',
|
|
'one-hub',
|
|
'done-hub',
|
|
'sub2api',
|
|
'veloera',
|
|
]);
|
|
if (supported.has(value)) return value;
|
|
|
|
if (value.includes('wong')) return 'new-api';
|
|
if (value.includes('anyrouter')) return 'anyrouter';
|
|
if (value.includes('done')) return 'done-hub';
|
|
|
|
return 'new-api';
|
|
}
|
|
|
|
function buildAccountsSectionFromRefBackup(data: RawBackupData): AccountsBackupSection | null {
|
|
const accountsContainer = isRecord(data.accounts) ? data.accounts : null;
|
|
const rows = Array.isArray(accountsContainer?.accounts) ? accountsContainer.accounts : null;
|
|
if (!rows) return null;
|
|
|
|
const sites: SiteRow[] = [];
|
|
const accounts: AccountRow[] = [];
|
|
const accountTokens: AccountTokenRow[] = [];
|
|
const tokenRoutes: TokenRouteRow[] = [];
|
|
const routeChannels: RouteChannelRow[] = [];
|
|
|
|
const siteIdByKey = new Map<string, number>();
|
|
let nextSiteId = 1;
|
|
let nextAccountId = 1;
|
|
let nextTokenId = 1;
|
|
|
|
for (const item of rows) {
|
|
if (!isRecord(item)) continue;
|
|
|
|
const siteUrl = asString(item.site_url);
|
|
if (!siteUrl) continue;
|
|
|
|
const platform = normalizeLegacyPlatform(asString(item.site_type));
|
|
const siteName = asString(item.site_name) || siteUrl;
|
|
const siteKey = `${platform}::${siteUrl}`;
|
|
|
|
let siteId = siteIdByKey.get(siteKey) || 0;
|
|
if (!siteId) {
|
|
siteId = nextSiteId++;
|
|
siteIdByKey.set(siteKey, siteId);
|
|
sites.push({
|
|
id: siteId,
|
|
name: siteName,
|
|
url: siteUrl,
|
|
externalCheckinUrl: null,
|
|
platform,
|
|
proxyUrl: null,
|
|
useSystemProxy: false,
|
|
customHeaders: null,
|
|
status: 'active',
|
|
isPinned: false,
|
|
sortOrder: sites.length,
|
|
globalWeight: 1,
|
|
apiKey: null,
|
|
createdAt: toIsoString(item.created_at),
|
|
updatedAt: toIsoString(item.updated_at),
|
|
});
|
|
}
|
|
|
|
const accountInfo = isRecord(item.account_info) ? item.account_info : {};
|
|
const cookieAuth = isRecord(item.cookieAuth) ? item.cookieAuth : {};
|
|
const authType = asString(item.authType);
|
|
|
|
const accountAccessToken =
|
|
asString(accountInfo.access_token)
|
|
|| asString(cookieAuth.sessionCookie)
|
|
|| asString((item as Record<string, unknown>).access_token);
|
|
if (!accountAccessToken) continue;
|
|
|
|
const platformUserId = asNumber(accountInfo.id, 0);
|
|
const username = asString(accountInfo.username)
|
|
|| asString(item.username)
|
|
|| (platformUserId > 0 ? `user-${platformUserId}` : `account-${nextAccountId}`);
|
|
|
|
let apiToken: string | null = null;
|
|
if (authType === 'api_key') {
|
|
apiToken = accountAccessToken;
|
|
}
|
|
|
|
const createdAt = toIsoString(item.created_at);
|
|
const updatedAt = toIsoString(item.updated_at);
|
|
const checkin = isRecord(item.checkIn) ? item.checkIn : {};
|
|
const extraConfigPayload = {
|
|
platformUserId: platformUserId > 0 ? platformUserId : undefined,
|
|
authType: authType || undefined,
|
|
source: 'ref-all-api-hub',
|
|
};
|
|
|
|
const accountId = nextAccountId++;
|
|
const importedBalance = normalizeLegacyQuota(accountInfo.quota);
|
|
const importedUsed = normalizeLegacyQuota(accountInfo.today_quota_consumption);
|
|
const importedQuota = importedBalance + importedUsed;
|
|
|
|
accounts.push({
|
|
id: accountId,
|
|
siteId,
|
|
username,
|
|
accessToken: accountAccessToken,
|
|
apiToken,
|
|
oauthProvider: null,
|
|
oauthAccountKey: null,
|
|
oauthProjectId: null,
|
|
balance: importedBalance,
|
|
balanceUsed: importedUsed,
|
|
quota: importedQuota > 0 ? importedQuota : importedBalance,
|
|
unitCost: null,
|
|
valueScore: 0,
|
|
status: asBoolean(item.disabled, false) ? 'disabled' : 'active',
|
|
isPinned: false,
|
|
sortOrder: accounts.length,
|
|
checkinEnabled: asBoolean(checkin.autoCheckInEnabled, true),
|
|
lastCheckinAt: null,
|
|
lastBalanceRefresh: null,
|
|
extraConfig: JSON.stringify(extraConfigPayload),
|
|
createdAt,
|
|
updatedAt,
|
|
});
|
|
|
|
if (apiToken) {
|
|
accountTokens.push({
|
|
id: nextTokenId++,
|
|
accountId,
|
|
name: 'default',
|
|
token: apiToken,
|
|
tokenGroup: 'default',
|
|
valueStatus: 'ready',
|
|
source: 'legacy',
|
|
enabled: true,
|
|
isDefault: true,
|
|
createdAt,
|
|
updatedAt,
|
|
});
|
|
}
|
|
}
|
|
|
|
return {
|
|
sites,
|
|
accounts,
|
|
accountTokens,
|
|
tokenRoutes,
|
|
routeChannels,
|
|
};
|
|
}
|
|
|
|
function buildPreferencesSectionFromRefBackup(data: RawBackupData): PreferencesBackupSection | null {
|
|
const settings: Array<{ key: string; value: unknown }> = [];
|
|
|
|
if (isRecord(data.preferences)) {
|
|
settings.push({ key: 'legacy_preferences_ref_v2', value: data.preferences });
|
|
}
|
|
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 });
|
|
}
|
|
|
|
if (settings.length === 0) return null;
|
|
return { settings };
|
|
}
|
|
|
|
function parseSettingValue(raw: string | null): unknown {
|
|
if (raw === null || raw === undefined) return null;
|
|
try {
|
|
return JSON.parse(raw);
|
|
} catch {
|
|
return raw;
|
|
}
|
|
}
|
|
|
|
function stringifySettingValue(value: unknown): string {
|
|
return JSON.stringify(value);
|
|
}
|
|
|
|
function isFiniteNumber(value: unknown): value is number {
|
|
return typeof value === 'number' && Number.isFinite(value);
|
|
}
|
|
|
|
function isSettingValueAcceptable(key: string, value: unknown): boolean {
|
|
if (key === 'checkin_cron' || key === 'balance_refresh_cron' || key === 'log_cleanup_cron') {
|
|
return typeof value === 'string' && cron.validate(value);
|
|
}
|
|
|
|
if (key === 'log_cleanup_usage_logs_enabled' || key === 'log_cleanup_program_logs_enabled') {
|
|
return typeof value === 'boolean';
|
|
}
|
|
|
|
if (key === 'log_cleanup_retention_days') {
|
|
return isFiniteNumber(value) && value >= 1;
|
|
}
|
|
|
|
if (key === 'proxy_token') {
|
|
return typeof value === 'string'
|
|
&& value.trim().length >= 6
|
|
&& value.trim().startsWith('sk-');
|
|
}
|
|
|
|
if (key === 'smtp_port') {
|
|
return isFiniteNumber(value) && value > 0;
|
|
}
|
|
|
|
if (key === 'routing_weights') {
|
|
if (!isRecord(value)) return false;
|
|
const keys = ['baseWeightFactor', 'valueScoreFactor', 'costWeight', 'balanceWeight', 'usageWeight'] as const;
|
|
return keys.every((weightKey) => value[weightKey] === undefined || isFiniteNumber(value[weightKey]));
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
async function exportAccountsSection(): Promise<AccountsBackupSection> {
|
|
const sites = await db.select().from(schema.sites).orderBy(asc(schema.sites.id)).all();
|
|
const accounts = await db.select().from(schema.accounts).orderBy(asc(schema.accounts.id)).all();
|
|
const accountTokens = await db.select().from(schema.accountTokens).orderBy(asc(schema.accountTokens.id)).all();
|
|
const tokenRoutes = await db.select().from(schema.tokenRoutes).orderBy(asc(schema.tokenRoutes.id)).all();
|
|
const routeChannels = await db.select().from(schema.routeChannels).orderBy(asc(schema.routeChannels.id)).all();
|
|
|
|
return {
|
|
sites,
|
|
accounts,
|
|
accountTokens,
|
|
tokenRoutes,
|
|
routeChannels,
|
|
};
|
|
}
|
|
|
|
async function exportPreferencesSection(): Promise<PreferencesBackupSection> {
|
|
const settings = (await db.select().from(schema.settings).all())
|
|
.filter((row) => !EXCLUDED_SETTING_KEYS.has(row.key))
|
|
.map((row) => ({
|
|
key: row.key,
|
|
value: parseSettingValue(row.value),
|
|
}));
|
|
|
|
return { settings };
|
|
}
|
|
|
|
export async function exportBackup(type: BackupExportType): Promise<BackupV2> {
|
|
const now = Date.now();
|
|
if (type === 'accounts') {
|
|
return {
|
|
version: BACKUP_VERSION,
|
|
timestamp: now,
|
|
type: 'accounts',
|
|
accounts: await exportAccountsSection(),
|
|
};
|
|
}
|
|
|
|
if (type === 'preferences') {
|
|
return {
|
|
version: BACKUP_VERSION,
|
|
timestamp: now,
|
|
type: 'preferences',
|
|
preferences: await exportPreferencesSection(),
|
|
};
|
|
}
|
|
|
|
return {
|
|
version: BACKUP_VERSION,
|
|
timestamp: now,
|
|
accounts: await exportAccountsSection(),
|
|
preferences: await exportPreferencesSection(),
|
|
};
|
|
}
|
|
|
|
function coerceAccountsSection(input: unknown): AccountsBackupSection | null {
|
|
if (!isRecord(input)) return null;
|
|
|
|
const sites = Array.isArray(input.sites) ? input.sites as SiteRow[] : null;
|
|
const accounts = Array.isArray(input.accounts) ? input.accounts as AccountRow[] : null;
|
|
const accountTokens = Array.isArray(input.accountTokens) ? input.accountTokens as AccountTokenRow[] : null;
|
|
const tokenRoutes = Array.isArray(input.tokenRoutes) ? input.tokenRoutes as TokenRouteRow[] : null;
|
|
const routeChannels = Array.isArray(input.routeChannels) ? input.routeChannels as RouteChannelRow[] : null;
|
|
|
|
if (!sites || !accounts || !accountTokens || !tokenRoutes || !routeChannels) return null;
|
|
|
|
return {
|
|
sites,
|
|
accounts,
|
|
accountTokens,
|
|
tokenRoutes,
|
|
routeChannels,
|
|
};
|
|
}
|
|
|
|
function coercePreferencesSection(input: unknown): PreferencesBackupSection | null {
|
|
if (!isRecord(input)) return null;
|
|
const settingsRaw = input.settings;
|
|
if (!Array.isArray(settingsRaw)) return null;
|
|
|
|
const settings = settingsRaw
|
|
.map((row) => {
|
|
if (!isRecord(row)) return null;
|
|
const key = typeof row.key === 'string' ? row.key.trim() : '';
|
|
if (!key || EXCLUDED_SETTING_KEYS.has(key)) return null;
|
|
return { key, value: row.value };
|
|
})
|
|
.filter((row): row is { key: string; value: unknown } => !!row);
|
|
|
|
return { settings };
|
|
}
|
|
|
|
function detectAccountsSection(data: RawBackupData): AccountsBackupSection | null {
|
|
const rootMatch = coerceAccountsSection(data);
|
|
if (rootMatch) return rootMatch;
|
|
|
|
if ('accounts' in data) {
|
|
const nested = coerceAccountsSection(data.accounts);
|
|
if (nested) return nested;
|
|
}
|
|
|
|
if (isRecord(data.data) && 'accounts' in data.data) {
|
|
const legacyNested = coerceAccountsSection((data.data as Record<string, unknown>).accounts);
|
|
if (legacyNested) return legacyNested;
|
|
}
|
|
|
|
const refFormat = buildAccountsSectionFromRefBackup(data);
|
|
if (refFormat) return refFormat;
|
|
|
|
return null;
|
|
}
|
|
|
|
function detectPreferencesSection(data: RawBackupData): PreferencesBackupSection | null {
|
|
const rootMatch = coercePreferencesSection(data);
|
|
if (rootMatch) return rootMatch;
|
|
|
|
if ('preferences' in data) {
|
|
const nested = coercePreferencesSection(data.preferences);
|
|
if (nested) return nested;
|
|
}
|
|
|
|
if (isRecord(data.data) && 'preferences' in data.data) {
|
|
const legacyNested = coercePreferencesSection((data.data as Record<string, unknown>).preferences);
|
|
if (legacyNested) return legacyNested;
|
|
}
|
|
|
|
const refFormat = buildPreferencesSectionFromRefBackup(data);
|
|
if (refFormat) return refFormat;
|
|
|
|
return null;
|
|
}
|
|
|
|
async function importAccountsSection(section: AccountsBackupSection): Promise<void> {
|
|
await db.transaction(async (tx) => {
|
|
await tx.delete(schema.routeChannels).run();
|
|
await tx.delete(schema.tokenRoutes).run();
|
|
await tx.delete(schema.tokenModelAvailability).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.accounts).run();
|
|
await tx.delete(schema.sites).run();
|
|
|
|
for (const row of section.sites) {
|
|
await tx.insert(schema.sites).values({
|
|
id: row.id,
|
|
name: row.name,
|
|
url: row.url,
|
|
externalCheckinUrl: row.externalCheckinUrl ?? null,
|
|
platform: row.platform,
|
|
proxyUrl: row.proxyUrl ?? null,
|
|
useSystemProxy: row.useSystemProxy ?? false,
|
|
customHeaders: row.customHeaders ?? null,
|
|
status: row.status || 'active',
|
|
isPinned: row.isPinned ?? false,
|
|
sortOrder: row.sortOrder ?? 0,
|
|
globalWeight: row.globalWeight ?? 1,
|
|
apiKey: row.apiKey,
|
|
createdAt: row.createdAt,
|
|
updatedAt: row.updatedAt,
|
|
}).run();
|
|
}
|
|
|
|
for (const row of section.accounts) {
|
|
const oauthColumns = resolveImportedOauthColumns(row);
|
|
await tx.insert(schema.accounts).values({
|
|
id: row.id,
|
|
siteId: row.siteId,
|
|
username: row.username,
|
|
accessToken: row.accessToken,
|
|
apiToken: row.apiToken,
|
|
oauthProvider: oauthColumns.oauthProvider,
|
|
oauthAccountKey: oauthColumns.oauthAccountKey,
|
|
oauthProjectId: oauthColumns.oauthProjectId,
|
|
balance: row.balance,
|
|
balanceUsed: row.balanceUsed,
|
|
quota: row.quota,
|
|
unitCost: row.unitCost,
|
|
valueScore: row.valueScore,
|
|
status: row.status,
|
|
isPinned: row.isPinned ?? false,
|
|
sortOrder: row.sortOrder ?? 0,
|
|
checkinEnabled: row.checkinEnabled,
|
|
lastCheckinAt: row.lastCheckinAt,
|
|
lastBalanceRefresh: row.lastBalanceRefresh,
|
|
extraConfig: row.extraConfig,
|
|
createdAt: row.createdAt,
|
|
updatedAt: row.updatedAt,
|
|
}).run();
|
|
}
|
|
|
|
for (const row of section.accountTokens) {
|
|
await tx.insert(schema.accountTokens).values({
|
|
id: row.id,
|
|
accountId: row.accountId,
|
|
name: row.name,
|
|
token: row.token,
|
|
tokenGroup: row.tokenGroup ?? null,
|
|
valueStatus: row.valueStatus ?? 'ready',
|
|
source: row.source,
|
|
enabled: row.enabled,
|
|
isDefault: row.isDefault,
|
|
createdAt: row.createdAt,
|
|
updatedAt: row.updatedAt,
|
|
}).run();
|
|
}
|
|
|
|
for (const row of section.tokenRoutes) {
|
|
await tx.insert(schema.tokenRoutes).values({
|
|
id: row.id,
|
|
modelPattern: row.modelPattern,
|
|
displayName: row.displayName ?? null,
|
|
displayIcon: row.displayIcon ?? null,
|
|
modelMapping: row.modelMapping,
|
|
enabled: row.enabled,
|
|
createdAt: row.createdAt,
|
|
updatedAt: row.updatedAt,
|
|
}).run();
|
|
}
|
|
|
|
for (const row of section.routeChannels) {
|
|
await tx.insert(schema.routeChannels).values({
|
|
id: row.id,
|
|
routeId: row.routeId,
|
|
accountId: row.accountId,
|
|
tokenId: row.tokenId,
|
|
sourceModel: row.sourceModel ?? null,
|
|
priority: row.priority,
|
|
weight: row.weight,
|
|
enabled: row.enabled,
|
|
manualOverride: row.manualOverride,
|
|
successCount: row.successCount,
|
|
failCount: row.failCount,
|
|
totalLatencyMs: row.totalLatencyMs,
|
|
totalCost: row.totalCost,
|
|
lastUsedAt: row.lastUsedAt,
|
|
lastFailAt: row.lastFailAt,
|
|
cooldownUntil: row.cooldownUntil,
|
|
}).run();
|
|
}
|
|
});
|
|
}
|
|
|
|
async function importPreferencesSection(section: PreferencesBackupSection): Promise<Array<{ key: string; value: unknown }>> {
|
|
const applied: Array<{ key: string; value: unknown }> = [];
|
|
|
|
await db.transaction(async (tx) => {
|
|
for (const row of section.settings) {
|
|
if (!isSettingValueAcceptable(row.key, row.value)) continue;
|
|
|
|
await upsertSetting(row.key, row.value, tx);
|
|
applied.push({ key: row.key, value: row.value });
|
|
}
|
|
});
|
|
|
|
return applied;
|
|
}
|
|
|
|
export async function importBackup(data: RawBackupData): Promise<BackupImportResult> {
|
|
if (!isRecord(data)) {
|
|
throw new Error('导入数据格式错误:必须为 JSON 对象');
|
|
}
|
|
|
|
if (!('timestamp' in data) || data.timestamp === null || data.timestamp === undefined) {
|
|
throw new Error('导入数据格式错误:缺少 timestamp');
|
|
}
|
|
|
|
const accountsSection = detectAccountsSection(data);
|
|
const preferencesSection = detectPreferencesSection(data);
|
|
|
|
const type = typeof data.type === 'string' ? data.type : '';
|
|
const accountsRequested = type === 'accounts' || !!accountsSection;
|
|
const preferencesRequested = type === 'preferences' || !!preferencesSection;
|
|
|
|
if (!accountsRequested && !preferencesRequested) {
|
|
throw new Error('导入数据中没有可识别的账号或设置数据');
|
|
}
|
|
|
|
let accountsImported = false;
|
|
let preferencesImported = false;
|
|
let appliedSettings: Array<{ key: string; value: unknown }> = [];
|
|
|
|
if (accountsRequested) {
|
|
if (!accountsSection) {
|
|
throw new Error('导入数据格式错误:账号数据结构不正确');
|
|
}
|
|
await importAccountsSection(accountsSection);
|
|
accountsImported = true;
|
|
}
|
|
|
|
if (preferencesRequested) {
|
|
if (!preferencesSection) {
|
|
throw new Error('导入数据格式错误:设置数据结构不正确');
|
|
}
|
|
appliedSettings = await importPreferencesSection(preferencesSection);
|
|
preferencesImported = true;
|
|
}
|
|
|
|
return {
|
|
allImported: (!accountsRequested || accountsImported) && (!preferencesRequested || preferencesImported),
|
|
sections: {
|
|
accounts: accountsImported,
|
|
preferences: preferencesImported,
|
|
},
|
|
appliedSettings,
|
|
};
|
|
}
|