From aa18a354bbce202c487e9c041bf5ec2e9087132c Mon Sep 17 00:00:00 2001 From: musistudio Date: Sat, 27 Dec 2025 22:23:37 +0800 Subject: [PATCH] add presets ui --- packages/cli/src/utils/index.ts | 1 + packages/server/src/server.ts | 89 ++++++++++++++++- packages/ui/src/components/Presets.tsx | 129 +++---------------------- packages/ui/src/lib/api.ts | 10 ++ 4 files changed, 115 insertions(+), 114 deletions(-) diff --git a/packages/cli/src/utils/index.ts b/packages/cli/src/utils/index.ts index ec88850..c17dde1 100644 --- a/packages/cli/src/utils/index.ts +++ b/packages/cli/src/utils/index.ts @@ -189,6 +189,7 @@ export const run = async (args: string[] = []) => { return; } const server = await getServer(); + const app = server.app; // Save the PID of the background process writeFileSync(PID_FILE, process.pid.toString()); diff --git a/packages/server/src/server.ts b/packages/server/src/server.ts index 4dbfa6a..cad8e71 100644 --- a/packages/server/src/server.ts +++ b/packages/server/src/server.ts @@ -406,14 +406,101 @@ export const createServer = async (config: any): Promise => { } }); + // 获取预设市场列表 + app.get("/api/presets/market", async (req: any, reply: any) => { + try { + const marketUrl = "https://pub-0dc3e1677e894f07bbea11b17a29e032.r2.dev/presets.json"; + + const response = await fetch(marketUrl); + if (!response.ok) { + throw new Error(`Failed to fetch market presets: ${response.status} ${response.statusText}`); + } + + const marketPresets = await response.json(); + return { presets: marketPresets }; + } catch (error: any) { + console.error("Failed to get market presets:", error); + reply.status(500).send({ error: error.message || "Failed to get market presets" }); + } + }); + + // 从 GitHub 仓库安装预设 + app.post("/api/presets/install/github", async (req: any, reply: any) => { + try { + const { repo, name } = req.body; + + if (!repo) { + reply.status(400).send({ error: "Repository URL is required" }); + return; + } + + // 解析 GitHub 仓库 URL + // 支持格式: https://github.com/owner/repo 或 https://github.com/owner/repo.git + const githubRepoMatch = repo.match(/github\.com[:/]([^/]+)\/([^/.]+)/); + if (!githubRepoMatch) { + reply.status(400).send({ error: "Invalid GitHub repository URL" }); + return; + } + + const [, owner, repoName] = githubRepoMatch; + + // 下载 GitHub 仓库的 ZIP 文件 + const downloadUrl = `https://github.com/${owner}/${repoName}/archive/refs/heads/main.zip`; + const tempFile = await downloadPresetToTemp(downloadUrl); + + // 加载预设 + const preset = await loadPresetFromZip(tempFile); + + // 确定预设名称 + const presetName = name || preset.metadata?.name || repoName; + + // 检查是否已安装 + if (await isPresetInstalled(presetName)) { + unlinkSync(tempFile); + reply.status(409).send({ error: "Preset already installed" }); + return; + } + + // 解压到目标目录 + const targetDir = getPresetDir(presetName); + await extractPreset(tempFile, targetDir); + + // 清理临时文件 + unlinkSync(tempFile); + + return { + success: true, + presetName, + preset: { + ...preset.metadata, + installed: true, + } + }; + } catch (error: any) { + console.error("Failed to install preset from GitHub:", error); + reply.status(500).send({ error: error.message || "Failed to install preset from GitHub" }); + } + }); + // 辅助函数:从 ZIP 加载预设 async function loadPresetFromZip(zipFile: string): Promise { const AdmZip = (await import('adm-zip')).default; const zip = new AdmZip(zipFile); - const entry = zip.getEntry('manifest.json'); + + // 首先尝试在根目录查找 manifest.json + let entry = zip.getEntry('manifest.json'); + + // 如果根目录没有,尝试在子目录中查找(处理 GitHub 仓库的压缩包结构) + if (!entry) { + const entries = zip.getEntries(); + // 查找任意 manifest.json 文件 + entry = entries.find(e => e.entryName.includes('manifest.json')) || null; + } + if (!entry) { throw new Error('Invalid preset file: manifest.json not found'); } + const manifest = JSON.parse(entry.getData().toString('utf-8')) as ManifestFile; return manifestToPresetFile(manifest); } diff --git a/packages/ui/src/components/Presets.tsx b/packages/ui/src/components/Presets.tsx index 2a29072..2cd9bd0 100644 --- a/packages/ui/src/components/Presets.tsx +++ b/packages/ui/src/components/Presets.tsx @@ -42,16 +42,9 @@ interface PresetDetail extends PresetMetadata { interface MarketPreset { id: string; name: string; - version: string; - description?: string; author?: string; - homepage?: string; - repository?: string; - license?: string; - keywords?: string[]; - downloadUrl: string; - downloads?: number; - rating?: number; + description?: string; + repo: string; } export function Presets() { @@ -87,70 +80,8 @@ export function Presets() { const loadMarketPresets = async () => { setMarketLoading(true); try { - // TODO: 替换为实际的市场 API - // const response = await api.getMarketPresets(); - // setMarketPresets(response.presets || []); - - // 模拟数据 - const mockMarketPresets: MarketPreset[] = [ - { - id: 'openai-compatible', - name: 'OpenAI Compatible', - version: '1.0.0', - description: 'Full-featured OpenAI API compatible preset with support for GPT-4, GPT-3.5, and more.', - author: 'CCR Community', - homepage: 'https://github.com/example/openai-preset', - repository: 'https://github.com/example/openai-preset', - license: 'MIT', - keywords: ['openai', 'gpt', 'chat'], - downloadUrl: 'https://example.com/openai.ccrsets', - downloads: 1234, - rating: 4.8 - }, - { - id: 'anthropic-optimized', - name: 'Anthropic Optimized', - version: '1.2.0', - description: 'Optimized configuration for Claude and other Anthropic models with enhanced token management.', - author: 'CCR Team', - homepage: 'https://github.com/example/anthropic-preset', - repository: 'https://github.com/example/anthropic-preset', - license: 'Apache-2.0', - keywords: ['anthropic', 'claude', 'ai'], - downloadUrl: 'https://example.com/anthropic.ccrsets', - downloads: 892, - rating: 4.9 - }, - { - id: 'multi-provider', - name: 'Multi-Provider Router', - version: '2.0.0', - description: 'Intelligent routing across multiple providers based on cost, speed, and capability.', - author: 'CCR Community', - homepage: 'https://github.com/example/multi-provider-preset', - repository: 'https://github.com/example/multi-provider-preset', - license: 'MIT', - keywords: ['router', 'multi-provider', 'optimization'], - downloadUrl: 'https://example.com/multi-provider.ccrsets', - downloads: 567, - rating: 4.6 - }, - { - id: 'development-tools', - name: 'Development Tools', - version: '1.1.0', - description: 'Optimized for coding and development tasks with special focus on code generation and debugging.', - author: 'DevTeam', - homepage: 'https://github.com/example/dev-tools-preset', - repository: 'https://github.com/example/dev-tools-preset', - license: 'MIT', - keywords: ['development', 'coding', 'programming'], - downloadUrl: 'https://example.com/dev-tools.ccrsets', - downloads: 445, - rating: 4.7 - } - ]; - setMarketPresets(mockMarketPresets); + const response = await api.getMarketPresets(); + setMarketPresets(response.presets || []); } catch (error) { console.error('Failed to load market presets:', error); setToast({ message: t('presets.load_market_failed'), type: 'error' }); @@ -163,7 +94,7 @@ export function Presets() { const handleInstallFromMarket = async (preset: MarketPreset) => { try { setInstallingFromMarket(preset.id); - await api.installPresetFromUrl(preset.downloadUrl); + await api.installPresetFromGitHub(preset.repo, preset.name); setToast({ message: t('presets.preset_installed'), type: 'success' }); setMarketDialogOpen(false); await loadPresets(); @@ -186,8 +117,7 @@ export function Presets() { const filteredMarketPresets = marketPresets.filter(preset => preset.name.toLowerCase().includes(marketSearch.toLowerCase()) || preset.description?.toLowerCase().includes(marketSearch.toLowerCase()) || - preset.author?.toLowerCase().includes(marketSearch.toLowerCase()) || - preset.keywords?.some(keyword => keyword.toLowerCase().includes(marketSearch.toLowerCase())) + preset.author?.toLowerCase().includes(marketSearch.toLowerCase()) ); // 加载预设列表 @@ -583,53 +513,26 @@ export function Presets() {

{preset.name}

- v{preset.version} - {preset.rating && ( -
- - {preset.rating} -
- )}
{preset.description && (

{preset.description}

)} -
+
{preset.author && (
{t('presets.by', { author: preset.author })} - {preset.repository && ( - - - - )} + + +
)} - {preset.downloads && ( - {t('presets.downloads', { count: preset.downloads })} - )} - {preset.license && ( - {preset.license} - )}
- {preset.keywords && preset.keywords.length > 0 && ( -
- {preset.keywords.map((keyword) => ( - - {keyword} - - ))} -
- )}