Refactor Gemini model display: filter old versions, group low/high pкo, and improve naming (#114)
* Refactor Gemini model display: filter old versions, group low/high pro, and improve naming * Fix potential negative minutes and refine Gemini 3 Pro grouping logic based on PR feedback * The account change process has been sped up * Address PR review: enhance error message and parameterize polling interval in process handler * Address PR review: fix potential race condition in database backup logic * fix: prevent renderer white screen and harden sentry gating * feat: localize model labels * chore: enable hoisted node-linker * chore: ignore .agent/.claude/.codex in prettier --------- Co-authored-by: Draculabo <[email protected]>
This commit is contained in:
1 parent
b9497648c6
commit
8ed4593f73
162 files changed
+2981
-2541
No files matched your search
+29
-28
@@ -197,20 +197,25 @@ export async function switchCloudAccount(accountId: string): Promise<void> {
|
||||
account.device_profile = generated;
|
||||
}
|
||||
|
||||
// 1. Ensure token is fresh before injecting
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
if (account.token.expiry_timestamp < now + 300) {
|
||||
logger.info(`Token for ${account.email} near expiry, refreshing before switch...`);
|
||||
try {
|
||||
const newTokenData = await GoogleAPIService.refreshAccessToken(account.token.refresh_token);
|
||||
account.token.access_token = newTokenData.access_token;
|
||||
account.token.expires_in = newTokenData.expires_in;
|
||||
account.token.expiry_timestamp = now + newTokenData.expires_in;
|
||||
await CloudAccountRepo.updateToken(account.id, account.token);
|
||||
} catch (e) {
|
||||
logger.warn('Failed to refresh token before switch, trying with existing token', e);
|
||||
// 1. Prepare token refresh promise (start it in parallel with process exit)
|
||||
const tokenRefreshPromise = (async () => {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
if (account.token.expiry_timestamp < now + 1200) { // Increased buffer to 20m
|
||||
logger.info(`Token for ${account.email} near expiry, refreshing in parallel...`);
|
||||
try {
|
||||
const newTokenData = await GoogleAPIService.refreshAccessToken(
|
||||
account.token.refresh_token,
|
||||
);
|
||||
account.token.access_token = newTokenData.access_token;
|
||||
account.token.expires_in = newTokenData.expires_in;
|
||||
account.token.expiry_timestamp = now + newTokenData.expires_in;
|
||||
await CloudAccountRepo.updateToken(account.id, account.token);
|
||||
logger.info(`Token refreshed for ${account.email}`);
|
||||
} catch (e) {
|
||||
logger.warn('Failed to refresh token in parallel, will try to use existing', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
await executeSwitchFlow({
|
||||
scope: 'cloud',
|
||||
@@ -218,29 +223,25 @@ export async function switchCloudAccount(accountId: string): Promise<void> {
|
||||
applyFingerprint: isIdentityProfileApplyEnabled(),
|
||||
processExitTimeoutMs: 10000,
|
||||
performSwitch: async () => {
|
||||
// 3. Backup Database (New Logic)
|
||||
const dbPaths = getAntigravityDbPaths();
|
||||
// Find the valid DB path
|
||||
let dbPath: string | null = null;
|
||||
for (const p of dbPaths) {
|
||||
if (fs.existsSync(p)) {
|
||||
dbPath = p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Wait for token refresh to complete before injection if it was started
|
||||
await tokenRefreshPromise;
|
||||
|
||||
if (dbPath) {
|
||||
// 3. Backup Database (Optimized to avoid race conditions)
|
||||
const dbPaths = getAntigravityDbPaths();
|
||||
for (const dbPath of dbPaths) {
|
||||
try {
|
||||
const backupPath = `${dbPath}.backup`;
|
||||
fs.copyFileSync(dbPath, backupPath);
|
||||
await fs.promises.copyFile(dbPath, backupPath);
|
||||
logger.info(`Backed up database to ${backupPath}`);
|
||||
} catch (e) {
|
||||
logger.error('Failed to backup database', e);
|
||||
break; // Success, stop trying other paths
|
||||
} catch (e: any) {
|
||||
// If file not found, just try the next path
|
||||
if (e.code === 'ENOENT') continue;
|
||||
logger.error(`Failed to backup database at ${dbPath}`, e);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Inject Token
|
||||
// injectedCloudToken uses direct DB/FS access which is sync better-sqlite3.
|
||||
CloudAccountRepo.injectCloudToken(account);
|
||||
|
||||
// 5. Update usage and active status
|
||||
|
||||
@@ -20,10 +20,7 @@ import {
|
||||
startAuthFlow,
|
||||
} from './handler';
|
||||
import { CloudAccountSchema } from '../../types/cloudAccount';
|
||||
import {
|
||||
DeviceProfileSchema,
|
||||
DeviceProfilesSnapshotSchema,
|
||||
} from '../../types/account';
|
||||
import { DeviceProfileSchema, DeviceProfilesSnapshotSchema } from '../../types/account';
|
||||
import { CloudAccountRepo } from '../database/cloudHandler';
|
||||
import { logger } from '../../utils/logger';
|
||||
import { getSwitchMetricsSnapshot } from '../switchMetrics';
|
||||
|
||||
+25
-20
@@ -34,10 +34,8 @@ export interface DeviceHardeningSnapshot {
|
||||
lastFailureAt: number | null;
|
||||
}
|
||||
|
||||
const UUID_V4_REGEX =
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
||||
const SQM_ID_REGEX =
|
||||
/^\{[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}\}$/;
|
||||
const UUID_V4_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
||||
const SQM_ID_REGEX = /^\{[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}\}$/;
|
||||
|
||||
interface DeviceProfileTemplate {
|
||||
machineIdPrefix: string;
|
||||
@@ -183,7 +181,11 @@ function getTelemetryField(storage: Record<string, unknown>, key: string): strin
|
||||
}
|
||||
|
||||
function ensureTelemetryObject(storage: Record<string, unknown>): Record<string, unknown> {
|
||||
if (!storage.telemetry || typeof storage.telemetry !== 'object' || Array.isArray(storage.telemetry)) {
|
||||
if (
|
||||
!storage.telemetry ||
|
||||
typeof storage.telemetry !== 'object' ||
|
||||
Array.isArray(storage.telemetry)
|
||||
) {
|
||||
storage.telemetry = {};
|
||||
}
|
||||
return storage.telemetry as Record<string, unknown>;
|
||||
@@ -272,9 +274,7 @@ function pickDeviceProfileTemplate(): DeviceProfileTemplate {
|
||||
}
|
||||
|
||||
function deterministicUuidFromSeed(seed: string, namespace: string): string {
|
||||
const hash = createHash('sha256')
|
||||
.update(`${namespace}:${seed}`)
|
||||
.digest();
|
||||
const hash = createHash('sha256').update(`${namespace}:${seed}`).digest();
|
||||
const bytes = Buffer.from(hash.subarray(0, 16));
|
||||
|
||||
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
||||
@@ -463,10 +463,18 @@ function buildApplyError(
|
||||
): Error {
|
||||
const message = original instanceof Error ? original.message : String(original);
|
||||
const wrapped = new Error(`device_apply_failed:${reason}:${stage}:${message}`);
|
||||
(wrapped as Error & { deviceFailureReason?: DeviceApplyFailureReason; deviceFailureStage?: string })
|
||||
.deviceFailureReason = reason;
|
||||
(wrapped as Error & { deviceFailureReason?: DeviceApplyFailureReason; deviceFailureStage?: string })
|
||||
.deviceFailureStage = stage;
|
||||
(
|
||||
wrapped as Error & {
|
||||
deviceFailureReason?: DeviceApplyFailureReason;
|
||||
deviceFailureStage?: string;
|
||||
}
|
||||
).deviceFailureReason = reason;
|
||||
(
|
||||
wrapped as Error & {
|
||||
deviceFailureReason?: DeviceApplyFailureReason;
|
||||
deviceFailureStage?: string;
|
||||
}
|
||||
).deviceFailureStage = stage;
|
||||
return wrapped;
|
||||
}
|
||||
|
||||
@@ -490,12 +498,8 @@ export function isIdentityProfileApplyEnabled(): boolean {
|
||||
return !isSafeModeActiveNow();
|
||||
}
|
||||
const normalized = raw.trim().toLowerCase();
|
||||
const envEnabled = (
|
||||
normalized !== '0' &&
|
||||
normalized !== 'false' &&
|
||||
normalized !== 'off' &&
|
||||
normalized !== 'no'
|
||||
);
|
||||
const envEnabled =
|
||||
normalized !== '0' && normalized !== 'false' && normalized !== 'off' && normalized !== 'no';
|
||||
if (!envEnabled) {
|
||||
return false;
|
||||
}
|
||||
@@ -605,8 +609,9 @@ export function syncStateServiceMachineIdValue(serviceMachineId: string, dbPath?
|
||||
db = new Database(targetDbPath);
|
||||
db.pragma('busy_timeout = 3000');
|
||||
db.exec('CREATE TABLE IF NOT EXISTS ItemTable (key TEXT PRIMARY KEY, value TEXT);');
|
||||
db.prepare("INSERT OR REPLACE INTO ItemTable (key, value) VALUES ('storage.serviceMachineId', ?)")
|
||||
.run(serviceMachineId);
|
||||
db.prepare(
|
||||
"INSERT OR REPLACE INTO ItemTable (key, value) VALUES ('storage.serviceMachineId', ?)",
|
||||
).run(serviceMachineId);
|
||||
return;
|
||||
} catch (error) {
|
||||
if (isSqliteBusyError(error) && attempt < SQLITE_RETRY_COUNT) {
|
||||
|
||||
@@ -345,15 +345,18 @@ export async function closeAntigravity(): Promise<void> {
|
||||
* @param timeoutMs {number} The timeout in milliseconds.
|
||||
* @returns {Promise<void>} A promise that resolves when the process exits.
|
||||
*/
|
||||
export async function _waitForProcessExit(timeoutMs: number): Promise<void> {
|
||||
export async function _waitForProcessExit(
|
||||
timeoutMs: number,
|
||||
pollInterval = 100, // Make it configurable, but keep fast 100ms default
|
||||
): Promise<void> {
|
||||
const startTime = Date.now();
|
||||
while (Date.now() - startTime < timeoutMs) {
|
||||
if (!(await isProcessRunning())) {
|
||||
return;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
||||
}
|
||||
throw new Error('Process did not exit within timeout');
|
||||
throw new Error(`Antigravity process did not exit within ${timeoutMs}ms`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,11 @@ import { type DeviceProfile } from '../types/account';
|
||||
import { logger } from '../utils/logger';
|
||||
import { closeAntigravity, startAntigravity, _waitForProcessExit } from './process/handler';
|
||||
import { applyDeviceProfile } from './device/handler';
|
||||
import { type SwitchFailureReason, recordSwitchFailure, recordSwitchSuccess } from './switchMetrics';
|
||||
import {
|
||||
type SwitchFailureReason,
|
||||
recordSwitchFailure,
|
||||
recordSwitchSuccess,
|
||||
} from './switchMetrics';
|
||||
|
||||
export interface SwitchFlowOptions {
|
||||
scope: 'local' | 'cloud';
|
||||
@@ -47,13 +51,7 @@ function toSwitchFailureReason(stage: string, error: unknown): SwitchFailureReas
|
||||
}
|
||||
|
||||
export async function executeSwitchFlow(options: SwitchFlowOptions): Promise<void> {
|
||||
const {
|
||||
scope,
|
||||
targetProfile,
|
||||
applyFingerprint,
|
||||
processExitTimeoutMs,
|
||||
performSwitch,
|
||||
} = options;
|
||||
const { scope, targetProfile, applyFingerprint, processExitTimeoutMs, performSwitch } = options;
|
||||
|
||||
let stage = 'close';
|
||||
try {
|
||||
|
||||
Reference in new issue
Block a user