remove ccrsets

This commit is contained in:
musistudio
2026-01-01 15:54:40 +08:00
parent d149517026
commit ec7ac8cc9f
12 changed files with 116 additions and 319 deletions

View File

@@ -169,13 +169,13 @@ Each preset contains:
Located in `packages/shared/src/preset/`: Located in `packages/shared/src/preset/`:
- **export.ts**: Export current configuration as a preset (.ccrsets file) - **export.ts**: Export current configuration as a preset directory
- `exportPreset(presetName, config, options)`: Creates ZIP archive with manifest.json - `exportPreset(presetName, config, options)`: Creates preset directory with manifest.json
- Automatically sanitizes sensitive data (api_key fields become `{{field}}` placeholders) - Automatically sanitizes sensitive data (api_key fields become `{{field}}` placeholders)
- **install.ts**: Install and manage presets - **install.ts**: Install and manage presets
- `installPreset(preset, config, options)`: Install preset to config - `installPreset(preset, config, options)`: Install preset to config
- `loadPreset(source)`: Load preset from file, URL, or directory - `loadPreset(source)`: Load preset from directory
- `listPresets()`: List all installed presets - `listPresets()`: List all installed presets
- `isPresetInstalled(presetName)`: Check if preset is installed - `isPresetInstalled(presetName)`: Check if preset is installed
- `validatePreset(preset)`: Validate preset structure - `validatePreset(preset)`: Validate preset structure
@@ -189,7 +189,7 @@ Located in `packages/shared/src/preset/`:
### Preset File Format ### Preset File Format
**manifest.json** (in ZIP archive or extracted directory): **manifest.json** (in preset directory):
```json ```json
{ {
"name": "my-preset", "name": "my-preset",

View File

@@ -264,9 +264,8 @@ ccr preset export my-preset
# Export with metadata # Export with metadata
ccr preset export my-preset --description "My OpenAI config" --author "Your Name" --tags "openai,production" ccr preset export my-preset --description "My OpenAI config" --author "Your Name" --tags "openai,production"
# Install a preset from file, URL, or registry # Install a preset from local directory
ccr preset install my-preset.ccrsets ccr preset install /path/to/preset
ccr preset install https://example.com/preset.ccrsets
# List all installed presets # List all installed presets
ccr preset list ccr preset list
@@ -279,8 +278,8 @@ ccr preset delete my-preset
``` ```
**Preset Features:** **Preset Features:**
- **Export**: Save your current configuration as a `.ccrsets` file (ZIP archive with manifest.json) - **Export**: Save your current configuration as a preset directory (with manifest.json)
- **Install**: Install presets from local files, URLs, or the preset registry - **Install**: Install presets from local directories
- **Sensitive Data Handling**: API keys and other sensitive data are automatically sanitized during export (marked as `{{field}}` placeholders) - **Sensitive Data Handling**: API keys and other sensitive data are automatically sanitized during export (marked as `{{field}}` placeholders)
- **Dynamic Configuration**: Presets can include input schemas for collecting required information during installation - **Dynamic Configuration**: Presets can include input schemas for collecting required information during installation
- **Version Control**: Each preset includes version metadata for tracking updates - **Version Control**: Each preset includes version metadata for tracking updates

View File

@@ -239,9 +239,8 @@ ccr preset export my-preset
# 使用元数据导出 # 使用元数据导出
ccr preset export my-preset --description "我的 OpenAI 配置" --author "您的名字" --tags "openai,生产环境" ccr preset export my-preset --description "我的 OpenAI 配置" --author "您的名字" --tags "openai,生产环境"
# 从文件、URL 或注册表安装预设 # 从本地目录安装预设
ccr preset install my-preset.ccrsets ccr preset install /path/to/preset
ccr preset install https://example.com/preset.ccrsets
# 列出所有已安装的预设 # 列出所有已安装的预设
ccr preset list ccr preset list
@@ -254,8 +253,8 @@ ccr preset delete my-preset
``` ```
**预设功能:** **预设功能:**
- **导出**:将当前配置保存为 `.ccrsets` 文件(包含 manifest.json 的 ZIP 存档 - **导出**:将当前配置保存为预设目录(包含 manifest.json
- **安装**:从本地文件、URL 或预设注册表安装预设 - **安装**:从本地目录安装预设
- **敏感数据处理**:导出期间自动清理 API 密钥和其他敏感数据(标记为 `{{field}}` 占位符) - **敏感数据处理**:导出期间自动清理 API 密钥和其他敏感数据(标记为 `{{field}}` 占位符)
- **动态配置**:预设可以包含输入架构,用于在安装期间收集所需信息 - **动态配置**:预设可以包含输入架构,用于在安装期间收集所需信息
- **版本控制**:每个预设包含版本元数据,用于跟踪更新 - **版本控制**:每个预设包含版本元数据,用于跟踪更新

View File

@@ -58,7 +58,7 @@ Commands:
-h, help Show help information -h, help Show help information
Presets: Presets:
Any preset-name defined in ~/.claude-code-router/presets/*.ccrsets Any preset directory in ~/.claude-code-router/presets/
Examples: Examples:
ccr start ccr start
@@ -66,7 +66,7 @@ Examples:
ccr my-preset "Write a Hello World" # Use preset configuration ccr my-preset "Write a Hello World" # Use preset configuration
ccr model ccr model
ccr preset export my-config # Export current config as preset ccr preset export my-config # Export current config as preset
ccr preset install my-config.ccrsets # Install a preset ccr preset install /path/to/preset # Install a preset from directory
ccr preset list # List all presets ccr preset list # List all presets
eval "$(ccr activate)" # Set environment variables globally eval "$(ccr activate)" # Set environment variables globally
ccr ui ccr ui

View File

@@ -72,7 +72,7 @@ export async function exportPresetCli(
// 4. Display summary // 4. Display summary
console.log(`\n${BOLDGREEN}✓ Preset exported successfully${RESET}\n`); console.log(`\n${BOLDGREEN}✓ Preset exported successfully${RESET}\n`);
console.log(`${BOLDCYAN}Location:${RESET} ${result.outputPath}\n`); console.log(`${BOLDCYAN}Location:${RESET} ${result.presetDir}\n`);
console.log(`${BOLDCYAN}Summary:${RESET}`); console.log(`${BOLDCYAN}Summary:${RESET}`);
console.log(` - Providers: ${result.sanitizedConfig.Providers?.length || 0}`); console.log(` - Providers: ${result.sanitizedConfig.Providers?.length || 0}`);
console.log(` - Router rules: ${Object.keys(result.sanitizedConfig.Router || {}).length}`); console.log(` - Router rules: ${Object.keys(result.sanitizedConfig.Router || {}).length}`);
@@ -93,9 +93,9 @@ export async function exportPresetCli(
// Display sharing tips // Display sharing tips
console.log(`\n${BOLDCYAN}To share this preset:${RESET}`); console.log(`\n${BOLDCYAN}To share this preset:${RESET}`);
console.log(` 1. Share the file: ${result.outputPath}`); console.log(` 1. Share the directory: ${result.presetDir}`);
console.log(` 2. Upload to GitHub Gist or your repository`); console.log(` 2. Upload to GitHub Gist or your repository`);
console.log(` 3. Others can install with: ${GREEN}ccr preset install <file>${RESET}\n`); console.log(` 3. Others can install with: ${GREEN}ccr preset install <directory>${RESET}\n`);
} catch (error: any) { } catch (error: any) {
console.error(`\n${YELLOW}Error exporting preset:${RESET} ${error.message}`); console.error(`\n${YELLOW}Error exporting preset:${RESET} ${error.message}`);

View File

@@ -13,8 +13,6 @@ import {
readManifestFromDir, readManifestFromDir,
manifestToPresetFile, manifestToPresetFile,
saveManifest, saveManifest,
extractPreset,
findPresetFile,
isPresetInstalled, isPresetInstalled,
ManifestFile, ManifestFile,
PresetFile, PresetFile,
@@ -151,53 +149,39 @@ export async function installPresetCli(
try { try {
// Determine preset name // Determine preset name
let presetName = options.name; let presetName = options.name;
let sourceZip: string | undefined; let sourceDir: string | undefined;
let isReconfigure = false; // Whether to reconfigure installed preset let isReconfigure = false; // Whether to reconfigure installed preset
// Determine source type and get ZIP file path // Determine source type and get directory path
if (source.startsWith('http://') || source.startsWith('https://')) { if (source.startsWith('http://') || source.startsWith('https://')) {
// URL: download to temp file // URL installation not supported
if (!presetName) { throw new Error('URL installation is not supported. Please download the preset directory and install from local path.');
const urlParts = source.split('/');
const filename = urlParts[urlParts.length - 1];
presetName = filename.replace('.ccrsets', '');
}
// downloadPresetToTemp imported from shared package will return temp file
// but we'll auto-cleanup in loadPreset, so no need to handle here
// Re-download to temp file for extractPreset usage
// Since loadPreset already downloaded and deleted, special handling needed here
throw new Error('URL installation not fully implemented yet');
} else if (source.includes('/') || source.includes('\\')) { } else if (source.includes('/') || source.includes('\\')) {
// File path // Directory path
if (!presetName) { if (!presetName) {
const filename = path.basename(source); presetName = path.basename(source);
presetName = filename.replace('.ccrsets', '');
} }
// Verify file exists // Verify directory exists
try { try {
await fs.access(source); const stats = await fs.stat(source);
if (!stats.isDirectory()) {
throw new Error(`Source is not a directory: ${source}`);
}
} catch { } catch {
throw new Error(`Preset file not found: ${source}`); throw new Error(`Preset directory not found: ${source}`);
} }
sourceZip = source; sourceDir = source;
} else { } else {
// Preset name (without path) // Preset name (without path)
presetName = source; presetName = source;
// Search files by priority: current directory -> presets directory // Check if already installed (directory exists)
const presetFile = await findPresetFile(source); if (await isPresetInstalled(source)) {
// Already installed, reconfigure
if (presetFile) { isReconfigure = true;
sourceZip = presetFile;
} else { } else {
// Check if already installed (directory exists) // Not found, error
if (await isPresetInstalled(source)) { throw new Error(`Preset '${source}' not found. Please provide a valid preset directory path.`);
// Already installed, reconfigure
isReconfigure = true;
} else {
// Neither exists, error
throw new Error(`Preset '${source}' not found in current directory or presets directory.`);
}
} }
} }
@@ -212,17 +196,16 @@ export async function installPresetCli(
// Apply preset (will ask for sensitive info) // Apply preset (will ask for sensitive info)
await applyPresetCli(presetName, preset); await applyPresetCli(presetName, preset);
} else { } else {
// New installation: extract to target directory // New installation: read from source directory
if (!sourceZip) { if (!sourceDir) {
throw new Error('Source ZIP file is required for installation'); throw new Error('Source directory is required for installation');
} }
const targetDir = getPresetDir(presetName);
console.log(`${BOLDCYAN}Extracting preset to:${RESET} ${targetDir}`);
await extractPreset(sourceZip, targetDir);
console.log(`${GREEN}${RESET} Extracted successfully\n`);
// Read manifest from extracted directory console.log(`${BOLDCYAN}Reading preset from:${RESET} ${sourceDir}`);
const manifest = await readManifestFromDir(targetDir); console.log(`${GREEN}${RESET} Read successfully\n`);
// Read manifest from source directory
const manifest = await readManifestFromDir(sourceDir);
const preset = manifestToPresetFile(manifest); const preset = manifestToPresetFile(manifest);
// Apply preset (ask user info, etc.) // Apply preset (ask user info, etc.)

View File

@@ -8,14 +8,14 @@ import {
getPresetDir, getPresetDir,
readManifestFromDir, readManifestFromDir,
manifestToPresetFile, manifestToPresetFile,
extractPreset,
saveManifest, saveManifest,
isPresetInstalled, isPresetInstalled,
downloadPresetToTemp, extractPreset,
getTempDir,
HOME_DIR, HOME_DIR,
extractMetadata, extractMetadata,
loadConfigFromManifest, loadConfigFromManifest,
downloadPresetToTemp,
getTempDir,
type PresetFile, type PresetFile,
type ManifestFile, type ManifestFile,
type PresetMetadata, type PresetMetadata,
@@ -288,103 +288,6 @@ export const createServer = async (config: any): Promise<any> => {
} }
}); });
// Upload and install preset (supports file upload)
app.post("/api/presets/install", async (req: any, reply: any) => {
try {
const { source, name, url } = req.body;
// If URL is provided, download from URL
if (url) {
const tempFile = await downloadPresetToTemp(url);
const preset = await loadPresetFromZip(tempFile);
// Determine preset name
const presetName = name || preset.metadata?.name || `preset-${Date.now()}`;
// Check if already installed
if (await isPresetInstalled(presetName)) {
reply.status(409).send({ error: "Preset already installed" });
return;
}
// Extract to target directory
const targetDir = getPresetDir(presetName);
await extractPreset(tempFile, targetDir);
// Clean up temp file
unlinkSync(tempFile);
return {
success: true,
presetName,
preset: {
...preset.metadata,
installed: true,
}
};
}
// If no URL, need to handle file upload (using multipart/form-data)
// This part requires FormData upload on client side
reply.status(400).send({ error: "Please provide a URL or upload a file" });
} catch (error: any) {
console.error("Failed to install preset:", error);
reply.status(500).send({ error: error.message || "Failed to install preset" });
}
});
// Upload preset file (multipart/form-data)
app.post("/api/presets/upload", async (req: any, reply: any) => {
try {
const data = await req.file();
if (!data) {
reply.status(400).send({ error: "No file uploaded" });
return;
}
const tempDir = getTempDir();
mkdirSync(tempDir, { recursive: true });
const tempFile = join(tempDir, `preset-${Date.now()}.ccrsets`);
// Save uploaded file to temp location
const buffer = await data.toBuffer();
writeFileSync(tempFile, buffer);
// Load preset
const preset = await loadPresetFromZip(tempFile);
// Determine preset name
const presetName = data.fields.name?.value || preset.metadata?.name || `preset-${Date.now()}`;
// Check if already installed
if (await isPresetInstalled(presetName)) {
unlinkSync(tempFile);
reply.status(409).send({ error: "Preset already installed" });
return;
}
// Extract to target directory
const targetDir = getPresetDir(presetName);
await extractPreset(tempFile, targetDir);
// Clean up temp file
unlinkSync(tempFile);
return {
success: true,
presetName,
preset: {
...preset.metadata,
installed: true,
}
};
} catch (error: any) {
console.error("Failed to upload preset:", error);
reply.status(500).send({ error: error.message || "Failed to upload preset" });
}
});
// Apply preset (configure sensitive information) // Apply preset (configure sensitive information)
app.post("/api/presets/:name/apply", async (req: any, reply: any) => { app.post("/api/presets/:name/apply", async (req: any, reply: any) => {
try { try {

View File

@@ -4,18 +4,15 @@
*/ */
import * as fs from 'fs/promises'; import * as fs from 'fs/promises';
import * as fsSync from 'fs';
import * as path from 'path'; import * as path from 'path';
import archiver from 'archiver';
import { sanitizeConfig } from './sensitiveFields'; import { sanitizeConfig } from './sensitiveFields';
import { PresetFile, PresetMetadata, ManifestFile } from './types'; import { PresetMetadata, ManifestFile } from './types';
import { HOME_DIR } from '../constants'; import { HOME_DIR } from '../constants';
/** /**
* Export options * Export options
*/ */
export interface ExportOptions { export interface ExportOptions {
output?: string;
includeSensitive?: boolean; includeSensitive?: boolean;
description?: string; description?: string;
author?: string; author?: string;
@@ -26,7 +23,7 @@ export interface ExportOptions {
* Export result * Export result
*/ */
export interface ExportResult { export interface ExportResult {
outputPath: string; presetDir: string;
sanitizedConfig: any; sanitizedConfig: any;
metadata: PresetMetadata; metadata: PresetMetadata;
requiredInputs: any[]; requiredInputs: any[];
@@ -93,42 +90,31 @@ export async function exportPreset(
requiredInputs: options.includeSensitive ? undefined : requiredInputs, requiredInputs: options.includeSensitive ? undefined : requiredInputs,
}; };
// 4. Determine output path // 4. Create preset directory
const presetsDir = path.join(HOME_DIR, 'presets'); const presetDir = path.join(HOME_DIR, 'presets', presetName);
// Ensure presets directory exists // Check if preset directory already exists
await fs.mkdir(presetsDir, { recursive: true }); try {
await fs.access(presetDir);
throw new Error(`Preset directory already exists: ${presetName}`);
} catch (error: any) {
if (error.code !== 'ENOENT') {
throw error;
}
}
const outputPath = options.output || path.join(presetsDir, `${presetName}.ccrsets`); // Create preset directory
await fs.mkdir(presetDir, { recursive: true });
// 5. Create archive // 5. Write manifest.json to preset directory
const output = fsSync.createWriteStream(outputPath); const manifestPath = path.join(presetDir, 'manifest.json');
const archive = archiver('zip', { await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2), 'utf-8');
zlib: { level: 9 } // Highest compression level
});
return new Promise<ExportResult>((resolve, reject) => { return {
output.on('close', () => { presetDir,
resolve({ sanitizedConfig,
outputPath, metadata,
sanitizedConfig, requiredInputs,
metadata, sanitizedCount,
requiredInputs, };
sanitizedCount,
});
});
archive.on('error', (err: Error) => {
reject(err);
});
// Connect output stream
archive.pipe(output);
// Add manifest.json to archive
archive.append(JSON.stringify(manifest, null, 2), { name: 'manifest.json' });
// Finalize archive
archive.finalize();
});
} }

View File

@@ -7,7 +7,7 @@ import * as fs from 'fs/promises';
import * as path from 'path'; import * as path from 'path';
import JSON5 from 'json5'; import JSON5 from 'json5';
import AdmZip from 'adm-zip'; import AdmZip from 'adm-zip';
import { PresetFile, MergeStrategy, RequiredInput, ManifestFile, PresetInfo, PresetMetadata } from './types'; import { PresetFile, ManifestFile, PresetInfo, PresetMetadata } from './types';
import { HOME_DIR, PRESETS_DIR } from '../constants'; import { HOME_DIR, PRESETS_DIR } from '../constants';
import { loadConfigFromManifest } from './schema'; import { loadConfigFromManifest } from './schema';
@@ -236,48 +236,27 @@ export async function downloadPresetToTemp(url: string): Promise<string> {
const tempDir = getTempDir(); const tempDir = getTempDir();
await fs.mkdir(tempDir, { recursive: true }); await fs.mkdir(tempDir, { recursive: true });
const tempFile = path.join(tempDir, `preset-${Date.now()}.ccrsets`); const tempFile = path.join(tempDir, `preset-${Date.now()}.zip`);
await fs.writeFile(tempFile, Buffer.from(buffer)); await fs.writeFile(tempFile, Buffer.from(buffer));
return tempFile; return tempFile;
} }
/**
* Load preset from local ZIP file
* @param zipFile ZIP file path
* @returns PresetFile
*/
export async function loadPresetFromZip(zipFile: string): Promise<PresetFile> {
const zip = new AdmZip(zipFile);
const entry = zip.getEntry('manifest.json');
if (!entry) {
throw new Error('Invalid preset file: manifest.json not found');
}
const manifest = JSON5.parse(entry.getData().toString('utf-8')) as ManifestFile;
return manifestToPresetFile(manifest);
}
/** /**
* Load preset file * Load preset file
* @param source Preset source (file path, URL, or preset name) * @param source Preset source (preset name or directory path)
*/ */
export async function loadPreset(source: string): Promise<PresetFile> { export async function loadPreset(source: string): Promise<PresetFile> {
// Check if it's a URL
if (source.startsWith('http://') || source.startsWith('https://')) {
const tempFile = await downloadPresetToTemp(source);
const preset = await loadPresetFromZip(tempFile);
// Delete temp file
await fs.unlink(tempFile).catch(() => {});
return preset;
}
// Check if it's absolute or relative path (contains / or \) // Check if it's absolute or relative path (contains / or \)
if (source.includes('/') || source.includes('\\')) { if (source.includes('/') || source.includes('\\')) {
// File path // Directory path - read manifest from directory
return await loadPresetFromZip(source); const manifestPath = path.join(source, 'manifest.json');
const content = await fs.readFile(manifestPath, 'utf-8');
const manifest = JSON5.parse(content) as ManifestFile;
return manifestToPresetFile(manifest);
} }
// Otherwise treat as preset name (read from extracted directory) // Otherwise treat as preset name (read from presets directory)
const presetDir = getPresetDir(source); const presetDir = getPresetDir(source);
const manifest = await readManifestFromDir(presetDir); const manifest = await readManifestFromDir(presetDir);
return manifestToPresetFile(manifest); return manifestToPresetFile(manifest);
@@ -370,33 +349,6 @@ export async function saveManifest(presetName: string, manifest: ManifestFile):
await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2), 'utf-8'); await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2), 'utf-8');
} }
/**
* Find preset file
* @param source Preset source
* @returns File path or null
*/
export async function findPresetFile(source: string): Promise<string | null> {
// Current directory file
const currentDirFile = path.join(process.cwd(), `${source}.ccrsets`);
// presets directory file
const presetsDirFile = path.join(HOME_DIR, 'presets', `${source}.ccrsets`);
// Check current directory
try {
await fs.access(currentDirFile);
return currentDirFile;
} catch {
// Check presets directory
try {
await fs.access(presetsDirFile);
return presetsDirFile;
} catch {
return null;
}
}
}
/** /**
* Check if preset is already installed * Check if preset is already installed
* @param presetName Preset name * @param presetName Preset name

View File

@@ -274,19 +274,15 @@ export function Presets() {
} }
// 确定预设名称 // 确定预设名称
const presetName = installName || ( const presetName = installName || '';
installMethod === 'file'
? installFile!.name.replace('.ccrsets', '')
: installUrl!.split('/').pop()!.replace('.ccrsets', '')
);
// Step 1: Install preset (extract to directory) // Step 1: Install preset from GitHub repository
let installResult; let installResult;
if (installMethod === 'url' && installUrl) { if (installMethod === 'url' && installUrl) {
installResult = await api.installPresetFromUrl(installUrl, presetName); // Install from GitHub repository
} else if (installMethod === 'file' && installFile) { installResult = await api.installPresetFromGitHub(installUrl, presetName);
installResult = await api.uploadPresetFile(installFile, presetName);
} else { } else {
setToast({ message: t('presets.please_provide_url'), type: 'warning' });
return; return;
} }
@@ -488,15 +484,7 @@ export function Presets() {
<div className="space-y-4 py-4"> <div className="space-y-4 py-4">
<div className="flex gap-2"> <div className="flex gap-2">
<Button <Button
variant={installMethod === 'file' ? 'default' : 'outline'} variant="default"
onClick={() => setInstallMethod('file')}
className="flex-1"
>
<Upload className="mr-2 h-4 w-4" />
{t('presets.upload_file')}
</Button>
<Button
variant={installMethod === 'url' ? 'default' : 'outline'}
onClick={() => setInstallMethod('url')} onClick={() => setInstallMethod('url')}
className="flex-1" className="flex-1"
> >
@@ -505,28 +493,17 @@ export function Presets() {
</Button> </Button>
</div> </div>
{installMethod === 'file' ? ( <div className="space-y-2">
<div className="space-y-2"> <Label htmlFor="preset-url">{t('presets.github_repository')}</Label>
<Label htmlFor="preset-file">{t('presets.preset_file')}</Label> <Input
<Input id="preset-url"
id="preset-file" type="url"
type="file" placeholder={t('presets.preset_url_placeholder')}
accept=".ccrsets" value={installUrl}
onChange={(e) => setInstallFile(e.target.files?.[0] || null)} onChange={(e) => setInstallUrl(e.target.value)}
/> />
</div> <p className="text-xs text-gray-500">{t('presets.github_url_hint')}</p>
) : ( </div>
<div className="space-y-2">
<Label htmlFor="preset-url">{t('presets.preset_url')}</Label>
<Input
id="preset-url"
type="url"
placeholder={t('presets.preset_url_placeholder')}
value={installUrl}
onChange={(e) => setInstallUrl(e.target.value)}
/>
</div>
)}
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="preset-name">{t('presets.preset_name')}</Label> <Label htmlFor="preset-name">{t('presets.preset_name')}</Label>

View File

@@ -260,15 +260,14 @@
"close": "Close", "close": "Close",
"delete": "Delete", "delete": "Delete",
"install_dialog_title": "Install Preset", "install_dialog_title": "Install Preset",
"install_dialog_description": "Install a preset from a file or URL", "install_dialog_description": "Install a preset from a GitHub repository",
"upload_file": "Upload File", "from_url": "From GitHub",
"from_url": "From URL", "github_repository": "GitHub Repository",
"preset_file": "Preset File (.ccrsets)", "preset_url": "Repository URL",
"preset_url": "Preset URL", "preset_url_placeholder": "https://github.com/owner/repo",
"preset_url_placeholder": "https://example.com/preset.ccrsets",
"preset_name": "Preset Name (Optional)", "preset_name": "Preset Name (Optional)",
"preset_name_placeholder": "Auto-generated from file", "preset_name_placeholder": "Auto-generated from repository",
"please_provide_file_or_url": "Please provide a file or URL", "github_url_hint": "Enter GitHub repository URL (e.g., https://github.com/owner/repo)",
"detail_dialog_title": "Preset Details", "detail_dialog_title": "Preset Details",
"required_information": "Required Information", "required_information": "Required Information",
"delete_dialog_title": "Delete Preset", "delete_dialog_title": "Delete Preset",
@@ -284,8 +283,8 @@
"please_fill_field": "Please fill in {{field}}", "please_fill_field": "Please fill in {{field}}",
"load_market_failed": "Failed to load market presets", "load_market_failed": "Failed to load market presets",
"preset_installed_config_required": "Preset installed, please complete configuration", "preset_installed_config_required": "Preset installed, please complete configuration",
"please_provide_file": "Please provide a preset file", "please_provide_file": "Please provide a preset directory",
"please_provide_url": "Please provide a preset URL", "please_provide_url": "Please provide a valid GitHub repository URL",
"form": { "form": {
"field_required": "{{field}} is required", "field_required": "{{field}} is required",
"must_be_number": "{{field}} must be a number", "must_be_number": "{{field}} must be a number",

View File

@@ -260,15 +260,14 @@
"close": "关闭", "close": "关闭",
"delete": "删除", "delete": "删除",
"install_dialog_title": "安装预设", "install_dialog_title": "安装预设",
"install_dialog_description": "从文件或URL安装预设", "install_dialog_description": "从 GitHub 仓库安装预设",
"upload_file": "上传文件", "from_url": "从 GitHub",
"from_url": "从 URL", "github_repository": "GitHub 仓库",
"preset_file": "预设文件 (.ccrsets)", "preset_url": "仓库 URL",
"preset_url": "预设 URL", "preset_url_placeholder": "https://github.com/owner/repo",
"preset_url_placeholder": "https://example.com/preset.ccrsets",
"preset_name": "预设名称 (可选)", "preset_name": "预设名称 (可选)",
"preset_name_placeholder": "从文件自动生成", "preset_name_placeholder": "从仓库自动生成",
"please_provide_file_or_url": "请提供文件或 URL", "github_url_hint": "输入 GitHub 仓库 URL例如https://github.com/owner/repo",
"detail_dialog_title": "预设详情", "detail_dialog_title": "预设详情",
"required_information": "必需信息", "required_information": "必需信息",
"delete_dialog_title": "删除预设", "delete_dialog_title": "删除预设",
@@ -284,8 +283,8 @@
"please_fill_field": "请填写 {{field}}", "please_fill_field": "请填写 {{field}}",
"load_market_failed": "加载市场预设失败", "load_market_failed": "加载市场预设失败",
"preset_installed_config_required": "预设已安装,请完成配置", "preset_installed_config_required": "预设已安装,请完成配置",
"please_provide_file": "请提供预设文件", "please_provide_file": "请提供预设目录",
"please_provide_url": "请提供预设 URL", "please_provide_url": "请提供有效的 GitHub 仓库 URL",
"form": { "form": {
"field_required": "{{field}} 为必填项", "field_required": "{{field}} 为必填项",
"must_be_number": "{{field}} 必须是数字", "must_be_number": "{{field}} 必须是数字",