mirror of
https://github.com/musistudio/claude-code-router.git
synced 2026-02-19 23:20:49 +08:00
add presets ui
This commit is contained in:
@@ -189,6 +189,7 @@ export const run = async (args: string[] = []) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const server = await getServer();
|
const server = await getServer();
|
||||||
|
const app = server.app;
|
||||||
// Save the PID of the background process
|
// Save the PID of the background process
|
||||||
writeFileSync(PID_FILE, process.pid.toString());
|
writeFileSync(PID_FILE, process.pid.toString());
|
||||||
|
|
||||||
|
|||||||
@@ -406,14 +406,101 @@ export const createServer = async (config: any): Promise<any> => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 获取预设市场列表
|
||||||
|
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 加载预设
|
// 辅助函数:从 ZIP 加载预设
|
||||||
async function loadPresetFromZip(zipFile: string): Promise<PresetFile> {
|
async function loadPresetFromZip(zipFile: string): Promise<PresetFile> {
|
||||||
const AdmZip = (await import('adm-zip')).default;
|
const AdmZip = (await import('adm-zip')).default;
|
||||||
const zip = new AdmZip(zipFile);
|
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) {
|
if (!entry) {
|
||||||
throw new Error('Invalid preset file: manifest.json not found');
|
throw new Error('Invalid preset file: manifest.json not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const manifest = JSON.parse(entry.getData().toString('utf-8')) as ManifestFile;
|
const manifest = JSON.parse(entry.getData().toString('utf-8')) as ManifestFile;
|
||||||
return manifestToPresetFile(manifest);
|
return manifestToPresetFile(manifest);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,16 +42,9 @@ interface PresetDetail extends PresetMetadata {
|
|||||||
interface MarketPreset {
|
interface MarketPreset {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
version: string;
|
|
||||||
description?: string;
|
|
||||||
author?: string;
|
author?: string;
|
||||||
homepage?: string;
|
description?: string;
|
||||||
repository?: string;
|
repo: string;
|
||||||
license?: string;
|
|
||||||
keywords?: string[];
|
|
||||||
downloadUrl: string;
|
|
||||||
downloads?: number;
|
|
||||||
rating?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Presets() {
|
export function Presets() {
|
||||||
@@ -87,70 +80,8 @@ export function Presets() {
|
|||||||
const loadMarketPresets = async () => {
|
const loadMarketPresets = async () => {
|
||||||
setMarketLoading(true);
|
setMarketLoading(true);
|
||||||
try {
|
try {
|
||||||
// TODO: 替换为实际的市场 API
|
const response = await api.getMarketPresets();
|
||||||
// const response = await api.getMarketPresets();
|
setMarketPresets(response.presets || []);
|
||||||
// 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);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load market presets:', error);
|
console.error('Failed to load market presets:', error);
|
||||||
setToast({ message: t('presets.load_market_failed'), type: 'error' });
|
setToast({ message: t('presets.load_market_failed'), type: 'error' });
|
||||||
@@ -163,7 +94,7 @@ export function Presets() {
|
|||||||
const handleInstallFromMarket = async (preset: MarketPreset) => {
|
const handleInstallFromMarket = async (preset: MarketPreset) => {
|
||||||
try {
|
try {
|
||||||
setInstallingFromMarket(preset.id);
|
setInstallingFromMarket(preset.id);
|
||||||
await api.installPresetFromUrl(preset.downloadUrl);
|
await api.installPresetFromGitHub(preset.repo, preset.name);
|
||||||
setToast({ message: t('presets.preset_installed'), type: 'success' });
|
setToast({ message: t('presets.preset_installed'), type: 'success' });
|
||||||
setMarketDialogOpen(false);
|
setMarketDialogOpen(false);
|
||||||
await loadPresets();
|
await loadPresets();
|
||||||
@@ -186,8 +117,7 @@ export function Presets() {
|
|||||||
const filteredMarketPresets = marketPresets.filter(preset =>
|
const filteredMarketPresets = marketPresets.filter(preset =>
|
||||||
preset.name.toLowerCase().includes(marketSearch.toLowerCase()) ||
|
preset.name.toLowerCase().includes(marketSearch.toLowerCase()) ||
|
||||||
preset.description?.toLowerCase().includes(marketSearch.toLowerCase()) ||
|
preset.description?.toLowerCase().includes(marketSearch.toLowerCase()) ||
|
||||||
preset.author?.toLowerCase().includes(marketSearch.toLowerCase()) ||
|
preset.author?.toLowerCase().includes(marketSearch.toLowerCase())
|
||||||
preset.keywords?.some(keyword => keyword.toLowerCase().includes(marketSearch.toLowerCase()))
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// 加载预设列表
|
// 加载预设列表
|
||||||
@@ -583,53 +513,26 @@ export function Presets() {
|
|||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<h3 className="font-semibold text-lg">{preset.name}</h3>
|
<h3 className="font-semibold text-lg">{preset.name}</h3>
|
||||||
<span className="text-xs text-gray-500">v{preset.version}</span>
|
|
||||||
{preset.rating && (
|
|
||||||
<div className="flex items-center gap-1 text-xs text-yellow-600">
|
|
||||||
<span>★</span>
|
|
||||||
<span>{preset.rating}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
{preset.description && (
|
{preset.description && (
|
||||||
<p className="text-sm text-gray-600 mb-2">{preset.description}</p>
|
<p className="text-sm text-gray-600 mb-2">{preset.description}</p>
|
||||||
)}
|
)}
|
||||||
<div className="flex items-center gap-4 text-sm text-gray-500 mb-2">
|
<div className="flex items-center gap-4 text-sm text-gray-500">
|
||||||
{preset.author && (
|
{preset.author && (
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<span className="font-medium">{t('presets.by', { author: preset.author })}</span>
|
<span className="font-medium">{t('presets.by', { author: preset.author })}</span>
|
||||||
{preset.repository && (
|
|
||||||
<a
|
<a
|
||||||
href={preset.repository}
|
href={`https://github.com/${preset.repo}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-gray-600 hover:text-gray-900 transition-colors"
|
className="text-gray-600 hover:text-gray-900 transition-colors"
|
||||||
title={t('presets.github_repository')}
|
title={t('presets.github_repository')}
|
||||||
>
|
>
|
||||||
<i className="ri-github-fill text-base"></i>
|
<i className="ri-github-fill text-xl"></i>
|
||||||
</a>
|
</a>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{preset.downloads && (
|
|
||||||
<span>{t('presets.downloads', { count: preset.downloads })}</span>
|
|
||||||
)}
|
|
||||||
{preset.license && (
|
|
||||||
<span>{preset.license}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
{preset.keywords && preset.keywords.length > 0 && (
|
|
||||||
<div className="flex flex-wrap gap-1">
|
|
||||||
{preset.keywords.map((keyword) => (
|
|
||||||
<span
|
|
||||||
key={keyword}
|
|
||||||
className="px-2 py-0.5 bg-blue-50 text-blue-600 rounded text-xs"
|
|
||||||
>
|
|
||||||
{keyword}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => handleInstallFromMarket(preset)}
|
onClick={() => handleInstallFromMarket(preset)}
|
||||||
|
|||||||
@@ -302,6 +302,16 @@ class ApiClient {
|
|||||||
async deletePreset(name: string): Promise<any> {
|
async deletePreset(name: string): Promise<any> {
|
||||||
return this.delete<any>(`/presets/${encodeURIComponent(name)}`);
|
return this.delete<any>(`/presets/${encodeURIComponent(name)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get market presets
|
||||||
|
async getMarketPresets(): Promise<{ presets: Array<any> }> {
|
||||||
|
return this.get<{ presets: Array<any> }>('/presets/market');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Install preset from GitHub repository
|
||||||
|
async installPresetFromGitHub(repo: string, name?: string): Promise<any> {
|
||||||
|
return this.post<any>('/presets/install/github', { repo, name });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a default instance of the API client
|
// Create a default instance of the API client
|
||||||
|
|||||||
Reference in New Issue
Block a user