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:
AngeloooooandDraculabo authored and GitHub committed 2026-02-19 19:37:30 +08:00
1 parent b9497648c6
commit 8ed4593f73
162 files changed
+2981 -2541

No files matched your search

+29 -28
View File
@@ -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
+1 -4
View File
@@ -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
View File
@@ -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) {
+6 -3
View File
@@ -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`);
}
/**
+6 -8
View File
@@ -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 {