mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-02 08:19:59 +00:00
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:
@@ -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[];
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
Reference in New Issue
Block a user