mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 14:16:43 +00:00
fix(cliproxy): degrade gracefully on GitHub release 403s
- fall back to stale CLIProxy version caches instead of surfacing 500s - remove duplicate dashboard update-check fetches and disable retry/focus refetch - add backend and UI regressions for the degraded-path behavior
This commit is contained in:
@@ -33,6 +33,23 @@ export function getVersionPinPath(backend: CLIProxyBackend = DEFAULT_BACKEND): s
|
||||
* Read version cache if still valid (backend-specific)
|
||||
*/
|
||||
export function readVersionCache(backend: CLIProxyBackend = DEFAULT_BACKEND): VersionCache | null {
|
||||
return readVersionCacheInternal(backend, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read version cache even if expired.
|
||||
* Used as a resilience fallback when GitHub release lookups fail.
|
||||
*/
|
||||
export function readStaleVersionCache(
|
||||
backend: CLIProxyBackend = DEFAULT_BACKEND
|
||||
): VersionCache | null {
|
||||
return readVersionCacheInternal(backend, true);
|
||||
}
|
||||
|
||||
function readVersionCacheInternal(
|
||||
backend: CLIProxyBackend,
|
||||
allowExpired: boolean
|
||||
): VersionCache | null {
|
||||
const cachePath = getVersionCachePath(backend);
|
||||
if (!fs.existsSync(cachePath)) {
|
||||
return null;
|
||||
@@ -42,8 +59,8 @@ export function readVersionCache(backend: CLIProxyBackend = DEFAULT_BACKEND): Ve
|
||||
const content = fs.readFileSync(cachePath, 'utf8');
|
||||
const cache: VersionCache = JSON.parse(content);
|
||||
|
||||
// Check if cache is still valid
|
||||
if (Date.now() - cache.checkedAt < VERSION_CACHE_DURATION_MS) {
|
||||
// Check if cache is still valid, unless caller explicitly allows stale fallback.
|
||||
if (allowExpired || Date.now() - cache.checkedAt < VERSION_CACHE_DURATION_MS) {
|
||||
return cache;
|
||||
}
|
||||
|
||||
@@ -192,6 +209,23 @@ export function getVersionListCachePath(backend: CLIProxyBackend = DEFAULT_BACKE
|
||||
*/
|
||||
export function readVersionListCache(
|
||||
backend: CLIProxyBackend = DEFAULT_BACKEND
|
||||
): VersionListCache | null {
|
||||
return readVersionListCacheInternal(backend, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read version list cache even if expired.
|
||||
* Used as a resilience fallback when GitHub release lookups fail.
|
||||
*/
|
||||
export function readStaleVersionListCache(
|
||||
backend: CLIProxyBackend = DEFAULT_BACKEND
|
||||
): VersionListCache | null {
|
||||
return readVersionListCacheInternal(backend, true);
|
||||
}
|
||||
|
||||
function readVersionListCacheInternal(
|
||||
backend: CLIProxyBackend,
|
||||
allowExpired: boolean
|
||||
): VersionListCache | null {
|
||||
const cachePath = getVersionListCachePath(backend);
|
||||
if (!fs.existsSync(cachePath)) {
|
||||
@@ -202,8 +236,8 @@ export function readVersionListCache(
|
||||
const content = fs.readFileSync(cachePath, 'utf8');
|
||||
const cache: VersionListCache = JSON.parse(content);
|
||||
|
||||
// Check if cache is still valid (1 hour)
|
||||
if (Date.now() - cache.checkedAt < VERSION_CACHE_DURATION_MS) {
|
||||
// Check if cache is still valid, unless caller explicitly allows stale fallback.
|
||||
if (allowExpired || Date.now() - cache.checkedAt < VERSION_CACHE_DURATION_MS) {
|
||||
return cache;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,9 +6,11 @@
|
||||
import { fetchJson } from './downloader';
|
||||
import {
|
||||
readVersionCache,
|
||||
readStaleVersionCache,
|
||||
writeVersionCache,
|
||||
readInstalledVersion,
|
||||
readVersionListCache,
|
||||
readStaleVersionListCache,
|
||||
writeVersionListCache,
|
||||
} from './version-cache';
|
||||
import { UpdateCheckResult, VersionListResult, getGitHubApiUrls } from './types';
|
||||
@@ -19,6 +21,18 @@ import {
|
||||
} from '../platform-detector';
|
||||
import type { CLIProxyBackend } from '../types';
|
||||
|
||||
interface FetchLatestVersionDeps {
|
||||
fetchJsonFn?: typeof fetchJson;
|
||||
}
|
||||
|
||||
interface CheckForUpdatesDeps {
|
||||
fetchLatestVersionFn?: (verbose: boolean, backend: CLIProxyBackend) => Promise<string>;
|
||||
}
|
||||
|
||||
interface FetchAllVersionsDeps {
|
||||
fetchJsonFn?: typeof fetchJson;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare semver versions (true if latest > current)
|
||||
* Handles CLIProxyAPIPlus versioning: strips -0 suffix before comparison
|
||||
@@ -61,10 +75,11 @@ export function isVersionFaulty(version: string): boolean {
|
||||
*/
|
||||
export async function fetchLatestVersion(
|
||||
verbose = false,
|
||||
backend: CLIProxyBackend = DEFAULT_BACKEND
|
||||
backend: CLIProxyBackend = DEFAULT_BACKEND,
|
||||
deps: FetchLatestVersionDeps = {}
|
||||
): Promise<string> {
|
||||
const urls = getGitHubApiUrls(backend);
|
||||
const response = await fetchJson(urls.latestRelease, verbose);
|
||||
const response = await (deps.fetchJsonFn ?? fetchJson)(urls.latestRelease, verbose);
|
||||
|
||||
// Extract version from tag_name (format: "v6.5.27" or "6.5.27")
|
||||
const tagName = response.tag_name as string;
|
||||
@@ -84,7 +99,8 @@ export async function checkForUpdates(
|
||||
binPath: string,
|
||||
configVersion: string,
|
||||
verbose = false,
|
||||
backend: CLIProxyBackend = DEFAULT_BACKEND
|
||||
backend: CLIProxyBackend = DEFAULT_BACKEND,
|
||||
deps: CheckForUpdatesDeps = {}
|
||||
): Promise<UpdateCheckResult> {
|
||||
const currentVersion = readInstalledVersion(binPath, configVersion);
|
||||
|
||||
@@ -103,18 +119,38 @@ export async function checkForUpdates(
|
||||
};
|
||||
}
|
||||
|
||||
// Fetch from GitHub API (backend-specific repo)
|
||||
const latestVersion = await fetchLatestVersion(verbose, backend);
|
||||
const now = Date.now();
|
||||
writeVersionCache(latestVersion, backend);
|
||||
try {
|
||||
// Fetch from GitHub API (backend-specific repo)
|
||||
const latestVersion = await (deps.fetchLatestVersionFn ?? fetchLatestVersion)(verbose, backend);
|
||||
const now = Date.now();
|
||||
writeVersionCache(latestVersion, backend);
|
||||
|
||||
return {
|
||||
hasUpdate: isNewerVersion(latestVersion, currentVersion),
|
||||
currentVersion,
|
||||
latestVersion,
|
||||
fromCache: false,
|
||||
checkedAt: now,
|
||||
};
|
||||
return {
|
||||
hasUpdate: isNewerVersion(latestVersion, currentVersion),
|
||||
currentVersion,
|
||||
latestVersion,
|
||||
fromCache: false,
|
||||
checkedAt: now,
|
||||
};
|
||||
} catch (error) {
|
||||
const staleCache = readStaleVersionCache(backend);
|
||||
if (staleCache) {
|
||||
if (verbose) {
|
||||
console.error(
|
||||
`[cliproxy] GitHub latest release lookup failed, using stale cache: ${(error as Error).message}`
|
||||
);
|
||||
}
|
||||
return {
|
||||
hasUpdate: isNewerVersion(staleCache.latestVersion, currentVersion),
|
||||
currentVersion,
|
||||
latestVersion: staleCache.latestVersion,
|
||||
fromCache: true,
|
||||
checkedAt: staleCache.checkedAt,
|
||||
};
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -125,7 +161,8 @@ export async function checkForUpdates(
|
||||
*/
|
||||
export async function fetchAllVersions(
|
||||
verbose = false,
|
||||
backend: CLIProxyBackend = DEFAULT_BACKEND
|
||||
backend: CLIProxyBackend = DEFAULT_BACKEND,
|
||||
deps: FetchAllVersionsDeps = {}
|
||||
): Promise<VersionListResult> {
|
||||
// Try cache first (backend-specific)
|
||||
const cache = readVersionListCache(backend);
|
||||
@@ -136,31 +173,46 @@ export async function fetchAllVersions(
|
||||
return { ...cache, fromCache: true };
|
||||
}
|
||||
|
||||
// Fetch from GitHub API (backend-specific repo)
|
||||
const urls = getGitHubApiUrls(backend);
|
||||
const response = await fetchJson(urls.allReleases, verbose);
|
||||
try {
|
||||
// Fetch from GitHub API (backend-specific repo)
|
||||
const urls = getGitHubApiUrls(backend);
|
||||
const response = await (deps.fetchJsonFn ?? fetchJson)(urls.allReleases, verbose);
|
||||
|
||||
// Extract and normalize versions
|
||||
const releases = response as unknown as Array<{ tag_name: string }>;
|
||||
const versions = releases
|
||||
.map((r) => r.tag_name.replace(/^v/, ''))
|
||||
.filter((v) => /^\d+\.\d+\.\d+(-\d+)?$/.test(v)); // Valid semver only
|
||||
// Extract and normalize versions
|
||||
const releases = response as unknown as Array<{ tag_name: string }>;
|
||||
const versions = releases
|
||||
.map((r) => r.tag_name.replace(/^v/, ''))
|
||||
.filter((v) => /^\d+\.\d+\.\d+(-\d+)?$/.test(v)); // Valid semver only
|
||||
|
||||
const latest = versions[0] || '';
|
||||
const latest = versions[0] || '';
|
||||
|
||||
// Find latest stable (not newer than max stable AND not in faulty range)
|
||||
const latestStable =
|
||||
versions.find((v) => !isNewerVersion(v, CLIPROXY_MAX_STABLE_VERSION) && !isVersionFaulty(v)) ||
|
||||
CLIPROXY_MAX_STABLE_VERSION;
|
||||
// Find latest stable (not newer than max stable AND not in faulty range)
|
||||
const latestStable =
|
||||
versions.find(
|
||||
(v) => !isNewerVersion(v, CLIPROXY_MAX_STABLE_VERSION) && !isVersionFaulty(v)
|
||||
) || CLIPROXY_MAX_STABLE_VERSION;
|
||||
|
||||
const result: VersionListResult = {
|
||||
versions,
|
||||
latestStable,
|
||||
latest,
|
||||
fromCache: false,
|
||||
checkedAt: Date.now(),
|
||||
};
|
||||
const result: VersionListResult = {
|
||||
versions,
|
||||
latestStable,
|
||||
latest,
|
||||
fromCache: false,
|
||||
checkedAt: Date.now(),
|
||||
};
|
||||
|
||||
writeVersionListCache(result, backend);
|
||||
return result;
|
||||
writeVersionListCache(result, backend);
|
||||
return result;
|
||||
} catch (error) {
|
||||
const staleCache = readStaleVersionListCache(backend);
|
||||
if (staleCache) {
|
||||
if (verbose) {
|
||||
console.error(
|
||||
`[cliproxy] GitHub release list lookup failed, using stale cache: ${(error as Error).message}`
|
||||
);
|
||||
}
|
||||
return { ...staleCache, fromCache: true };
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user