From af2ddecad0b18e5e9015a46b7ae2dabc71bfc50d Mon Sep 17 00:00:00 2001 From: musistudio Date: Sat, 15 Nov 2025 17:54:48 +0800 Subject: [PATCH] add version manager --- src/codeManager/downloder.ts | 190 ++++++++++ src/codeManager/wrapper.ts | 0 src/server.ts | 194 +++++++++- ui/src/App.tsx | 20 +- ui/src/components/VersionManagerDialog.tsx | 407 +++++++++++++++++++++ ui/src/components/ui/scroll-area.tsx | 19 + ui/src/lib/api.ts | 85 +++-- ui/src/locales/en.json | 32 ++ ui/src/locales/zh.json | 32 ++ ui/tsconfig.tsbuildinfo | 2 +- 10 files changed, 946 insertions(+), 35 deletions(-) create mode 100644 src/codeManager/downloder.ts create mode 100644 src/codeManager/wrapper.ts create mode 100644 ui/src/components/VersionManagerDialog.tsx create mode 100644 ui/src/components/ui/scroll-area.tsx diff --git a/src/codeManager/downloder.ts b/src/codeManager/downloder.ts new file mode 100644 index 0000000..e843994 --- /dev/null +++ b/src/codeManager/downloder.ts @@ -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 { + 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 { + 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 { + 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 { + // 如果没有指定解压目录,则使用版本号作为目录名 + 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 { + const tarballPath = await this.downloadVersion(options); + return this.extractPackage(tarballPath, options.destination); + } +} + +// 导出便捷函数 +export async function downloadClaudeCode(version?: string, options?: Omit): Promise { + const downloader = new NpmDownloader(); + return downloader.downloadVersion({ ...options, version }); +} + +export async function downloadAndExtractClaudeCode(version?: string, options?: Omit): Promise { + const downloader = new NpmDownloader(); + return downloader.downloadAndExtract({ ...options, version }); +} + +export async function getLatestClaudeCodeVersion(): Promise { + const downloader = new NpmDownloader(); + return downloader.getLatestVersion(); +} + +export async function getAvailableClaudeCodeVersions(): Promise { + const downloader = new NpmDownloader(); + return downloader.getAvailableVersions(); +} \ No newline at end of file diff --git a/src/codeManager/wrapper.ts b/src/codeManager/wrapper.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/server.ts b/src/server.ts index 6ca3e4d..59986b7 100644 --- a/src/server.ts +++ b/src/server.ts @@ -3,9 +3,11 @@ import { readConfigFile, writeConfigFile, backupConfigFile } from "./utils"; import { checkForUpdates, performUpdate } from "./utils"; import { join } from "path"; 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 {calculateTokenCount} from "./utils/router"; +import { NpmDownloader } from "./codeManager/downloder"; +import { HOME_DIR } from "./constants"; export const createServer = (config: any): Server => { 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; }; diff --git a/ui/src/App.tsx b/ui/src/App.tsx index b641680..accb28d 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -7,10 +7,11 @@ import { Providers } from "@/components/Providers"; import { Router } from "@/components/Router"; import { JsonEditor } from "@/components/JsonEditor"; import { LogViewer } from "@/components/LogViewer"; +import { VersionManagerDialog } from "@/components/VersionManagerDialog"; import { Button } from "@/components/ui/button"; import { useConfig } from "@/components/ConfigProvider"; 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 { Popover, PopoverContent, @@ -34,6 +35,7 @@ function App() { const [isSettingsOpen, setIsSettingsOpen] = useState(false); const [isJsonEditorOpen, setIsJsonEditorOpen] = useState(false); const [isLogViewerOpen, setIsLogViewerOpen] = useState(false); + const [isVersionManagerOpen, setIsVersionManagerOpen] = useState(false); const [isCheckingAuth, setIsCheckingAuth] = useState(true); const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' | 'warning' } | null>(null); // 版本检查状态 @@ -281,6 +283,9 @@ function App() { + + )} + + + + ))} + {downloadedVersions.length === 0 && ( +
+ +

{t('version_manager.no_downloaded_versions')}

+
+ )} + + + + + + + + + +
+ {t('version_manager.available_versions')} + +
+ + {t('version_manager.download_description')} + +
+ + +
+ {availableVersions.map((version) => { + const isDownloaded = downloadedVersions.some(v => v.version === version); + const isDownloading = downloadingVersion === version; + + return ( +
+
+ + {version} + {isDownloaded && ( + {t('version_manager.downloaded')} + )} +
+ +
+ ); + })} + {availableVersions.length === 0 && !isLoading && ( +
+ +

{t('version_manager.no_available_versions')}

+
+ )} + {isLoading && ( +
+ +

{t('version_manager.loading_versions')}

+
+ )} +
+
+
+
+
+ + + + + + + + + {/* 删除确认对话框 */} + + + + + + {t('version_manager.confirm_delete_title')} + + + {(() => { + 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 && ( +
+

{deleteError}

+
+ )} +
+
+ + + + +
+
+ + ); +}; \ No newline at end of file diff --git a/ui/src/components/ui/scroll-area.tsx b/ui/src/components/ui/scroll-area.tsx new file mode 100644 index 0000000..77ce11b --- /dev/null +++ b/ui/src/components/ui/scroll-area.tsx @@ -0,0 +1,19 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const ScrollArea = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, children, ...props }, ref) => ( +
+ {children} +
+)) +ScrollArea.displayName = "ScrollArea" + +export { ScrollArea } \ No newline at end of file diff --git a/ui/src/lib/api.ts b/ui/src/lib/api.ts index b98e787..8cc5c57 100644 --- a/ui/src/lib/api.ts +++ b/ui/src/lib/api.ts @@ -45,7 +45,7 @@ class ApiClient { localStorage.removeItem('apiKey'); } } - + // Update temp API key setTempApiKey(tempApiKey: string | null) { this.tempApiKey = tempApiKey; @@ -56,25 +56,25 @@ class ApiClient { const headers: Record = { 'Accept': 'application/json', }; - + // Use temp API key if available, otherwise use regular API key if (this.tempApiKey) { headers['X-Temp-API-Key'] = this.tempApiKey; } else if (this.apiKey) { headers['X-API-Key'] = this.apiKey; } - + if (contentType) { headers['Content-Type'] = contentType; } - + return headers; } // Generic fetch wrapper with base URL and authentication private async apiFetch(endpoint: string, options: RequestInit = {}): Promise { const url = `${this.baseUrl}${endpoint}`; - + const config: RequestInit = { ...options, headers: { @@ -82,10 +82,10 @@ class ApiClient { ...options.headers, }, }; - + try { const response = await fetch(url, config); - + // Handle 401 Unauthorized responses if (response.status === 401) { // Remove API key when it's invalid @@ -99,13 +99,14 @@ class ApiClient { } 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) { return {} as T; } - + const text = await response.text(); return text ? JSON.parse(text) : ({} as T); @@ -142,6 +143,7 @@ class ApiClient { async delete(endpoint: string): Promise { return this.apiFetch(endpoint, { method: 'DELETE', + headers: this.createHeaders(''), // 不设置Content-Type,因为没有请求体 }); } @@ -150,95 +152,122 @@ class ApiClient { async getConfig(): Promise { return this.get('/config'); } - + // Update entire configuration async updateConfig(config: Config): Promise { return this.post('/config', config); } - + // Get providers async getProviders(): Promise { return this.get('/api/providers'); } - + // Add a new provider async addProvider(provider: Provider): Promise { return this.post('/api/providers', provider); } - + // Update a provider async updateProvider(index: number, provider: Provider): Promise { return this.post(`/api/providers/${index}`, provider); } - + // Delete a provider async deleteProvider(index: number): Promise { return this.delete(`/api/providers/${index}`); } - + // Get transformers async getTransformers(): Promise { return this.get('/api/transformers'); } - + // Add a new transformer async addTransformer(transformer: Transformer): Promise { return this.post('/api/transformers', transformer); } - + // Update a transformer async updateTransformer(index: number, transformer: Transformer): Promise { return this.post(`/api/transformers/${index}`, transformer); } - + // Delete a transformer async deleteTransformer(index: number): Promise { return this.delete(`/api/transformers/${index}`); } - + // Get configuration (new endpoint) async getConfigNew(): Promise { return this.get('/config'); } - + // Save configuration (new endpoint) async saveConfig(config: Config): Promise { return this.post('/config', config); } - + // Restart service async restartService(): Promise { return this.post('/restart', {}); } - + // Check for updates async checkForUpdates(): Promise<{ hasUpdate: boolean; latestVersion?: string; changelog?: string }> { return this.get<{ hasUpdate: boolean; latestVersion?: string; changelog?: string }>('/update/check'); } - + // Perform update async performUpdate(): Promise<{ success: boolean; message: string }> { return this.post<{ success: boolean; message: string }>('/api/update/perform', {}); } - + // Get log files list async getLogFiles(): Promise> { return this.get>('/logs/files'); } - + // Get logs from specific file async getLogs(filePath: string): Promise { return this.get(`/logs?file=${encodeURIComponent(filePath)}`); } - + // Clear logs from specific file async clearLogs(filePath: string): Promise { return this.delete(`/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 export const api = new ApiClient(); // Export the class for creating custom instances -export default ApiClient; \ No newline at end of file +export default ApiClient; diff --git a/ui/src/locales/en.json b/ui/src/locales/en.json index 201cb56..42e4a6d 100644 --- a/ui/src/locales/en.json +++ b/ui/src/locales/en.json @@ -224,5 +224,37 @@ "worker_init_failed": "Failed to initialize worker", "grouping_not_supported": "Log grouping not supported by server", "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" } } diff --git a/ui/src/locales/zh.json b/ui/src/locales/zh.json index 0f994bb..bb1d2ec 100644 --- a/ui/src/locales/zh.json +++ b/ui/src/locales/zh.json @@ -224,5 +224,37 @@ "worker_init_failed": "Worker初始化失败", "grouping_not_supported": "服务器不支持日志分组", "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": "取消" } } diff --git a/ui/tsconfig.tsbuildinfo b/ui/tsconfig.tsbuildinfo index 28cd675..7d47c7f 100644 --- a/ui/tsconfig.tsbuildinfo +++ b/ui/tsconfig.tsbuildinfo @@ -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"} \ No newline at end of file +{"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"} \ No newline at end of file