mirror of
https://github.com/musistudio/claude-code-router.git
synced 2026-01-30 06:12:06 +00:00
add version manager
This commit is contained in:
190
src/codeManager/downloder.ts
Normal file
190
src/codeManager/downloder.ts
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
import { existsSync, mkdirSync, rmSync } from 'fs';
|
||||||
|
import { join, dirname } from 'path';
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
|
||||||
|
interface DownloadOptions {
|
||||||
|
version?: string;
|
||||||
|
destination?: string;
|
||||||
|
onProgress?: (message: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NpmDownloader {
|
||||||
|
private readonly packageName = '@anthropic-ai/claude-code';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从tarball文件路径提取版本号
|
||||||
|
*/
|
||||||
|
private extractVersionFromTarballPath(tarballPath: string): string {
|
||||||
|
const tarballName = tarballPath.split('/').pop() || '';
|
||||||
|
// tarball文件名格式: anthropic-ai-claude-code-1.0.0.tgz
|
||||||
|
const match = tarballName.match(/anthropic-ai-claude-code-(.+)\.tgz$/);
|
||||||
|
if (match && match[1]) {
|
||||||
|
return match[1];
|
||||||
|
}
|
||||||
|
throw new Error(`Could not extract version from tarball path: ${tarballPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取可用版本列表
|
||||||
|
*/
|
||||||
|
async getAvailableVersions(): Promise<string[]> {
|
||||||
|
try {
|
||||||
|
const command = `npm view ${this.packageName} versions --json`;
|
||||||
|
const result = execSync(command, { encoding: 'utf8', stdio: 'pipe' });
|
||||||
|
const versions = JSON.parse(result);
|
||||||
|
return Array.isArray(versions) ? versions : [];
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(`Failed to get available versions: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取最新版本
|
||||||
|
*/
|
||||||
|
async getLatestVersion(): Promise<string> {
|
||||||
|
try {
|
||||||
|
const command = `npm view ${this.packageName} version`;
|
||||||
|
const result = execSync(command, { encoding: 'utf8', stdio: 'pipe' });
|
||||||
|
return result.trim();
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(`Failed to get latest version: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载指定版本的包
|
||||||
|
*/
|
||||||
|
async downloadVersion(options: DownloadOptions = {}): Promise<string> {
|
||||||
|
const { version: requestedVersion, destination, onProgress } = options;
|
||||||
|
|
||||||
|
// 设置下载目录
|
||||||
|
const packageNameSafe = this.packageName.replace('/', '-');
|
||||||
|
const defaultDestination = join(process.cwd(), 'downloads', packageNameSafe);
|
||||||
|
const downloadDir = destination || defaultDestination;
|
||||||
|
|
||||||
|
// 确保目录存在
|
||||||
|
if (!existsSync(downloadDir)) {
|
||||||
|
mkdirSync(downloadDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确定版本
|
||||||
|
let version = requestedVersion;
|
||||||
|
if (!version) {
|
||||||
|
version = await this.getLatestVersion();
|
||||||
|
onProgress?.(`No version specified, using latest version: ${version}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
onProgress?.(`Downloading ${this.packageName}@${version}...`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 使用 npm pack 下载包
|
||||||
|
const packCommand = `npm pack ${this.packageName}@${version}`;
|
||||||
|
onProgress?.(`Running: ${packCommand}`);
|
||||||
|
|
||||||
|
// 在下载目录中执行 npm pack
|
||||||
|
const { spawn } = require('child_process');
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const process = spawn(packCommand, [], {
|
||||||
|
shell: true,
|
||||||
|
cwd: downloadDir,
|
||||||
|
stdio: 'pipe'
|
||||||
|
});
|
||||||
|
|
||||||
|
let output = '';
|
||||||
|
let errorOutput = '';
|
||||||
|
|
||||||
|
process.stdout?.on('data', (data: Buffer) => {
|
||||||
|
output += data.toString();
|
||||||
|
onProgress?.(data.toString().trim());
|
||||||
|
});
|
||||||
|
|
||||||
|
process.stderr?.on('data', (data: Buffer) => {
|
||||||
|
errorOutput += data.toString();
|
||||||
|
onProgress?.(`Error: ${data.toString().trim()}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('close', (code: number) => {
|
||||||
|
if (code !== 0) {
|
||||||
|
reject(new Error(`npm pack failed with code ${code}: ${errorOutput}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// npm pack 会输出文件名,提取文件路径
|
||||||
|
const tarballName = output.trim().split('\n').pop();
|
||||||
|
if (!tarballName) {
|
||||||
|
reject(new Error('Could not determine downloaded file name'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tarballPath = join(downloadDir, tarballName);
|
||||||
|
|
||||||
|
onProgress?.(`Successfully downloaded to: ${tarballPath}`);
|
||||||
|
resolve(tarballPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('error', (error: Error) => {
|
||||||
|
reject(new Error(`Failed to execute npm pack: ${error.message}`));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(`Failed to download package: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解压下载的 tarball 文件到版本号目录
|
||||||
|
*/
|
||||||
|
async extractPackage(tarballPath: string, extractTo?: string): Promise<string> {
|
||||||
|
// 如果没有指定解压目录,则使用版本号作为目录名
|
||||||
|
const extractDir = extractTo || join(dirname(tarballPath), this.extractVersionFromTarballPath(tarballPath));
|
||||||
|
|
||||||
|
// 确保解压目录存在
|
||||||
|
if (!existsSync(extractDir)) {
|
||||||
|
mkdirSync(extractDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 使用 tar 命令解压到版本号目录
|
||||||
|
const extractCommand = `tar -xzf "${tarballPath}" -C "${extractDir}" --strip-components=1`;
|
||||||
|
execSync(extractCommand, { stdio: 'inherit' });
|
||||||
|
|
||||||
|
// 删除 tarball 文件
|
||||||
|
rmSync(tarballPath);
|
||||||
|
|
||||||
|
return extractDir;
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(`Failed to extract package: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载并解压包
|
||||||
|
*/
|
||||||
|
async downloadAndExtract(options: DownloadOptions = {}): Promise<string> {
|
||||||
|
const tarballPath = await this.downloadVersion(options);
|
||||||
|
return this.extractPackage(tarballPath, options.destination);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出便捷函数
|
||||||
|
export async function downloadClaudeCode(version?: string, options?: Omit<DownloadOptions, 'version'>): Promise<string> {
|
||||||
|
const downloader = new NpmDownloader();
|
||||||
|
return downloader.downloadVersion({ ...options, version });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function downloadAndExtractClaudeCode(version?: string, options?: Omit<DownloadOptions, 'version'>): Promise<string> {
|
||||||
|
const downloader = new NpmDownloader();
|
||||||
|
return downloader.downloadAndExtract({ ...options, version });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getLatestClaudeCodeVersion(): Promise<string> {
|
||||||
|
const downloader = new NpmDownloader();
|
||||||
|
return downloader.getLatestVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAvailableClaudeCodeVersions(): Promise<string[]> {
|
||||||
|
const downloader = new NpmDownloader();
|
||||||
|
return downloader.getAvailableVersions();
|
||||||
|
}
|
||||||
0
src/codeManager/wrapper.ts
Normal file
0
src/codeManager/wrapper.ts
Normal file
194
src/server.ts
194
src/server.ts
@@ -3,9 +3,11 @@ import { readConfigFile, writeConfigFile, backupConfigFile } from "./utils";
|
|||||||
import { checkForUpdates, performUpdate } from "./utils";
|
import { checkForUpdates, performUpdate } from "./utils";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
import fastifyStatic from "@fastify/static";
|
import fastifyStatic from "@fastify/static";
|
||||||
import { readdirSync, statSync, readFileSync, writeFileSync, existsSync } from "fs";
|
import { readdirSync, statSync, readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
||||||
import { homedir } from "os";
|
import { homedir } from "os";
|
||||||
import {calculateTokenCount} from "./utils/router";
|
import {calculateTokenCount} from "./utils/router";
|
||||||
|
import { NpmDownloader } from "./codeManager/downloder";
|
||||||
|
import { HOME_DIR } from "./constants";
|
||||||
|
|
||||||
export const createServer = (config: any): Server => {
|
export const createServer = (config: any): Server => {
|
||||||
const server = new Server(config);
|
const server = new Server(config);
|
||||||
@@ -198,5 +200,195 @@ export const createServer = (config: any): Server => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Claude Code 版本管理相关 API
|
||||||
|
const versionsDir = join(HOME_DIR, 'versions');
|
||||||
|
const currentVersionPath = join(HOME_DIR, 'current-version.json');
|
||||||
|
|
||||||
|
// 确保版本目录存在
|
||||||
|
if (!existsSync(versionsDir)) {
|
||||||
|
mkdirSync(versionsDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化下载器
|
||||||
|
const downloader = new NpmDownloader();
|
||||||
|
|
||||||
|
// 辅助函数:读取当前版本
|
||||||
|
const readCurrentVersion = () => {
|
||||||
|
try {
|
||||||
|
if (existsSync(currentVersionPath)) {
|
||||||
|
const content = readFileSync(currentVersionPath, 'utf8');
|
||||||
|
const data = JSON.parse(content);
|
||||||
|
return data.currentVersion || '';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to read current version:', error);
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 辅助函数:保存当前版本
|
||||||
|
const saveCurrentVersion = (version: string) => {
|
||||||
|
try {
|
||||||
|
writeFileSync(currentVersionPath, JSON.stringify({ currentVersion: version }, null, 2));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save current version:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 辅助函数:获取已下载的版本列表(扫描 versions 文件夹)
|
||||||
|
const getDownloadedVersions = () => {
|
||||||
|
try {
|
||||||
|
const versions = [];
|
||||||
|
if (existsSync(versionsDir)) {
|
||||||
|
const folders = readdirSync(versionsDir);
|
||||||
|
for (const folder of folders) {
|
||||||
|
const folderPath = join(versionsDir, folder);
|
||||||
|
const stats = statSync(folderPath);
|
||||||
|
if (stats.isDirectory()) {
|
||||||
|
versions.push({
|
||||||
|
version: folder,
|
||||||
|
downloadPath: folderPath,
|
||||||
|
downloadedAt: stats.birthtime.toISOString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 按下载时间倒序排列
|
||||||
|
versions.sort((a, b) => new Date(b.downloadedAt).getTime() - new Date(a.downloadedAt).getTime());
|
||||||
|
}
|
||||||
|
return versions;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get downloaded versions:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取可用版本列表
|
||||||
|
server.app.get("/api/claude-code/versions", async (req, reply) => {
|
||||||
|
try {
|
||||||
|
const versions = await downloader.getAvailableVersions();
|
||||||
|
|
||||||
|
// 按版本号倒序排列(最新在前)
|
||||||
|
const sortedVersions = versions.sort((a, b) => b.localeCompare(a));
|
||||||
|
|
||||||
|
return { versions: sortedVersions };
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to get available versions:", error);
|
||||||
|
reply.status(500).send({ error: "Failed to get available versions" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取已下载的版本列表
|
||||||
|
server.app.get("/api/claude-code/downloaded", async (req, reply) => {
|
||||||
|
try {
|
||||||
|
const currentVersion = readCurrentVersion();
|
||||||
|
const downloadedVersions = getDownloadedVersions();
|
||||||
|
const versionsWithStatus = downloadedVersions.map(v => ({
|
||||||
|
...v,
|
||||||
|
isCurrent: v.version === currentVersion
|
||||||
|
}));
|
||||||
|
return { versions: versionsWithStatus, currentVersion };
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to get downloaded versions:", error);
|
||||||
|
reply.status(500).send({ error: "Failed to get downloaded versions" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 下载指定版本
|
||||||
|
server.app.post("/api/claude-code/download/:version", async (req, reply) => {
|
||||||
|
try {
|
||||||
|
const { version } = req.params as any;
|
||||||
|
const currentVersion = readCurrentVersion();
|
||||||
|
const downloadedVersions = getDownloadedVersions();
|
||||||
|
|
||||||
|
// 检查是否已下载
|
||||||
|
if (downloadedVersions.some((v: any) => v.version === version)) {
|
||||||
|
reply.status(400).send({ error: "Version already downloaded" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建版本号目录
|
||||||
|
const versionDir = join(versionsDir, version);
|
||||||
|
if (!existsSync(versionDir)) {
|
||||||
|
mkdirSync(versionDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始下载到版本号目录
|
||||||
|
const extractedPath = await downloader.downloadAndExtract({
|
||||||
|
version,
|
||||||
|
destination: versionDir,
|
||||||
|
onProgress: (message) => console.log(`Download progress for ${version}:`, message)
|
||||||
|
});
|
||||||
|
|
||||||
|
// 如果没有当前版本,设置当前版本
|
||||||
|
if (!currentVersion) {
|
||||||
|
saveCurrentVersion(version);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, version, path: extractedPath };
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to download version:", error);
|
||||||
|
reply.status(500).send({ error: `Failed to download version: ${(error as Error).message}` });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 删除指定版本
|
||||||
|
server.app.post("/api/claude-code/version/delete", async (req, reply) => {
|
||||||
|
try {
|
||||||
|
const { version } = req.body as any;
|
||||||
|
const currentVersion = readCurrentVersion();
|
||||||
|
const downloadedVersions = getDownloadedVersions();
|
||||||
|
|
||||||
|
// 不能删除当前版本
|
||||||
|
if (version === currentVersion) {
|
||||||
|
reply.status(400).send({ error: "Cannot delete current version" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const versionDir = join(versionsDir, version);
|
||||||
|
|
||||||
|
// 检查版本目录是否存在
|
||||||
|
if (!existsSync(versionDir)) {
|
||||||
|
reply.status(404).send({ error: "Version not found" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除版本目录
|
||||||
|
const { execSync } = require('child_process');
|
||||||
|
try {
|
||||||
|
execSync(`rm -rf "${versionDir}"`, { stdio: 'pipe' });
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to delete directory for version', version, ':', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, version };
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to delete version:", error);
|
||||||
|
reply.status(500).send({ error: "Failed to delete version" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 切换到指定版本
|
||||||
|
server.app.post("/api/claude-code/switch/:version", async (req, reply) => {
|
||||||
|
try {
|
||||||
|
const { version } = req.params as any;
|
||||||
|
const downloadedVersions = getDownloadedVersions();
|
||||||
|
|
||||||
|
// 检查版本是否已下载
|
||||||
|
const versionExists = downloadedVersions.some((v: any) => v.version === version);
|
||||||
|
if (!versionExists) {
|
||||||
|
reply.status(404).send({ error: "Version not downloaded" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新当前版本
|
||||||
|
saveCurrentVersion(version);
|
||||||
|
|
||||||
|
return { success: true, currentVersion: version };
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to switch version:", error);
|
||||||
|
reply.status(500).send({ error: "Failed to switch version" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return server;
|
return server;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,10 +7,11 @@ import { Providers } from "@/components/Providers";
|
|||||||
import { Router } from "@/components/Router";
|
import { Router } from "@/components/Router";
|
||||||
import { JsonEditor } from "@/components/JsonEditor";
|
import { JsonEditor } from "@/components/JsonEditor";
|
||||||
import { LogViewer } from "@/components/LogViewer";
|
import { LogViewer } from "@/components/LogViewer";
|
||||||
|
import { VersionManagerDialog } from "@/components/VersionManagerDialog";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useConfig } from "@/components/ConfigProvider";
|
import { useConfig } from "@/components/ConfigProvider";
|
||||||
import { api } from "@/lib/api";
|
import { api } from "@/lib/api";
|
||||||
import { Settings, Languages, Save, RefreshCw, FileJson, CircleArrowUp, FileText } from "lucide-react";
|
import { Settings, Languages, Save, RefreshCw, FileJson, CircleArrowUp, FileText, Package } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
@@ -34,6 +35,7 @@ function App() {
|
|||||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||||||
const [isJsonEditorOpen, setIsJsonEditorOpen] = useState(false);
|
const [isJsonEditorOpen, setIsJsonEditorOpen] = useState(false);
|
||||||
const [isLogViewerOpen, setIsLogViewerOpen] = useState(false);
|
const [isLogViewerOpen, setIsLogViewerOpen] = useState(false);
|
||||||
|
const [isVersionManagerOpen, setIsVersionManagerOpen] = useState(false);
|
||||||
const [isCheckingAuth, setIsCheckingAuth] = useState(true);
|
const [isCheckingAuth, setIsCheckingAuth] = useState(true);
|
||||||
const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' | 'warning' } | null>(null);
|
const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' | 'warning' } | null>(null);
|
||||||
// 版本检查状态
|
// 版本检查状态
|
||||||
@@ -281,6 +283,9 @@ function App() {
|
|||||||
<Button variant="ghost" size="icon" onClick={() => setIsLogViewerOpen(true)} className="transition-all-ease hover:scale-110">
|
<Button variant="ghost" size="icon" onClick={() => setIsLogViewerOpen(true)} className="transition-all-ease hover:scale-110">
|
||||||
<FileText className="h-5 w-5" />
|
<FileText className="h-5 w-5" />
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button variant="ghost" size="icon" onClick={() => setIsVersionManagerOpen(true)} className="transition-all-ease hover:scale-110">
|
||||||
|
<Package className="h-5 w-5" />
|
||||||
|
</Button>
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button variant="ghost" size="icon" className="transition-all-ease hover:scale-110">
|
<Button variant="ghost" size="icon" className="transition-all-ease hover:scale-110">
|
||||||
@@ -360,6 +365,11 @@ function App() {
|
|||||||
onOpenChange={setIsLogViewerOpen}
|
onOpenChange={setIsLogViewerOpen}
|
||||||
showToast={(message, type) => setToast({ message, type })}
|
showToast={(message, type) => setToast({ message, type })}
|
||||||
/>
|
/>
|
||||||
|
<VersionManagerDialog
|
||||||
|
open={isVersionManagerOpen}
|
||||||
|
onOpenChange={setIsVersionManagerOpen}
|
||||||
|
showToast={(message, type) => setToast({ message, type })}
|
||||||
|
/>
|
||||||
{/* 版本更新对话框 */}
|
{/* 版本更新对话框 */}
|
||||||
<Dialog open={isUpdateDialogOpen} onOpenChange={setIsUpdateDialogOpen}>
|
<Dialog open={isUpdateDialogOpen} onOpenChange={setIsUpdateDialogOpen}>
|
||||||
<DialogContent className="max-w-2xl">
|
<DialogContent className="max-w-2xl">
|
||||||
|
|||||||
407
ui/src/components/VersionManagerDialog.tsx
Normal file
407
ui/src/components/VersionManagerDialog.tsx
Normal file
@@ -0,0 +1,407 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
|
import {
|
||||||
|
Package,
|
||||||
|
Download,
|
||||||
|
CheckCircle,
|
||||||
|
Clock,
|
||||||
|
AlertCircle,
|
||||||
|
RefreshCw,
|
||||||
|
Trash2,
|
||||||
|
AlertTriangle
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { api } from '@/lib/api';
|
||||||
|
|
||||||
|
interface Version {
|
||||||
|
version: string;
|
||||||
|
isCurrent?: boolean;
|
||||||
|
isDownloaded?: boolean;
|
||||||
|
downloadPath?: string;
|
||||||
|
status?: 'available' | 'downloading' | 'downloaded' | 'error';
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VersionManagerDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
showToast?: (message: string, type: 'success' | 'error' | 'warning') => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const VersionManagerDialog: React.FC<VersionManagerDialogProps> = ({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
showToast
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [downloadedVersions, setDownloadedVersions] = useState<Version[]>([]);
|
||||||
|
const [availableVersions, setAvailableVersions] = useState<string[]>([]);
|
||||||
|
const [currentVersion, setCurrentVersion] = useState<string>('');
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [downloadingVersion, setDownloadingVersion] = useState<string | null>(null);
|
||||||
|
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
|
||||||
|
const [versionToDelete, setVersionToDelete] = useState<string>('');
|
||||||
|
const [deleteError, setDeleteError] = useState<string>('');
|
||||||
|
|
||||||
|
// 加载已下载的版本数据
|
||||||
|
const loadDownloadedVersions = async () => {
|
||||||
|
try {
|
||||||
|
const response = await api.getDownloadedClaudeCodeVersions();
|
||||||
|
setDownloadedVersions(response.versions.map(v => ({
|
||||||
|
...v,
|
||||||
|
isDownloaded: true,
|
||||||
|
status: 'downloaded' as const
|
||||||
|
})));
|
||||||
|
setCurrentVersion(response.currentVersion);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load downloaded versions:', error);
|
||||||
|
showToast?.('获取已下载版本失败', 'error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// 获取可用的版本列表
|
||||||
|
const fetchAvailableVersions = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await api.getClaudeCodeVersions();
|
||||||
|
setAvailableVersions(response.versions);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch available versions:', error);
|
||||||
|
showToast?.(t('version_manager.get_versions_failed'), 'error');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 下载指定版本
|
||||||
|
const downloadVersion = async (version: string) => {
|
||||||
|
setDownloadingVersion(version);
|
||||||
|
try {
|
||||||
|
const response = await api.downloadClaudeCodeVersion(version);
|
||||||
|
if (response.success) {
|
||||||
|
// 重新加载已下载版本列表
|
||||||
|
await loadDownloadedVersions();
|
||||||
|
showToast?.(`版本 ${version} 下载成功`, 'success');
|
||||||
|
} else {
|
||||||
|
showToast?.(`下载版本 ${version} 失败`, 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to download version:', error);
|
||||||
|
showToast?.(`下载版本 ${version} 失败`, 'error');
|
||||||
|
} finally {
|
||||||
|
setDownloadingVersion(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除下载的版本
|
||||||
|
const handleDeleteVersion = async (version: string) => {
|
||||||
|
try {
|
||||||
|
const response = await api.deleteClaudeCodeVersion(version);
|
||||||
|
if (response.success) {
|
||||||
|
// 重新加载已下载版本列表
|
||||||
|
await loadDownloadedVersions();
|
||||||
|
showToast?.(`版本 ${version} 已删除`, 'success');
|
||||||
|
setDeleteConfirmOpen(false);
|
||||||
|
setVersionToDelete('');
|
||||||
|
setDeleteError('');
|
||||||
|
} else {
|
||||||
|
setDeleteError(t('version_manager.delete_server_error'));
|
||||||
|
showToast?.(t('version_manager.delete_failed', { version }), 'error');
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Failed to remove version:', error);
|
||||||
|
// 只使用接口返回的 error 字段内容,不显示 HTTP 层面的错误信息
|
||||||
|
const errorMessage = error?.response?.data?.error || t('version_manager.delete_unknown_error');
|
||||||
|
setDeleteError(errorMessage);
|
||||||
|
showToast?.(t('version_manager.delete_failed', { version }), 'error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 触发删除确认对话框
|
||||||
|
const removeVersion = (version: string) => {
|
||||||
|
setVersionToDelete(version);
|
||||||
|
setDeleteError('');
|
||||||
|
// 使用 setTimeout 确保 state 更新完成后再打开对话框
|
||||||
|
setTimeout(() => {
|
||||||
|
setDeleteConfirmOpen(true);
|
||||||
|
}, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 确认删除
|
||||||
|
const confirmDelete = () => {
|
||||||
|
if (versionToDelete) {
|
||||||
|
handleDeleteVersion(versionToDelete);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 取消删除
|
||||||
|
const cancelDelete = () => {
|
||||||
|
setDeleteConfirmOpen(false);
|
||||||
|
setVersionToDelete('');
|
||||||
|
setDeleteError('');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 切换当前使用的版本
|
||||||
|
const switchToVersion = async (version: string) => {
|
||||||
|
try {
|
||||||
|
const response = await api.switchClaudeCodeVersion(version);
|
||||||
|
if (response.success) {
|
||||||
|
// 重新加载已下载版本列表
|
||||||
|
await loadDownloadedVersions();
|
||||||
|
showToast?.(`已切换到版本 ${version}`, 'success');
|
||||||
|
} else {
|
||||||
|
showToast?.(`切换到版本 ${version} 失败`, 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to switch version:', error);
|
||||||
|
showToast?.(`切换到版本 ${version} 失败`, 'error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
loadDownloadedVersions();
|
||||||
|
fetchAvailableVersions();
|
||||||
|
}
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
const getVersionIcon = (version: Version) => {
|
||||||
|
if (version.isCurrent) {
|
||||||
|
return <CheckCircle className="h-4 w-4 text-green-500" />;
|
||||||
|
}
|
||||||
|
if (version.status === 'downloading') {
|
||||||
|
return <Clock className="h-4 w-4 text-yellow-500 animate-pulse" />;
|
||||||
|
}
|
||||||
|
if (version.status === 'error') {
|
||||||
|
return <AlertCircle className="h-4 w-4 text-red-500" />;
|
||||||
|
}
|
||||||
|
return <Package className="h-4 w-4 text-blue-500" />;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="max-w-4xl max-h-[80vh]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<Package className="h-5 w-5" />
|
||||||
|
{t('version_manager.title')}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{t('version_manager.description')}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<Tabs defaultValue="installed" className="flex-1">
|
||||||
|
<TabsList className="grid w-full grid-cols-2">
|
||||||
|
<TabsTrigger value="installed">{t('version_manager.installed_versions')}</TabsTrigger>
|
||||||
|
<TabsTrigger value="available">{t('version_manager.available_versions')}</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value="installed" className="space-y-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-lg">{t('version_manager.installed_versions')}</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
{t('version_manager.current_version')}: <Badge variant="outline">{currentVersion}</Badge>
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<ScrollArea className="h-64">
|
||||||
|
<div className="space-y-2">
|
||||||
|
{downloadedVersions.map((version) => (
|
||||||
|
<div
|
||||||
|
key={version.version}
|
||||||
|
className="flex items-center justify-between p-3 border rounded-lg hover:bg-gray-50"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{getVersionIcon(version)}
|
||||||
|
<div>
|
||||||
|
<span className="font-medium">{version.version}</span>
|
||||||
|
{version.isCurrent && (
|
||||||
|
<Badge className="ml-2" variant="default">
|
||||||
|
{t('version_manager.current_version')}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{!version.isCurrent && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => switchToVersion(version.version)}
|
||||||
|
>
|
||||||
|
{t('version_manager.switch_to_version')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => removeVersion(version.version)}
|
||||||
|
className="text-red-600 hover:text-red-700"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{downloadedVersions.length === 0 && (
|
||||||
|
<div className="text-center py-8 text-gray-500">
|
||||||
|
<Package className="h-12 w-12 mx-auto mb-2 opacity-50" />
|
||||||
|
<p>{t('version_manager.no_downloaded_versions')}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="available" className="space-y-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<CardTitle className="text-lg">{t('version_manager.available_versions')}</CardTitle>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={fetchAvailableVersions}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
<RefreshCw className={`h-4 w-4 mr-1 ${isLoading ? 'animate-spin' : ''}`} />
|
||||||
|
{t('version_manager.refresh')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<CardDescription>
|
||||||
|
{t('version_manager.download_description')}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<ScrollArea className="h-64">
|
||||||
|
<div className="grid gap-2">
|
||||||
|
{availableVersions.map((version) => {
|
||||||
|
const isDownloaded = downloadedVersions.some(v => v.version === version);
|
||||||
|
const isDownloading = downloadingVersion === version;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={version}
|
||||||
|
className="flex items-center justify-between p-3 border rounded-lg hover:bg-gray-50"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Package className="h-4 w-4 text-blue-500" />
|
||||||
|
<span className="font-medium">{version}</span>
|
||||||
|
{isDownloaded && (
|
||||||
|
<Badge variant="secondary">{t('version_manager.downloaded')}</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={() => downloadVersion(version)}
|
||||||
|
disabled={isDownloaded || isDownloading}
|
||||||
|
>
|
||||||
|
{isDownloading ? (
|
||||||
|
<>
|
||||||
|
<Clock className="h-4 w-4 mr-1 animate-pulse" />
|
||||||
|
{t('version_manager.downloading')}
|
||||||
|
</>
|
||||||
|
) : isDownloaded ? (
|
||||||
|
<>
|
||||||
|
<CheckCircle className="h-4 w-4 mr-1" />
|
||||||
|
{t('version_manager.downloaded')}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Download className="h-4 w-4 mr-1" />
|
||||||
|
{t('version_manager.download')}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{availableVersions.length === 0 && !isLoading && (
|
||||||
|
<div className="text-center py-8 text-gray-500">
|
||||||
|
<Package className="h-12 w-12 mx-auto mb-2 opacity-50" />
|
||||||
|
<p>{t('version_manager.no_available_versions')}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isLoading && (
|
||||||
|
<div className="text-center py-8 text-gray-500">
|
||||||
|
<RefreshCw className="h-12 w-12 mx-auto mb-2 animate-spin" />
|
||||||
|
<p>{t('version_manager.loading_versions')}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||||
|
{t('version_manager.close')}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* 删除确认对话框 */}
|
||||||
|
<Dialog open={deleteConfirmOpen} onOpenChange={setDeleteConfirmOpen}>
|
||||||
|
<DialogContent className="max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<AlertTriangle className="h-5 w-5 text-red-500" />
|
||||||
|
{t('version_manager.confirm_delete_title')}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{(() => {
|
||||||
|
if (!versionToDelete) {
|
||||||
|
return t('version_manager.confirm_delete_message', { version: '未知版本' });
|
||||||
|
}
|
||||||
|
// 直接使用字符串插值作为后备方案,以防i18next插值失败
|
||||||
|
const message = t('version_manager.confirm_delete_message', { version: versionToDelete });
|
||||||
|
if (message.includes('{version}')) {
|
||||||
|
// i18next插值失败,手动替换
|
||||||
|
return message.replace('{version}', versionToDelete);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
})()}
|
||||||
|
{deleteError && (
|
||||||
|
<div className="mt-3 p-3 bg-red-50 border border-red-200 rounded-md">
|
||||||
|
<p className="text-sm text-red-600">{deleteError}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={cancelDelete}>
|
||||||
|
{t('version_manager.cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
onClick={confirmDelete}
|
||||||
|
className="text-white bg-red-600 hover:bg-red-700"
|
||||||
|
>
|
||||||
|
{t('version_manager.confirm_delete_button')}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
19
ui/src/components/ui/scroll-area.tsx
Normal file
19
ui/src/components/ui/scroll-area.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const ScrollArea = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn("relative overflow-auto", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
ScrollArea.displayName = "ScrollArea"
|
||||||
|
|
||||||
|
export { ScrollArea }
|
||||||
@@ -99,7 +99,8 @@ class ApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`API request failed: ${response.status} ${response.statusText}`);
|
const data = await response.json();
|
||||||
|
throw new Error(`API request failed: ${response.status} ${response.statusText} ${data?.error}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.status === 204) {
|
if (response.status === 204) {
|
||||||
@@ -142,6 +143,7 @@ class ApiClient {
|
|||||||
async delete<T>(endpoint: string): Promise<T> {
|
async delete<T>(endpoint: string): Promise<T> {
|
||||||
return this.apiFetch<T>(endpoint, {
|
return this.apiFetch<T>(endpoint, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
|
headers: this.createHeaders(''), // 不设置Content-Type,因为没有请求体
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,6 +237,33 @@ class ApiClient {
|
|||||||
async clearLogs(filePath: string): Promise<void> {
|
async clearLogs(filePath: string): Promise<void> {
|
||||||
return this.delete<void>(`/logs?file=${encodeURIComponent(filePath)}`);
|
return this.delete<void>(`/logs?file=${encodeURIComponent(filePath)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Claude Code 版本管理 API
|
||||||
|
|
||||||
|
// 获取所有可用的 Claude Code 版本
|
||||||
|
async getClaudeCodeVersions(): Promise<{ versions: string[] }> {
|
||||||
|
return this.get<{ versions: string[] }>('/claude-code/versions');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取已下载的 Claude Code 版本
|
||||||
|
async getDownloadedClaudeCodeVersions(): Promise<{ versions: Array<{ version: string; isCurrent: boolean; downloadPath: string; downloadedAt: string }>; currentVersion: string }> {
|
||||||
|
return this.get<{ versions: Array<{ version: string; isCurrent: boolean; downloadPath: string; downloadedAt: string }>; currentVersion: string }>('/claude-code/downloaded');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下载指定版本的 Claude Code
|
||||||
|
async downloadClaudeCodeVersion(version: string): Promise<{ success: boolean; version: string; path: string }> {
|
||||||
|
return this.post<{ success: boolean; version: string; path: string }>(`/claude-code/download/${version}`, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除指定版本的 Claude Code
|
||||||
|
async deleteClaudeCodeVersion(version: string): Promise<{ success: boolean; version: string }> {
|
||||||
|
return this.post<{ success: boolean; version: string }>(`/claude-code/version/delete`, { version });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换到指定版本的 Claude Code
|
||||||
|
async switchClaudeCodeVersion(version: string): Promise<{ success: boolean; currentVersion: string }> {
|
||||||
|
return this.post<{ success: boolean; currentVersion: string }>(`/claude-code/switch/${version}`, {});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a default instance of the API client
|
// Create a default instance of the API client
|
||||||
|
|||||||
@@ -224,5 +224,37 @@
|
|||||||
"worker_init_failed": "Failed to initialize worker",
|
"worker_init_failed": "Failed to initialize worker",
|
||||||
"grouping_not_supported": "Log grouping not supported by server",
|
"grouping_not_supported": "Log grouping not supported by server",
|
||||||
"back": "Back"
|
"back": "Back"
|
||||||
|
},
|
||||||
|
"version_manager": {
|
||||||
|
"title": "Claude Code Version Manager",
|
||||||
|
"description": "Manage Claude Code versions, download new versions or switch between installed versions",
|
||||||
|
"installed_versions": "Installed Versions",
|
||||||
|
"available_versions": "Available Versions",
|
||||||
|
"current_version": "Current Version",
|
||||||
|
"no_available_versions": "No available versions",
|
||||||
|
"downloaded": "Downloaded",
|
||||||
|
"downloading": "Downloading",
|
||||||
|
"download": "Download",
|
||||||
|
"switch_to_version": "Switch to this version",
|
||||||
|
"delete_version": "Delete version",
|
||||||
|
"cannot_delete_current": "Cannot delete version {version}",
|
||||||
|
"download_success": "Version {version} downloaded successfully",
|
||||||
|
"download_failed": "Failed to download version {version}",
|
||||||
|
"switch_success": "Switched to version {version} successfully",
|
||||||
|
"switch_failed": "Failed to switch to version {version}",
|
||||||
|
"get_versions_failed": "Failed to get version list",
|
||||||
|
"version_deleted": "Version {version} deleted",
|
||||||
|
"delete_failed": "Failed to delete version {version}",
|
||||||
|
"confirm_delete_title": "Confirm Delete Version",
|
||||||
|
"confirm_delete_message": "Are you sure you want to delete version {version}?",
|
||||||
|
"confirm_delete_button": "Confirm Delete",
|
||||||
|
"delete_server_error": "Failed to delete version: Server returned error",
|
||||||
|
"delete_unknown_error": "Failed to delete version: Unknown error occurred",
|
||||||
|
"no_downloaded_versions": "No downloaded versions available",
|
||||||
|
"refresh": "Refresh",
|
||||||
|
"download_description": "Download available versions of @anthropic-ai/claude-code from NPM",
|
||||||
|
"loading_versions": "Loading version list...",
|
||||||
|
"close": "Close",
|
||||||
|
"cancel": "Cancel"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -224,5 +224,37 @@
|
|||||||
"worker_init_failed": "Worker初始化失败",
|
"worker_init_failed": "Worker初始化失败",
|
||||||
"grouping_not_supported": "服务器不支持日志分组",
|
"grouping_not_supported": "服务器不支持日志分组",
|
||||||
"back": "返回"
|
"back": "返回"
|
||||||
|
},
|
||||||
|
"version_manager": {
|
||||||
|
"title": "claude code版本管理",
|
||||||
|
"description": "管理 Claude Code 版本,下载新版本或切换已安装的版本",
|
||||||
|
"installed_versions": "已下载版本",
|
||||||
|
"available_versions": "可下载版本",
|
||||||
|
"current_version": "当前版本",
|
||||||
|
"no_available_versions": "暂无可用的版本",
|
||||||
|
"downloaded": "已下载",
|
||||||
|
"downloading": "下载中",
|
||||||
|
"download": "下载",
|
||||||
|
"switch_to_version": "切换到此版本",
|
||||||
|
"delete_version": "删除版本",
|
||||||
|
"cannot_delete_current": "版本 {version} 不能删除",
|
||||||
|
"download_success": "版本 {version} 下载成功",
|
||||||
|
"download_failed": "下载版本 {version} 失败",
|
||||||
|
"switch_success": "切换到版本 {version} 成功",
|
||||||
|
"switch_failed": "切换到版本 {version} 失败",
|
||||||
|
"get_versions_failed": "获取版本列表失败",
|
||||||
|
"version_deleted": "版本 {version} 已删除",
|
||||||
|
"delete_failed": "删除版本 {version} 失败",
|
||||||
|
"confirm_delete_title": "确认删除版本",
|
||||||
|
"confirm_delete_message": "您确定要删除版本 {version} 吗?",
|
||||||
|
"confirm_delete_button": "确认删除",
|
||||||
|
"delete_server_error": "删除版本失败:服务器返回错误",
|
||||||
|
"delete_unknown_error": "删除版本失败:发生未知错误",
|
||||||
|
"no_downloaded_versions": "暂无已下载的版本",
|
||||||
|
"refresh": "刷新",
|
||||||
|
"download_description": "从 NPM 下载 @anthropic-ai/claude-code 的可用版本",
|
||||||
|
"loading_versions": "加载版本列表中...",
|
||||||
|
"close": "关闭",
|
||||||
|
"cancel": "取消"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"root":["./src/app.tsx","./src/i18n.ts","./src/main.tsx","./src/routes.tsx","./src/types.ts","./src/vite-env.d.ts","./src/components/configprovider.tsx","./src/components/debugpage.tsx","./src/components/jsoneditor.tsx","./src/components/logviewer.tsx","./src/components/login.tsx","./src/components/protectedroute.tsx","./src/components/providerlist.tsx","./src/components/providers.tsx","./src/components/publicroute.tsx","./src/components/requesthistorydrawer.tsx","./src/components/router.tsx","./src/components/settingsdialog.tsx","./src/components/statuslineconfigdialog.tsx","./src/components/statuslineimportexport.tsx","./src/components/transformerlist.tsx","./src/components/transformers.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/color-picker.tsx","./src/components/ui/combo-input.tsx","./src/components/ui/combobox.tsx","./src/components/ui/command.tsx","./src/components/ui/dialog.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/multi-combobox.tsx","./src/components/ui/popover.tsx","./src/components/ui/switch.tsx","./src/components/ui/tabs.tsx","./src/components/ui/toast.tsx","./src/lib/api.ts","./src/lib/db.ts","./src/lib/utils.ts","./src/utils/statusline.ts"],"version":"5.8.3"}
|
{"root":["./src/app.tsx","./src/i18n.ts","./src/main.tsx","./src/routes.tsx","./src/types.ts","./src/vite-env.d.ts","./src/components/configprovider.tsx","./src/components/debugpage.tsx","./src/components/jsoneditor.tsx","./src/components/logviewer.tsx","./src/components/login.tsx","./src/components/protectedroute.tsx","./src/components/providerlist.tsx","./src/components/providers.tsx","./src/components/publicroute.tsx","./src/components/requesthistorydrawer.tsx","./src/components/router.tsx","./src/components/settingsdialog.tsx","./src/components/statuslineconfigdialog.tsx","./src/components/statuslineimportexport.tsx","./src/components/transformerlist.tsx","./src/components/transformers.tsx","./src/components/versionmanagerdialog.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/color-picker.tsx","./src/components/ui/combo-input.tsx","./src/components/ui/combobox.tsx","./src/components/ui/command.tsx","./src/components/ui/dialog.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/multi-combobox.tsx","./src/components/ui/popover.tsx","./src/components/ui/scroll-area.tsx","./src/components/ui/switch.tsx","./src/components/ui/tabs.tsx","./src/components/ui/toast.tsx","./src/lib/api.ts","./src/lib/db.ts","./src/lib/utils.ts","./src/utils/statusline.ts"],"version":"5.8.3"}
|
||||||
Reference in New Issue
Block a user