fix(cliproxy): use backend-specific GitHub repos for version fetching

Root cause: fetchLatestVersion and fetchAllVersions were hardcoded to
CLIProxyAPIPlus repo, ignoring backend selection.

Changes:
- Add getGitHubApiUrls(backend) helper to types.ts
- Make fetchLatestVersion and fetchAllVersions backend-aware
- Make version list cache backend-specific (bin/{backend}/.version-list-cache.json)
- Update /api/cliproxy/versions route to use configured backend
- Allow version management even when proxy not running (ProxyStatusWidget)
- Show settings button in all states for easier access
This commit is contained in:
kaitranntt
2026-01-23 14:55:51 -05:00
parent 498175e9fb
commit 0a1cbcc612
5 changed files with 149 additions and 111 deletions
+19 -3
View File
@@ -24,14 +24,30 @@ export const VERSION_CACHE_DURATION_MS = 60 * 60 * 1000;
/** Version pin file name - stores user's explicit version choice */ /** Version pin file name - stores user's explicit version choice */
export const VERSION_PIN_FILE = '.version-pin'; export const VERSION_PIN_FILE = '.version-pin';
/** GitHub API URL for latest release (CLIProxyAPIPlus fork with Kiro + Copilot support) */ /**
* GitHub API URLs - backend-specific
* @deprecated Use getGitHubApiUrls(backend) instead
*/
export const GITHUB_API_LATEST_RELEASE = export const GITHUB_API_LATEST_RELEASE =
'https://api.github.com/repos/router-for-me/CLIProxyAPIPlus/releases/latest'; 'https://api.github.com/repos/router-for-me/CLIProxyAPIPlus/releases/latest';
/** GitHub API URL for all releases */
export const GITHUB_API_ALL_RELEASES = export const GITHUB_API_ALL_RELEASES =
'https://api.github.com/repos/router-for-me/CLIProxyAPIPlus/releases'; 'https://api.github.com/repos/router-for-me/CLIProxyAPIPlus/releases';
/** GitHub repos per backend */
export const GITHUB_REPOS = {
original: 'router-for-me/CLIProxyAPI',
plus: 'router-for-me/CLIProxyAPIPlus',
} as const;
/** Get GitHub API URLs for specific backend */
export function getGitHubApiUrls(backend: 'original' | 'plus') {
const repo = GITHUB_REPOS[backend];
return {
latestRelease: `https://api.github.com/repos/${repo}/releases/latest`,
allReleases: `https://api.github.com/repos/${repo}/releases`,
};
}
/** Version list cache structure */ /** Version list cache structure */
export interface VersionListCache { export interface VersionListCache {
versions: string[]; versions: string[];
+15 -10
View File
@@ -5,7 +5,7 @@
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import { getCliproxyDir, getBinDir } from '../config-generator'; import { getBinDir } from '../config-generator';
import { import {
VersionCache, VersionCache,
VERSION_CACHE_DURATION_MS, VERSION_CACHE_DURATION_MS,
@@ -181,17 +181,19 @@ export function migrateVersionPin(backend: CLIProxyBackend): void {
const VERSION_LIST_CACHE_FILE = '.version-list-cache.json'; const VERSION_LIST_CACHE_FILE = '.version-list-cache.json';
/** /**
* Get path to version list cache file * Get path to version list cache file (backend-specific)
*/ */
export function getVersionListCachePath(): string { export function getVersionListCachePath(backend: CLIProxyBackend = DEFAULT_BACKEND): string {
return path.join(getCliproxyDir(), VERSION_LIST_CACHE_FILE); return path.join(getBinDir(), backend, VERSION_LIST_CACHE_FILE);
} }
/** /**
* Read version list cache if still valid * Read version list cache if still valid (backend-specific)
*/ */
export function readVersionListCache(): VersionListCache | null { export function readVersionListCache(
const cachePath = getVersionListCachePath(); backend: CLIProxyBackend = DEFAULT_BACKEND
): VersionListCache | null {
const cachePath = getVersionListCachePath(backend);
if (!fs.existsSync(cachePath)) { if (!fs.existsSync(cachePath)) {
return null; return null;
} }
@@ -212,10 +214,13 @@ export function readVersionListCache(): VersionListCache | null {
} }
/** /**
* Write version list to cache * Write version list to cache (backend-specific)
*/ */
export function writeVersionListCache(cache: VersionListCache): void { export function writeVersionListCache(
const cachePath = getVersionListCachePath(); cache: VersionListCache,
backend: CLIProxyBackend = DEFAULT_BACKEND
): void {
const cachePath = getVersionListCachePath(backend);
try { try {
fs.mkdirSync(path.dirname(cachePath), { recursive: true }); fs.mkdirSync(path.dirname(cachePath), { recursive: true });
+23 -16
View File
@@ -11,12 +11,7 @@ import {
readVersionListCache, readVersionListCache,
writeVersionListCache, writeVersionListCache,
} from './version-cache'; } from './version-cache';
import { import { UpdateCheckResult, VersionListResult, getGitHubApiUrls } from './types';
UpdateCheckResult,
GITHUB_API_LATEST_RELEASE,
GITHUB_API_ALL_RELEASES,
VersionListResult,
} from './types';
import { import {
CLIPROXY_MAX_STABLE_VERSION, CLIPROXY_MAX_STABLE_VERSION,
CLIPROXY_FAULTY_RANGE, CLIPROXY_FAULTY_RANGE,
@@ -61,9 +56,15 @@ export function isVersionFaulty(version: string): boolean {
/** /**
* Fetch latest version from GitHub API * Fetch latest version from GitHub API
* @param verbose Enable verbose logging
* @param backend Backend to fetch version for (uses correct GitHub repo)
*/ */
export async function fetchLatestVersion(verbose = false): Promise<string> { export async function fetchLatestVersion(
const response = await fetchJson(GITHUB_API_LATEST_RELEASE, verbose); verbose = false,
backend: CLIProxyBackend = DEFAULT_BACKEND
): Promise<string> {
const urls = getGitHubApiUrls(backend);
const response = await fetchJson(urls.latestRelease, verbose);
// Extract version from tag_name (format: "v6.5.27" or "6.5.27") // Extract version from tag_name (format: "v6.5.27" or "6.5.27")
const tagName = response.tag_name as string; const tagName = response.tag_name as string;
@@ -102,8 +103,8 @@ export async function checkForUpdates(
}; };
} }
// Fetch from GitHub API // Fetch from GitHub API (backend-specific repo)
const latestVersion = await fetchLatestVersion(verbose); const latestVersion = await fetchLatestVersion(verbose, backend);
const now = Date.now(); const now = Date.now();
writeVersionCache(latestVersion, backend); writeVersionCache(latestVersion, backend);
@@ -119,10 +120,15 @@ export async function checkForUpdates(
/** /**
* Fetch all available versions from GitHub releases * Fetch all available versions from GitHub releases
* Caches result for 1 hour to avoid rate limiting * Caches result for 1 hour to avoid rate limiting
* @param verbose Enable verbose logging
* @param backend Backend to fetch versions for (uses correct GitHub repo)
*/ */
export async function fetchAllVersions(verbose = false): Promise<VersionListResult> { export async function fetchAllVersions(
// Try cache first verbose = false,
const cache = readVersionListCache(); backend: CLIProxyBackend = DEFAULT_BACKEND
): Promise<VersionListResult> {
// Try cache first (backend-specific)
const cache = readVersionListCache(backend);
if (cache) { if (cache) {
if (verbose) { if (verbose) {
console.error(`[cliproxy] Using cached version list (${cache.versions.length} versions)`); console.error(`[cliproxy] Using cached version list (${cache.versions.length} versions)`);
@@ -130,8 +136,9 @@ export async function fetchAllVersions(verbose = false): Promise<VersionListResu
return { ...cache, fromCache: true }; return { ...cache, fromCache: true };
} }
// Fetch from GitHub API // Fetch from GitHub API (backend-specific repo)
const response = await fetchJson(GITHUB_API_ALL_RELEASES, verbose); const urls = getGitHubApiUrls(backend);
const response = await fetchJson(urls.allReleases, verbose);
// Extract and normalize versions // Extract and normalize versions
const releases = response as unknown as Array<{ tag_name: string }>; const releases = response as unknown as Array<{ tag_name: string }>;
@@ -154,6 +161,6 @@ export async function fetchAllVersions(verbose = false): Promise<VersionListResu
checkedAt: Date.now(), checkedAt: Date.now(),
}; };
writeVersionListCache(result); writeVersionListCache(result, backend);
return result; return result;
} }
+15 -2
View File
@@ -34,10 +34,22 @@ import {
import { import {
CLIPROXY_MAX_STABLE_VERSION, CLIPROXY_MAX_STABLE_VERSION,
CLIPROXY_FAULTY_RANGE, CLIPROXY_FAULTY_RANGE,
DEFAULT_BACKEND,
} from '../../cliproxy/platform-detector'; } from '../../cliproxy/platform-detector';
import { loadOrCreateUnifiedConfig } from '../../config/unified-config-loader';
const router = Router(); const router = Router();
/** Get configured backend from config */
function getConfiguredBackend() {
try {
const config = loadOrCreateUnifiedConfig();
return config.cliproxy?.backend || DEFAULT_BACKEND;
} catch {
return DEFAULT_BACKEND;
}
}
/** /**
* Extract status code and model from error log file (lightweight parsing) * Extract status code and model from error log file (lightweight parsing)
* Reads first 4KB for model, last 2KB for status code * Reads first 4KB for model, last 2KB for status code
@@ -549,8 +561,9 @@ router.get('/quota/:provider/:accountId', async (req: Request, res: Response): P
*/ */
router.get('/versions', async (_req: Request, res: Response): Promise<void> => { router.get('/versions', async (_req: Request, res: Response): Promise<void> => {
try { try {
const result = await fetchAllVersions(); const backend = getConfiguredBackend();
const currentVersion = getInstalledCliproxyVersion(); const result = await fetchAllVersions(false, backend);
const currentVersion = getInstalledCliproxyVersion(backend);
res.json({ res.json({
...result, ...result,
@@ -289,7 +289,7 @@ export function ProxyStatusWidget() {
<span className="text-sm font-medium">{updateCheck?.backendLabel ?? 'CLIProxy'}</span> <span className="text-sm font-medium">{updateCheck?.backendLabel ?? 'CLIProxy'}</span>
</div> </div>
{/* Right side: icon buttons when running */} {/* Right side: icon buttons */}
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
{isLoading ? ( {isLoading ? (
<RefreshCw className="w-3 h-3 animate-spin text-muted-foreground" /> <RefreshCw className="w-3 h-3 animate-spin text-muted-foreground" />
@@ -310,16 +310,15 @@ export function ProxyStatusWidget() {
isPending={stopProxy.isPending} isPending={stopProxy.isPending}
variant="destructive-ghost" variant="destructive-ghost"
/> />
<IconButton
icon={isExpanded ? X : Settings}
tooltip={isExpanded ? 'Close' : 'Version settings'}
onClick={() => setIsExpanded(!isExpanded)}
className={isExpanded ? 'bg-muted' : undefined}
/>
</> </>
) : ( ) : null}
<Power className="w-3 h-3 text-muted-foreground" /> {/* Settings button always visible */}
)} <IconButton
icon={isExpanded ? X : Settings}
tooltip={isExpanded ? 'Close' : 'Version settings'}
onClick={() => setIsExpanded(!isExpanded)}
className={isExpanded ? 'bg-muted' : undefined}
/>
</div> </div>
</div> </div>
@@ -376,82 +375,80 @@ export function ProxyStatusWidget() {
</div> </div>
)} )}
{/* Expanded section: Version Management */} {/* Expanded section: Version Management (available even when not running) */}
{isRunning && ( <Collapsible open={isExpanded} onOpenChange={setIsExpanded}>
<Collapsible open={isExpanded} onOpenChange={setIsExpanded}> <CollapsibleContent className="mt-3 pt-3 border-t border-muted">
<CollapsibleContent className="mt-3 pt-3 border-t border-muted"> {/* Section header */}
{/* Section header */} <h4 className="text-xs font-medium text-muted-foreground mb-3">Version Management</h4>
<h4 className="text-xs font-medium text-muted-foreground mb-3">Version Management</h4>
{/* Version picker row */} {/* Version picker row */}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{/* Dropdown - full width, no truncation */} {/* Dropdown - full width, no truncation */}
<Select <Select
value={selectedVersion} value={selectedVersion}
onValueChange={setSelectedVersion} onValueChange={setSelectedVersion}
disabled={versionsLoading} disabled={versionsLoading}
> >
<SelectTrigger className="h-8 text-xs flex-1"> <SelectTrigger className="h-8 text-xs flex-1">
<SelectValue placeholder="Select version to install..." /> <SelectValue placeholder="Select version to install..." />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{versionsData?.versions.slice(0, 20).map((v) => { {versionsData?.versions.slice(0, 20).map((v) => {
const vIsUnstable = const vIsUnstable =
versionsData?.maxStableVersion && versionsData?.maxStableVersion &&
isNewerVersionClient(v, versionsData.maxStableVersion); isNewerVersionClient(v, versionsData.maxStableVersion);
return ( return (
<SelectItem key={v} value={v} className="text-xs"> <SelectItem key={v} value={v} className="text-xs">
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
v{v} v{v}
{v === versionsData.latestStable && ( {v === versionsData.latestStable && (
<span className="text-green-600 dark:text-green-400">(stable)</span> <span className="text-green-600 dark:text-green-400">(stable)</span>
)} )}
{vIsUnstable && ( {vIsUnstable && (
<span className="text-amber-600 dark:text-amber-400"></span> <span className="text-amber-600 dark:text-amber-400"></span>
)} )}
</span> </span>
</SelectItem> </SelectItem>
); );
})} })}
</SelectContent> </SelectContent>
</Select> </Select>
{/* Install button */} {/* Install button */}
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
className="h-8 text-xs gap-1.5 px-3" className="h-8 text-xs gap-1.5 px-3"
onClick={() => handleInstallVersion(selectedVersion)} onClick={() => handleInstallVersion(selectedVersion)}
disabled={installVersion.isPending || !selectedVersion} disabled={installVersion.isPending || !selectedVersion}
> >
{installVersion.isPending ? ( {installVersion.isPending ? (
<RefreshCw className="w-3.5 h-3.5 animate-spin" /> <RefreshCw className="w-3.5 h-3.5 animate-spin" />
) : ( ) : (
<Download className="w-3.5 h-3.5" /> <Download className="w-3.5 h-3.5" />
)}
Install
</Button>
</div>
{/* Stability warning for selected version */}
{selectedVersion &&
versionsData?.maxStableVersion &&
isNewerVersionClient(selectedVersion, versionsData.maxStableVersion) && (
<div className="mt-2 flex items-center gap-1.5 text-[11px] text-amber-600 dark:text-amber-400">
<AlertTriangle className="w-3.5 h-3.5 flex-shrink-0" />
<span>Versions above {versionsData.maxStableVersion} have known issues</span>
</div>
)} )}
Install
</Button>
</div>
{/* Sync time */} {/* Stability warning for selected version */}
{updateCheck?.checkedAt && ( {selectedVersion &&
<div className="mt-2 text-[10px] text-muted-foreground/60"> versionsData?.maxStableVersion &&
Last checked {formatTimeAgo(updateCheck.checkedAt)} isNewerVersionClient(selectedVersion, versionsData.maxStableVersion) && (
<div className="mt-2 flex items-center gap-1.5 text-[11px] text-amber-600 dark:text-amber-400">
<AlertTriangle className="w-3.5 h-3.5 flex-shrink-0" />
<span>Versions above {versionsData.maxStableVersion} have known issues</span>
</div> </div>
)} )}
</CollapsibleContent>
</Collapsible> {/* Sync time */}
)} {updateCheck?.checkedAt && (
<div className="mt-2 text-[10px] text-muted-foreground/60">
Last checked {formatTimeAgo(updateCheck.checkedAt)}
</div>
)}
</CollapsibleContent>
</Collapsible>
{/* Not running state */} {/* Not running state */}
{!isRunning && ( {!isRunning && (