Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions apps/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,34 @@
import { defineConfig } from "@playwright/test";

const PORT = 3200;
const LOCAL_TEST_HOST = "localhost";

const noProxyHosts = ["localhost", "127.0.0.1", "::1"];
const noProxy = [process.env.NO_PROXY, process.env.no_proxy, ...noProxyHosts]
.filter(Boolean)
.join(",");

process.env.NO_PROXY = noProxy;
process.env.no_proxy = noProxy;

export default defineConfig({
testDir: "./tests",
timeout: 30_000,
fullyParallel: false,
use: {
baseURL: `http://127.0.0.1:${PORT}`,
baseURL: `http://${LOCAL_TEST_HOST}:${PORT}`,
trace: "on-first-retry",
video: "retain-on-failure",
},
webServer: {
command: "node tests/support/static-server.mjs",
url: `http://127.0.0.1:${PORT}`,
reuseExistingServer: true,
command: "pnpm run build:desktop && node tests/support/static-server.mjs",
url: `http://${LOCAL_TEST_HOST}:${PORT}`,
reuseExistingServer: false,
timeout: 120_000,
env: {
NO_PROXY: noProxy,
PORT: String(PORT),
no_proxy: noProxy,
},
},
});
27 changes: 26 additions & 1 deletion apps/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use tauri::Manager;
use serde::Serialize;
use tauri::{Emitter, Manager};

mod app_shell;
mod app_storage;
Expand All @@ -12,6 +13,16 @@ use app_shell::{
CLOSE_TO_TRAY_ON_CLOSE, TRAY_AVAILABLE,
};

const USAGE_REFRESH_COMPLETED_EVENT: &str = "usage-refresh-completed";

#[derive(Clone, Serialize)]
struct UsageRefreshCompletedPayload {
source: &'static str,
processed: usize,
total: usize,
completed_at: i64,
}

/// 函数 `run`
///
/// 作者: gaohongshun
Expand Down Expand Up @@ -49,6 +60,20 @@ pub fn run() {
if let Ok(log_dir) = app.path().app_log_dir() {
log::info!("log dir: {}", log_dir.display());
}
let usage_refresh_event_app = app.handle().clone();
codexmanager_service::set_usage_refresh_completed_handler(move |event| {
let payload = UsageRefreshCompletedPayload {
source: event.source,
processed: event.processed,
total: event.total,
completed_at: event.completed_at,
};
if let Err(err) = usage_refresh_event_app
.emit(USAGE_REFRESH_COMPLETED_EVENT, payload)
{
log::warn!("emit usage refresh completed event failed: {}", err);
}
});
if let Err(err) = setup_tray(app.handle()) {
TRAY_AVAILABLE.store(false, std::sync::atomic::Ordering::Relaxed);
CLOSE_TO_TRAY_ON_CLOSE.store(false, std::sync::atomic::Ordering::Relaxed);
Expand Down
112 changes: 109 additions & 3 deletions apps/src/hooks/useAccounts.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useMemo } from "react";
import { useEffect, useMemo, useRef } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
import { accountClient } from "@/lib/api/account-client";
Expand All @@ -10,13 +10,14 @@ import {
STARTUP_SNAPSHOT_REQUEST_LOG_LIMIT,
} from "@/lib/api/startup-snapshot";
import { getAppErrorMessage } from "@/lib/api/transport";
import { listenUsageRefreshCompleted } from "@/lib/api/usage-refresh-events";
import { useDesktopPageActive } from "@/hooks/useDesktopPageActive";
import { useDeferredDesktopActivation } from "@/hooks/useDeferredDesktopActivation";
import { useLocalDayRange } from "@/hooks/useLocalDayRange";
import { useRuntimeCapabilities } from "@/hooks/useRuntimeCapabilities";
import { useI18n } from "@/lib/i18n/provider";
import { useAppStore } from "@/lib/store/useAppStore";
import { AccountListResult, StartupSnapshot } from "@/types";
import { AccountListResult, AccountUsage, StartupSnapshot } from "@/types";

type ImportByDirectoryResult = Awaited<ReturnType<typeof accountClient.importByDirectory>>;
type ImportByFileResult = Awaited<ReturnType<typeof accountClient.importByFile>>;
Expand Down Expand Up @@ -112,6 +113,39 @@ function getAccountsAutoRefreshIntervalMs(
return Math.max(1, intervalSecs) * 1000;
}

function getUsageListRefreshIntervalMs(
enabled: boolean,
intervalSecs: number,
): number | false {
const intervalMs = getAccountsAutoRefreshIntervalMs(enabled, intervalSecs);
if (!intervalMs) {
return false;
}
return Math.min(5_000, intervalMs);
}

function buildUsageListFingerprint(usages: AccountUsage[]): string {
if (usages.length === 0) {
return "";
}

return usages
.map((usage) =>
[
usage.accountId,
usage.capturedAt ?? "",
usage.usedPercent ?? "",
usage.secondaryUsedPercent ?? "",
usage.resetsAt ?? "",
usage.secondaryResetsAt ?? "",
usage.availabilityStatus ?? "",
usage.creditsJson ?? "",
].join(":"),
)
.sort()
.join("|");
}

/**
* 函数 `useAccounts`
*
Expand Down Expand Up @@ -141,6 +175,11 @@ export function useAccounts() {
areAccountQueriesEnabled && backgroundTasks.usagePollingEnabled,
backgroundTasks.usagePollIntervalSecs,
);
const usageListRefreshIntervalMs = getUsageListRefreshIntervalMs(
areAccountQueriesEnabled && backgroundTasks.usagePollingEnabled,
backgroundTasks.usagePollIntervalSecs,
);
const usageListFingerprintRef = useRef<string | null>(null);
const startupSnapshot = queryClient.getQueryData<StartupSnapshot>(
buildStartupSnapshotQueryKey(
serviceStatus.addr,
Expand Down Expand Up @@ -197,12 +236,79 @@ export function useAccounts() {
queryFn: () => accountClient.listUsage(),
enabled: areAccountQueriesEnabled,
retry: 1,
refetchInterval: accountsAutoRefreshIntervalMs,
refetchInterval: usageListRefreshIntervalMs,
refetchIntervalInBackground: false,
placeholderData: (previousData) =>
previousData || (startupUsages.length > 0 ? startupUsages : undefined),
});

const usageListFingerprint = useMemo(
() => buildUsageListFingerprint(usagesQuery.data || []),
[usagesQuery.data],
);

useEffect(() => {
if (!areAccountQueriesEnabled) {
return;
}

let disposed = false;
let unlisten: (() => void) | null = null;
const refreshVisibleUsageData = () => {
void Promise.all([
queryClient.refetchQueries({ queryKey: ["usage", "list"], type: "active" }),
queryClient.refetchQueries({ queryKey: ["accounts", "list"], type: "active" }),
queryClient.invalidateQueries({ queryKey: ["usage-aggregate"] }),
queryClient.invalidateQueries({ queryKey: ["today-summary"] }),
queryClient.invalidateQueries({ queryKey: ["startup-snapshot"] }),
]);
};

void listenUsageRefreshCompleted(() => {
refreshVisibleUsageData();
}).then((cleanup) => {
if (disposed) {
cleanup();
return;
}
unlisten = cleanup;
});

return () => {
disposed = true;
unlisten?.();
};
}, [areAccountQueriesEnabled, queryClient]);

useEffect(() => {
if (!areAccountQueriesEnabled) {
usageListFingerprintRef.current = null;
return;
}

if (!usagesQuery.isFetched) {
return;
}

const previousFingerprint = usageListFingerprintRef.current;
usageListFingerprintRef.current = usageListFingerprint;
if (previousFingerprint == null || previousFingerprint === usageListFingerprint) {
return;
}

void Promise.all([
queryClient.invalidateQueries({ queryKey: ["accounts", "list"] }),
queryClient.invalidateQueries({ queryKey: ["usage-aggregate"] }),
queryClient.invalidateQueries({ queryKey: ["today-summary"] }),
queryClient.invalidateQueries({ queryKey: ["startup-snapshot"] }),
]);
}, [
areAccountQueriesEnabled,
queryClient,
usageListFingerprint,
usagesQuery.isFetched,
]);

const accounts = useMemo(() => {
return attachUsagesToAccounts(
accountsQuery.data?.items || [],
Expand Down
4 changes: 4 additions & 0 deletions apps/src/lib/api/account-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,10 @@ export const accountClient = {
);
return normalizeUsageSnapshot(unwrapUsageSnapshotPayload(result));
},
async getLatestUsage(): Promise<AccountUsage | null> {
const result = await invoke<unknown>("service_usage_read", withAddr());
return normalizeUsageSnapshot(unwrapUsageSnapshotPayload(result));
},
async listUsage(): Promise<AccountUsage[]> {
const result = await invoke<unknown>("service_usage_list", withAddr());
return normalizeUsageList(result);
Expand Down
53 changes: 53 additions & 0 deletions apps/src/lib/api/usage-refresh-events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { isTauriRuntime } from "./transport";

export const USAGE_REFRESH_COMPLETED_EVENT = "usage-refresh-completed";

export interface UsageRefreshCompletedPayload {
source?: string;
processed?: number;
total?: number;
completedAt?: number;
completed_at?: number;
}

export type UsageRefreshCompletedHandler = (
payload: UsageRefreshCompletedPayload
) => void;

type Unlisten = () => void;

function readUsageRefreshEventPayload(event: Event): UsageRefreshCompletedPayload {
if (event instanceof CustomEvent && typeof event.detail === "object" && event.detail) {
return event.detail as UsageRefreshCompletedPayload;
}
return {};
}

export async function listenUsageRefreshCompleted(
handler: UsageRefreshCompletedHandler
): Promise<Unlisten> {
if (typeof window === "undefined") {
return () => {};
}

const handleWindowEvent = (event: Event) => {
handler(readUsageRefreshEventPayload(event));
};
window.addEventListener(USAGE_REFRESH_COMPLETED_EVENT, handleWindowEvent);

let unlistenTauri: Unlisten | null = null;
if (isTauriRuntime()) {
const { listen } = await import("@tauri-apps/api/event");
unlistenTauri = await listen<UsageRefreshCompletedPayload>(
USAGE_REFRESH_COMPLETED_EVENT,
(event) => {
handler(event.payload || {});
},
);
}

return () => {
window.removeEventListener(USAGE_REFRESH_COMPLETED_EVENT, handleWindowEvent);
unlistenTauri?.();
};
}
Loading