finished presets

This commit is contained in:
musistudio
2025-12-30 16:49:16 +08:00
parent 06a18c0734
commit 559f5024c4
14 changed files with 869 additions and 588 deletions

View File

@@ -1,17 +1,16 @@
/** /**
* 预设命令处理器 CLI 层 * Preset command handler CLI layer
* 负责处理 CLI 交互,核心逻辑在 shared 包中 * Handles CLI interactions, core logic is in the shared package
*/ */
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 JSON5 from 'json5'; import JSON5 from 'json5';
import { exportPresetCli } from './export'; import { exportPresetCli } from './export';
import { installPresetCli, loadPreset } from './install'; import { installPresetCli, loadPreset } from './install';
import { MergeStrategy, HOME_DIR } from '@CCR/shared'; import { HOME_DIR } from '@CCR/shared';
// ANSI 颜色代码 // ANSI color codes
const RESET = "\x1B[0m"; const RESET = "\x1B[0m";
const GREEN = "\x1B[32m"; const GREEN = "\x1B[32m";
const YELLOW = "\x1B[33m"; const YELLOW = "\x1B[33m";
@@ -20,7 +19,7 @@ const BOLDYELLOW = "\x1B[1m\x1B[33m";
const DIM = "\x1B[2m"; const DIM = "\x1B[2m";
/** /**
* 列出本地预设 * List local presets
*/ */
async function listPresets(): Promise<void> { async function listPresets(): Promise<void> {
const presetsDir = path.join(HOME_DIR, 'presets'); const presetsDir = path.join(HOME_DIR, 'presets');
@@ -51,7 +50,7 @@ async function listPresets(): Promise<void> {
const content = await fs.readFile(manifestPath, 'utf-8'); const content = await fs.readFile(manifestPath, 'utf-8');
const manifest = JSON5.parse(content); const manifest = JSON5.parse(content);
// 从manifest中提取metadata字段 // Extract metadata fields from manifest
const { Providers, Router, PORT, HOST, API_TIMEOUT_MS, PROXY_URL, LOG, LOG_LEVEL, StatusLine, NON_INTERACTIVE_MODE, requiredInputs, ...metadata } = manifest; const { Providers, Router, PORT, HOST, API_TIMEOUT_MS, PROXY_URL, LOG, LOG_LEVEL, StatusLine, NON_INTERACTIVE_MODE, requiredInputs, ...metadata } = manifest;
const name = metadata.name || dirName; const name = metadata.name || dirName;
@@ -59,19 +58,19 @@ async function listPresets(): Promise<void> {
const author = metadata.author || ''; const author = metadata.author || '';
const version = metadata.version; const version = metadata.version;
// 显示预设名称 // Display preset name
if (version) { if (version) {
console.log(`${GREEN}${RESET} ${BOLDCYAN}${name}${RESET} (v${version})`); console.log(`${GREEN}${RESET} ${BOLDCYAN}${name}${RESET} (v${version})`);
} else { } else {
console.log(`${GREEN}${RESET} ${BOLDCYAN}${name}${RESET}`); console.log(`${GREEN}${RESET} ${BOLDCYAN}${name}${RESET}`);
} }
// 显示描述 // Display description
if (description) { if (description) {
console.log(` ${description}`); console.log(` ${description}`);
} }
// 显示作者 // Display author
if (author) { if (author) {
console.log(` ${DIM}by ${author}${RESET}`); console.log(` ${DIM}by ${author}${RESET}`);
} }
@@ -85,14 +84,21 @@ async function listPresets(): Promise<void> {
} }
/** /**
* 删除预设 * Delete preset
*/ */
async function deletePreset(name: string): Promise<void> { async function deletePreset(name: string): Promise<void> {
const presetsDir = path.join(HOME_DIR, 'presets'); const presetsDir = path.join(HOME_DIR, 'presets');
// Validate preset name (prevent path traversal)
if (!name || name.includes('..') || name.includes('/') || name.includes('\\')) {
console.error(`\n${YELLOW}Error:${RESET} Invalid preset name.\n`);
process.exit(1);
}
const presetDir = path.join(presetsDir, name); const presetDir = path.join(presetsDir, name);
try { try {
// 递归删除整个目录 // Recursively delete entire directory
await fs.rm(presetDir, { recursive: true, force: true }); await fs.rm(presetDir, { recursive: true, force: true });
console.log(`\n${GREEN}${RESET} Preset "${name}" deleted.\n`); console.log(`\n${GREEN}${RESET} Preset "${name}" deleted.\n`);
} catch (error: any) { } catch (error: any) {
@@ -106,27 +112,27 @@ async function deletePreset(name: string): Promise<void> {
} }
/** /**
* 显示预设信息 * Show preset information
*/ */
async function showPresetInfo(name: string): Promise<void> { async function showPresetInfo(name: string): Promise<void> {
try { try {
const preset = await loadPreset(name); const preset = await loadPreset(name);
const config = preset.config; const config = preset.config;
const metadata = preset.metadata || {}; const metadata = preset.metadata;
console.log(`\n${BOLDCYAN}═══════════════════════════════════════════════${RESET}`); console.log(`\n${BOLDCYAN}═══════════════════════════════════════════════${RESET}`);
if (metadata.name) { if (metadata?.name) {
console.log(`${BOLDCYAN}Preset: ${RESET}${metadata.name}`); console.log(`${BOLDCYAN}Preset: ${RESET}${metadata.name}`);
} else { } else {
console.log(`${BOLDCYAN}Preset: ${RESET}${name}`); console.log(`${BOLDCYAN}Preset: ${RESET}${name}`);
} }
console.log(`${BOLDCYAN}═══════════════════════════════════════════════${RESET}\n`); console.log(`${BOLDCYAN}═══════════════════════════════════════════════${RESET}\n`);
if (metadata.version) console.log(`${BOLDCYAN}Version:${RESET} ${metadata.version}`); if (metadata?.version) console.log(`${BOLDCYAN}Version:${RESET} ${metadata.version}`);
if (metadata.description) console.log(`${BOLDCYAN}Description:${RESET} ${metadata.description}`); if (metadata?.description) console.log(`${BOLDCYAN}Description:${RESET} ${metadata.description}`);
if (metadata.author) console.log(`${BOLDCYAN}Author:${RESET} ${metadata.author}`); if (metadata?.author) console.log(`${BOLDCYAN}Author:${RESET} ${metadata.author}`);
const keywords = (metadata as any).keywords; const keywords = metadata?.keywords;
if (keywords && keywords.length > 0) { if (keywords && keywords.length > 0) {
console.log(`${BOLDCYAN}Keywords:${RESET} ${keywords.join(', ')}`); console.log(`${BOLDCYAN}Keywords:${RESET} ${keywords.join(', ')}`);
} }
@@ -142,11 +148,12 @@ async function showPresetInfo(name: string): Promise<void> {
console.log(` Provider: ${config.provider}`); console.log(` Provider: ${config.provider}`);
} }
if (preset.requiredInputs && preset.requiredInputs.length > 0) { if (preset.schema && preset.schema.length > 0) {
console.log(`\n${BOLDYELLOW}Required inputs:${RESET}`); console.log(`\n${BOLDYELLOW}Required inputs:${RESET}`);
for (const input of preset.requiredInputs) { for (const input of preset.schema) {
const envVar = input.placeholder || input.field; const label = input.label || input.id;
console.log(` - ${input.field} ${DIM}(${envVar})${RESET}`); const prompt = input.prompt || '';
console.log(` - ${label}${prompt ? ` ${DIM}(${prompt})${RESET}` : ''}`);
} }
} }
@@ -158,7 +165,7 @@ async function showPresetInfo(name: string): Promise<void> {
} }
/** /**
* 处理预设命令 * Handle preset commands
*/ */
export async function handlePresetCommand(args: string[]): Promise<void> { export async function handlePresetCommand(args: string[]): Promise<void> {
const subCommand = args[0]; const subCommand = args[0];
@@ -172,7 +179,7 @@ export async function handlePresetCommand(args: string[]): Promise<void> {
process.exit(1); process.exit(1);
} }
// 解析选项 // Parse options
const options: any = {}; const options: any = {};
for (let i = 2; i < args.length; i++) { for (let i = 2; i < args.length; i++) {
if (args[i] === '--output' && args[i + 1]) { if (args[i] === '--output' && args[i + 1]) {
@@ -199,22 +206,7 @@ export async function handlePresetCommand(args: string[]): Promise<void> {
process.exit(1); process.exit(1);
} }
// 解析选项 await installPresetCli(source, {});
const installOptions: any = {};
for (let i = 2; i < args.length; i++) {
if (args[i] === '--strategy' && args[i + 1]) {
const strategy = args[++i];
if (['ask', 'overwrite', 'merge', 'skip'].includes(strategy)) {
installOptions.strategy = strategy as MergeStrategy;
} else {
console.error(`\nError: Invalid merge strategy "${strategy}"\n`);
console.error('Valid strategies: ask, overwrite, merge, skip\n');
process.exit(1);
}
}
}
await installPresetCli(source, installOptions);
break; break;
case 'list': case 'list':

View File

@@ -1,11 +1,10 @@
/** /**
* 预设安装功能 CLI 层 * Preset installation functionality CLI layer
* 负责处理 CLI 交互,核心逻辑在 shared 包中 * Handles CLI interactions, core logic is in the shared package
*/ */
import * as fs from 'fs/promises'; import * as fs from 'fs/promises';
import * as path from 'path'; import * as path from 'path';
import { password, confirm } from '@inquirer/prompts';
import { import {
loadPreset as loadPresetShared, loadPreset as loadPresetShared,
validatePreset, validatePreset,
@@ -19,18 +18,14 @@ import {
isPresetInstalled, isPresetInstalled,
ManifestFile, ManifestFile,
PresetFile, PresetFile,
RequiredInput,
UserInputValues, UserInputValues,
applyConfigMappings,
replaceTemplateVariables,
setValueByPath,
} from '@CCR/shared'; } from '@CCR/shared';
import { collectUserInputs } from '../prompt/schema-input'; import { collectUserInputs } from '../prompt/schema-input';
// 重新导出 loadPreset // Re-export loadPreset
export { loadPresetShared as loadPreset }; export { loadPresetShared as loadPreset };
// ANSI 颜色代码 // ANSI color codes
const RESET = "\x1B[0m"; const RESET = "\x1B[0m";
const GREEN = "\x1B[32m"; const GREEN = "\x1B[32m";
const BOLDGREEN = "\x1B[1m\x1B[32m"; const BOLDGREEN = "\x1B[1m\x1B[32m";
@@ -40,44 +35,9 @@ const BOLDCYAN = "\x1B[1m\x1B[36m";
const DIM = "\x1B[2m"; const DIM = "\x1B[2m";
/** /**
* 应用用户输入到配置新版schema * Apply preset to configuration
*/ * @param presetName Preset name
function applyUserInputs( * @param preset Preset object
preset: PresetFile,
values: UserInputValues
): PresetConfigSection {
let config = { ...preset.config };
// 1. 先应用 template如果存在
if (preset.template) {
config = replaceTemplateVariables(preset.template, values) as any;
}
// 2. 再应用 configMappings如果存在
if (preset.configMappings && preset.configMappings.length > 0) {
config = applyConfigMappings(preset.configMappings, values, config);
}
// 3. 兼容旧版:直接将 values 应用到 config
// 检查是否有任何值没有通过 mappings 应用
for (const [key, value] of Object.entries(values)) {
// 如果这个值已经在 template 或 mappings 中处理过,跳过
// 这里简化处理:直接应用所有值
// 在实际使用中template 和 mappings 应该覆盖所有需要设置的字段
// 尝试智能判断:如果 key 包含 '.' 或 '[',说明是路径
if (key.includes('.') || key.includes('[')) {
setValueByPath(config, key, value);
}
}
return config;
}
/**
* 应用预设到配置
* @param presetName 预设名称
* @param preset 预设对象
*/ */
export async function applyPresetCli( export async function applyPresetCli(
presetName: string, presetName: string,
@@ -86,7 +46,7 @@ export async function applyPresetCli(
try { try {
console.log(`${BOLDCYAN}Loading preset...${RESET} ${GREEN}${RESET}`); console.log(`${BOLDCYAN}Loading preset...${RESET} ${GREEN}${RESET}`);
// 验证预设 // Validate preset
const validation = await validatePreset(preset); const validation = await validatePreset(preset);
if (validation.warnings.length > 0) { if (validation.warnings.length > 0) {
console.log(`\n${YELLOW}Warnings:${RESET}`); console.log(`\n${YELLOW}Warnings:${RESET}`);
@@ -105,36 +65,35 @@ export async function applyPresetCli(
console.log(`${BOLDCYAN}Validating preset...${RESET} ${GREEN}${RESET}`); console.log(`${BOLDCYAN}Validating preset...${RESET} ${GREEN}${RESET}`);
// 检查是否需要配置 // Check if configuration is required
if (preset.schema && preset.schema.length > 0) { if (preset.schema && preset.schema.length > 0) {
console.log(`\n${BOLDCYAN}Configuration required:${RESET} ${preset.schema.length} field(s)\n`); console.log(`\n${BOLDCYAN}Configuration required:${RESET} ${preset.schema.length} field(s)\n`);
} else { } else {
console.log(`\n${DIM}No configuration required for this preset${RESET}\n`); console.log(`\n${DIM}No configuration required for this preset${RESET}\n`);
} }
// 收集用户输入 // Collect user inputs
let userInputs: UserInputValues = {}; let userInputs: UserInputValues = {};
// 使用 schema 系统 // Use schema system
if (preset.schema && preset.schema.length > 0) { if (preset.schema && preset.schema.length > 0) {
userInputs = await collectUserInputs(preset.schema, preset.config); userInputs = await collectUserInputs(preset.schema, preset.config);
} }
// 应用用户输入到配置 // Build manifest, keep original config, store user values in userValues
const finalConfig = applyUserInputs(preset, userInputs);
// 读取现有的manifest并更新
const manifest: ManifestFile = { const manifest: ManifestFile = {
name: presetName,
version: preset.metadata?.version || '1.0.0',
...(preset.metadata || {}), ...(preset.metadata || {}),
...finalConfig, ...preset.config, // Keep original config (may contain placeholders)
}; };
// 保存 schema(如果存在) // Save schema (if exists)
if (preset.schema) { if (preset.schema) {
manifest.schema = preset.schema; manifest.schema = preset.schema;
} }
// 保存其他配置 // Save other configurations
if (preset.template) { if (preset.template) {
manifest.template = preset.template; manifest.template = preset.template;
} }
@@ -142,10 +101,17 @@ export async function applyPresetCli(
manifest.configMappings = preset.configMappings; manifest.configMappings = preset.configMappings;
} }
// 保存到解压目录的manifest.json // Save user-filled values to userValues
if (Object.keys(userInputs).length > 0) {
manifest.userValues = userInputs;
}
// Save to manifest.json in extracted directory
await saveManifest(presetName, manifest); await saveManifest(presetName, manifest);
// 显示摘要 const presetDir = getPresetDir(presetName);
// Display summary
console.log(`\n${BOLDGREEN}✓ Preset configured successfully!${RESET}\n`); console.log(`\n${BOLDGREEN}✓ Preset configured successfully!${RESET}\n`);
console.log(`${BOLDCYAN}Preset directory:${RESET} ${presetDir}`); console.log(`${BOLDCYAN}Preset directory:${RESET} ${presetDir}`);
console.log(`${BOLDCYAN}Inputs configured:${RESET} ${Object.keys(userInputs).length}`); console.log(`${BOLDCYAN}Inputs configured:${RESET} ${Object.keys(userInputs).length}`);
@@ -173,7 +139,7 @@ export async function applyPresetCli(
} }
/** /**
* 安装预设(主入口) * Install preset (main entry point)
*/ */
export async function installPresetCli( export async function installPresetCli(
source: string, source: string,
@@ -182,37 +148,32 @@ export async function installPresetCli(
name?: string; name?: string;
} = {} } = {}
): Promise<void> { ): Promise<void> {
let tempFile: string | null = null;
try { try {
// 确定预设名称 // Determine preset name
let presetName = options.name; let presetName = options.name;
let sourceZip: string; let sourceZip: string | undefined;
let isReconfigure = false; // 是否是重新配置已安装的preset let isReconfigure = false; // Whether to reconfigure installed preset
// 判断source类型并获取ZIP文件路径 // Determine source type and get ZIP file path
if (source.startsWith('http://') || source.startsWith('https://')) { if (source.startsWith('http://') || source.startsWith('https://')) {
// URL:下载到临时文件 // URL: download to temp file
if (!presetName) { if (!presetName) {
const urlParts = source.split('/'); const urlParts = source.split('/');
const filename = urlParts[urlParts.length - 1]; const filename = urlParts[urlParts.length - 1];
presetName = filename.replace('.ccrsets', ''); presetName = filename.replace('.ccrsets', '');
} }
// 这里直接从 shared 包导入的 downloadPresetToTemp 会返回临时文件 // downloadPresetToTemp imported from shared package will return temp file
// 但我们会在 loadPreset 中自动清理,所以不需要在这里处理 // but we'll auto-cleanup in loadPreset, so no need to handle here
const preset = await loadPreset(source); // Re-download to temp file for extractPreset usage
if (!presetName) { // Since loadPreset already downloaded and deleted, special handling needed here
presetName = preset.metadata?.name || 'preset';
}
// 重新下载到临时文件以供 extractPreset 使用
// 由于 loadPreset 已经下载并删除了,这里需要特殊处理
throw new Error('URL installation not fully implemented yet'); throw new Error('URL installation not fully implemented yet');
} else if (source.includes('/') || source.includes('\\')) { } else if (source.includes('/') || source.includes('\\')) {
// 文件路径 // File path
if (!presetName) { if (!presetName) {
const filename = path.basename(source); const filename = path.basename(source);
presetName = filename.replace('.ccrsets', ''); presetName = filename.replace('.ccrsets', '');
} }
// 验证文件存在 // Verify file exists
try { try {
await fs.access(source); await fs.access(source);
} catch { } catch {
@@ -220,48 +181,51 @@ export async function installPresetCli(
} }
sourceZip = source; sourceZip = source;
} else { } else {
// 预设名称(不带路径) // Preset name (without path)
presetName = source; presetName = source;
// 按优先级查找文件:当前目录 -> presets目录 // Search files by priority: current directory -> presets directory
const presetFile = await findPresetFile(source); const presetFile = await findPresetFile(source);
if (presetFile) { if (presetFile) {
sourceZip = presetFile; sourceZip = presetFile;
} else { } else {
// 检查是否已安装(目录存在) // Check if already installed (directory exists)
if (await isPresetInstalled(source)) { if (await isPresetInstalled(source)) {
// 已安装,重新配置 // Already installed, reconfigure
isReconfigure = true; isReconfigure = true;
} else { } else {
// 都不存在,报错 // Neither exists, error
throw new Error(`Preset '${source}' not found in current directory or presets directory.`); throw new Error(`Preset '${source}' not found in current directory or presets directory.`);
} }
} }
} }
if (isReconfigure) { if (isReconfigure) {
// 重新配置已安装的preset // Reconfigure installed preset
console.log(`${BOLDCYAN}Reconfiguring preset:${RESET} ${presetName}\n`); console.log(`${BOLDCYAN}Reconfiguring preset:${RESET} ${presetName}\n`);
const presetDir = getPresetDir(presetName); const presetDir = getPresetDir(presetName);
const manifest = await readManifestFromDir(presetDir); const manifest = await readManifestFromDir(presetDir);
const preset = manifestToPresetFile(manifest); const preset = manifestToPresetFile(manifest);
// 应用preset会询问敏感信息 // Apply preset (will ask for sensitive info)
await applyPresetCli(presetName, preset); await applyPresetCli(presetName, preset);
} else { } else {
// 新安装:解压到目标目录 // New installation: extract to target directory
if (!sourceZip) {
throw new Error('Source ZIP file is required for installation');
}
const targetDir = getPresetDir(presetName); const targetDir = getPresetDir(presetName);
console.log(`${BOLDCYAN}Extracting preset to:${RESET} ${targetDir}`); console.log(`${BOLDCYAN}Extracting preset to:${RESET} ${targetDir}`);
await extractPreset(sourceZip, targetDir); await extractPreset(sourceZip, targetDir);
console.log(`${GREEN}${RESET} Extracted successfully\n`); console.log(`${GREEN}${RESET} Extracted successfully\n`);
// 从解压目录读取manifest // Read manifest from extracted directory
const manifest = await readManifestFromDir(targetDir); const manifest = await readManifestFromDir(targetDir);
const preset = manifestToPresetFile(manifest); const preset = manifestToPresetFile(manifest);
// 应用preset询问用户信息等 // Apply preset (ask user info, etc.)
await applyPresetCli(presetName, preset); await applyPresetCli(presetName, preset);
} }

View File

@@ -8,10 +8,28 @@ import { RegisterProviderRequest, LLMProvider } from "@/types/llm";
import { sendUnifiedRequest } from "@/utils/request"; import { sendUnifiedRequest } from "@/utils/request";
import { createApiError } from "./middleware"; import { createApiError } from "./middleware";
import { version } from "../../package.json"; import { version } from "../../package.json";
import { ConfigService } from "@/services/config";
import { ProviderService } from "@/services/provider";
import { TransformerService } from "@/services/transformer";
import { Transformer } from "@/types/transformer";
// Extend FastifyInstance to include custom services
declare module "fastify" {
interface FastifyInstance {
configService: ConfigService;
providerService: ProviderService;
transformerService: TransformerService;
}
interface FastifyRequest {
provider?: string;
}
}
/** /**
* 处理transformer端点的主函数 * Main handler for transformer endpoints
* 协调整个请求处理流程:验证提供者、处理请求转换器、发送请求、处理响应转换器、格式化响应 * Coordinates the entire request processing flow: validate provider, handle request transformers,
* send request, handle response transformers, format response
*/ */
async function handleTransformerEndpoint( async function handleTransformerEndpoint(
req: FastifyRequest, req: FastifyRequest,
@@ -21,9 +39,9 @@ async function handleTransformerEndpoint(
) { ) {
const body = req.body as any; const body = req.body as any;
const providerName = req.provider!; const providerName = req.provider!;
const provider = fastify._server!.providerService.getProvider(providerName); const provider = fastify.providerService.getProvider(providerName);
// 验证提供者是否存在 // Validate provider exists
if (!provider) { if (!provider) {
throw createApiError( throw createApiError(
`Provider '${providerName}' not found`, `Provider '${providerName}' not found`,
@@ -32,7 +50,7 @@ async function handleTransformerEndpoint(
); );
} }
// 处理请求转换器链 // Process request transformer chain
const { requestBody, config, bypass } = await processRequestTransformers( const { requestBody, config, bypass } = await processRequestTransformers(
body, body,
provider, provider,
@@ -43,7 +61,7 @@ async function handleTransformerEndpoint(
} }
); );
// 发送请求到LLM提供者 // Send request to LLM provider
const response = await sendRequestToProvider( const response = await sendRequestToProvider(
requestBody, requestBody,
config, config,
@@ -56,7 +74,7 @@ async function handleTransformerEndpoint(
} }
); );
// 处理响应转换器链 // Process response transformer chain
const finalResponse = await processResponseTransformers( const finalResponse = await processResponseTransformers(
requestBody, requestBody,
response, response,
@@ -68,14 +86,14 @@ async function handleTransformerEndpoint(
} }
); );
// 格式化并返回响应 // Format and return response
return formatResponse(finalResponse, reply, body); return formatResponse(finalResponse, reply, body);
} }
/** /**
* 处理请求转换器链 * Process request transformer chain
* 依次执行transformRequestOutprovider transformersmodel-specific transformers * Sequentially execute transformRequestOut, provider transformers, model-specific transformers
* 返回处理后的请求体、配置和是否跳过转换器的标志 * Returns processed request body, config, and flag indicating whether to skip transformers
*/ */
async function processRequestTransformers( async function processRequestTransformers(
body: any, body: any,
@@ -88,7 +106,7 @@ async function processRequestTransformers(
let config: any = {}; let config: any = {};
let bypass = false; let bypass = false;
// 检查是否应该跳过转换器(透传参数) // Check if transformers should be bypassed (passthrough mode)
bypass = shouldBypassTransformers(provider, transformer, body); bypass = shouldBypassTransformers(provider, transformer, body);
if (bypass) { if (bypass) {
@@ -100,7 +118,7 @@ async function processRequestTransformers(
config.headers = headers; config.headers = headers;
} }
// 执行transformertransformRequestOut方法 // Execute transformer's transformRequestOut method
if (!bypass && typeof transformer.transformRequestOut === "function") { if (!bypass && typeof transformer.transformRequestOut === "function") {
const transformOut = await transformer.transformRequestOut(requestBody); const transformOut = await transformer.transformRequestOut(requestBody);
if (transformOut.body) { if (transformOut.body) {
@@ -111,7 +129,7 @@ async function processRequestTransformers(
} }
} }
// 执行provider级别的转换器 // Execute provider-level transformers
if (!bypass && provider.transformer?.use?.length) { if (!bypass && provider.transformer?.use?.length) {
for (const providerTransformer of provider.transformer.use) { for (const providerTransformer of provider.transformer.use) {
if ( if (
@@ -134,7 +152,7 @@ async function processRequestTransformers(
} }
} }
// 执行模型特定的转换器 // Execute model-specific transformers
if (!bypass && provider.transformer?.[body.model]?.use?.length) { if (!bypass && provider.transformer?.[body.model]?.use?.length) {
for (const modelTransformer of provider.transformer[body.model].use) { for (const modelTransformer of provider.transformer[body.model].use) {
if ( if (
@@ -155,8 +173,8 @@ async function processRequestTransformers(
} }
/** /**
* 判断是否应该跳过转换器(透传参数) * Determine if transformers should be bypassed (passthrough mode)
* 当provider只使用一个transformer且该transformer与当前transformer相同时跳过其他转换器 * Skip other transformers when provider only uses one transformer and it matches the current one
*/ */
function shouldBypassTransformers( function shouldBypassTransformers(
provider: any, provider: any,
@@ -173,8 +191,8 @@ function shouldBypassTransformers(
} }
/** /**
* 发送请求到LLM提供者 * Send request to LLM provider
* 处理认证、构建请求配置、发送请求并处理错误 * Handle authentication, build request config, send request and handle errors
*/ */
async function sendRequestToProvider( async function sendRequestToProvider(
requestBody: any, requestBody: any,
@@ -187,7 +205,7 @@ async function sendRequestToProvider(
) { ) {
const url = config.url || new URL(provider.baseUrl); const url = config.url || new URL(provider.baseUrl);
// 在透传参数下处理认证 // Handle authentication in passthrough mode
if (bypass && typeof transformer.auth === "function") { if (bypass && typeof transformer.auth === "function") {
const auth = await transformer.auth(requestBody, provider); const auth = await transformer.auth(requestBody, provider);
if (auth.body) { if (auth.body) {
@@ -211,8 +229,8 @@ async function sendRequestToProvider(
} }
} }
// 发送HTTP请求 // Send HTTP request
// 准备headers // Prepare headers
const requestHeaders: Record<string, string> = { const requestHeaders: Record<string, string> = {
Authorization: `Bearer ${provider.apiKey}`, Authorization: `Bearer ${provider.apiKey}`,
...(config?.headers || {}), ...(config?.headers || {}),
@@ -233,7 +251,7 @@ async function sendRequestToProvider(
url, url,
requestBody, requestBody,
{ {
httpsProxy: fastify._server!.configService.getHttpsProxy(), httpsProxy: fastify.configService.getHttpsProxy(),
...config, ...config,
headers: JSON.parse(JSON.stringify(requestHeaders)), headers: JSON.parse(JSON.stringify(requestHeaders)),
}, },
@@ -241,7 +259,7 @@ async function sendRequestToProvider(
fastify.log fastify.log
); );
// 处理请求错误 // Handle request errors
if (!response.ok) { if (!response.ok) {
const errorText = await response.text(); const errorText = await response.text();
fastify.log.error( fastify.log.error(
@@ -258,8 +276,8 @@ async function sendRequestToProvider(
} }
/** /**
* 处理响应转换器链 * Process response transformer chain
* 依次执行provider transformersmodel-specific transformerstransformertransformResponseIn * Sequentially execute provider transformers, model-specific transformers, transformer's transformResponseIn
*/ */
async function processResponseTransformers( async function processResponseTransformers(
requestBody: any, requestBody: any,
@@ -271,43 +289,43 @@ async function processResponseTransformers(
) { ) {
let finalResponse = response; let finalResponse = response;
// 执行provider级别的响应转换器 // Execute provider-level response transformers
if (!bypass && provider.transformer?.use?.length) { if (!bypass && provider.transformer?.use?.length) {
for (const providerTransformer of Array.from( for (const providerTransformer of Array.from(
provider.transformer.use provider.transformer.use
).reverse()) { ).reverse() as Transformer[]) {
if ( if (
!providerTransformer || !providerTransformer ||
typeof providerTransformer.transformResponseOut !== "function" typeof providerTransformer.transformResponseOut !== "function"
) { ) {
continue; continue;
} }
finalResponse = await providerTransformer.transformResponseOut( finalResponse = await providerTransformer.transformResponseOut!(
finalResponse, finalResponse,
context context
); );
} }
} }
// 执行模型特定的响应转换器 // Execute model-specific response transformers
if (!bypass && provider.transformer?.[requestBody.model]?.use?.length) { if (!bypass && provider.transformer?.[requestBody.model]?.use?.length) {
for (const modelTransformer of Array.from( for (const modelTransformer of Array.from(
provider.transformer[requestBody.model].use provider.transformer[requestBody.model].use
).reverse()) { ).reverse() as Transformer[]) {
if ( if (
!modelTransformer || !modelTransformer ||
typeof modelTransformer.transformResponseOut !== "function" typeof modelTransformer.transformResponseOut !== "function"
) { ) {
continue; continue;
} }
finalResponse = await modelTransformer.transformResponseOut( finalResponse = await modelTransformer.transformResponseOut!(
finalResponse, finalResponse,
context context
); );
} }
} }
// 执行transformertransformResponseIn方法 // Execute transformer's transformResponseIn method
if (!bypass && transformer.transformResponseIn) { if (!bypass && transformer.transformResponseIn) {
finalResponse = await transformer.transformResponseIn( finalResponse = await transformer.transformResponseIn(
finalResponse, finalResponse,
@@ -319,16 +337,16 @@ async function processResponseTransformers(
} }
/** /**
* 格式化并返回响应 * Format and return response
* 处理HTTP状态码、流式响应和普通响应的格式化 * Handle HTTP status codes, format streaming and regular responses
*/ */
function formatResponse(response: any, reply: FastifyReply, body: any) { function formatResponse(response: any, reply: FastifyReply, body: any) {
// 设置HTTP状态码 // Set HTTP status code
if (!response.ok) { if (!response.ok) {
reply.code(response.status); reply.code(response.status);
} }
// 处理流式响应 // Handle streaming response
const isStream = body.stream === true; const isStream = body.stream === true;
if (isStream) { if (isStream) {
reply.header("Content-Type", "text/event-stream"); reply.header("Content-Type", "text/event-stream");
@@ -336,12 +354,12 @@ function formatResponse(response: any, reply: FastifyReply, body: any) {
reply.header("Connection", "keep-alive"); reply.header("Connection", "keep-alive");
return reply.send(response.body); return reply.send(response.body);
} else { } else {
// 处理普通JSON响应 // Handle regular JSON response
return response.json(); return response.json();
} }
} }
export const registerApiRoutes: FastifyPluginAsync = async ( export const registerApiRoutes = async (
fastify: FastifyInstance fastify: FastifyInstance
) => { ) => {
// Health and info endpoints // Health and info endpoints
@@ -354,7 +372,7 @@ export const registerApiRoutes: FastifyPluginAsync = async (
}); });
const transformersWithEndpoint = const transformersWithEndpoint =
fastify._server!.transformerService.getTransformersWithEndpoint(); fastify.transformerService.getTransformersWithEndpoint();
for (const { transformer } of transformersWithEndpoint) { for (const { transformer } of transformersWithEndpoint) {
if (transformer.endPoint) { if (transformer.endPoint) {
@@ -421,7 +439,7 @@ export const registerApiRoutes: FastifyPluginAsync = async (
} }
// Check if provider already exists // Check if provider already exists
if (fastify._server!.providerService.getProvider(request.body.name)) { if (fastify.providerService.getProvider(request.body.name)) {
throw createApiError( throw createApiError(
`Provider with name '${request.body.name}' already exists`, `Provider with name '${request.body.name}' already exists`,
400, 400,
@@ -429,12 +447,12 @@ export const registerApiRoutes: FastifyPluginAsync = async (
); );
} }
return fastify._server!.providerService.registerProvider(request.body); return fastify.providerService.registerProvider(request.body);
} }
); );
fastify.get("/providers", async () => { fastify.get("/providers", async () => {
return fastify._server!.providerService.getProviders(); return fastify.providerService.getProviders();
}); });
fastify.get( fastify.get(
@@ -449,7 +467,7 @@ export const registerApiRoutes: FastifyPluginAsync = async (
}, },
}, },
async (request: FastifyRequest<{ Params: { id: string } }>) => { async (request: FastifyRequest<{ Params: { id: string } }>) => {
const provider = fastify._server!.providerService.getProvider( const provider = fastify.providerService.getProvider(
request.params.id request.params.id
); );
if (!provider) { if (!provider) {
@@ -488,7 +506,7 @@ export const registerApiRoutes: FastifyPluginAsync = async (
}>, }>,
reply reply
) => { ) => {
const provider = fastify._server!.providerService.updateProvider( const provider = fastify.providerService.updateProvider(
request.params.id, request.params.id,
request.body request.body
); );
@@ -511,7 +529,7 @@ export const registerApiRoutes: FastifyPluginAsync = async (
}, },
}, },
async (request: FastifyRequest<{ Params: { id: string } }>) => { async (request: FastifyRequest<{ Params: { id: string } }>) => {
const success = fastify._server!.providerService.deleteProvider( const success = fastify.providerService.deleteProvider(
request.params.id request.params.id
); );
if (!success) { if (!success) {
@@ -544,7 +562,7 @@ export const registerApiRoutes: FastifyPluginAsync = async (
}>, }>,
reply reply
) => { ) => {
const success = fastify._server!.providerService.toggleProvider( const success = fastify.providerService.toggleProvider(
request.params.id, request.params.id,
request.body.enabled request.body.enabled
); );

View File

@@ -118,9 +118,23 @@ class Server {
this.app.addHook(hookName as any, hookFunction); this.app.addHook(hookName as any, hookFunction);
} }
public async registerNamespace(name: string, options: any) { public async registerNamespace(name: string, options?: any) {
if (!name) throw new Error("name is required"); if (!name) throw new Error("name is required");
const configService = new ConfigService(options); if (name === '/') {
await this.app.register(async (fastify) => {
fastify.decorate('configService', this.configService);
fastify.decorate('transformerService', this.transformerService);
fastify.decorate('providerService', this.providerService);
await registerApiRoutes(fastify);
});
return
}
if (!options) throw new Error("options is required");
const configService = new ConfigService({
initialConfig: {
providers: options.Providers,
}
});
const transformerService = new TransformerService( const transformerService = new TransformerService(
configService, configService,
this.app.log this.app.log
@@ -131,12 +145,17 @@ class Server {
transformerService, transformerService,
this.app.log this.app.log
); );
this.app.register((fastify) => { // await this.app.register((fastify) => {
// fastify.decorate('configService', configService);
// fastify.decorate('transformerService', transformerService);
// fastify.decorate('providerService', providerService);
// }, { prefix: name });
await this.app.register(async (fastify) => {
fastify.decorate('configService', configService); fastify.decorate('configService', configService);
fastify.decorate('transformerService', transformerService); fastify.decorate('transformerService', transformerService);
fastify.decorate('providerService', providerService); fastify.decorate('providerService', providerService);
await registerApiRoutes(fastify);
}, { prefix: name }); }, { prefix: name });
this.app.register(registerApiRoutes, { prefix: name });
} }
async start(): Promise<void> { async start(): Promise<void> {
@@ -179,7 +198,7 @@ class Server {
} }
); );
this.app.register(registerApiRoutes); await this.registerNamespace('/')
const address = await this.app.listen({ const address = await this.app.listen({
port: parseInt(this.configService.get("PORT") || "3000", 10), port: parseInt(this.configService.get("PORT") || "3000", 10),

View File

@@ -15,6 +15,8 @@ import {
downloadPresetToTemp, downloadPresetToTemp,
getTempDir, getTempDir,
HOME_DIR, HOME_DIR,
extractMetadata,
loadConfigFromManifest,
type PresetFile, type PresetFile,
type ManifestFile, type ManifestFile,
type PresetMetadata, type PresetMetadata,
@@ -81,7 +83,7 @@ export const createServer = async (config: any): Promise<any> => {
return reply.redirect("/ui/"); return reply.redirect("/ui/");
}); });
// 获取日志文件列表端点 // Get log file list endpoint
app.get("/api/logs/files", async (req: any, reply: any) => { app.get("/api/logs/files", async (req: any, reply: any) => {
try { try {
const logDir = join(homedir(), ".claude-code-router", "logs"); const logDir = join(homedir(), ".claude-code-router", "logs");
@@ -104,7 +106,7 @@ export const createServer = async (config: any): Promise<any> => {
} }
} }
// 按修改时间倒序排列 // Sort by modification time in descending order
logFiles.sort((a, b) => new Date(b.lastModified).getTime() - new Date(a.lastModified).getTime()); logFiles.sort((a, b) => new Date(b.lastModified).getTime() - new Date(a.lastModified).getTime());
} }
@@ -115,17 +117,17 @@ export const createServer = async (config: any): Promise<any> => {
} }
}); });
// 获取日志内容端点 // Get log content endpoint
app.get("/api/logs", async (req: any, reply: any) => { app.get("/api/logs", async (req: any, reply: any) => {
try { try {
const filePath = (req.query as any).file as string; const filePath = (req.query as any).file as string;
let logFilePath: string; let logFilePath: string;
if (filePath) { if (filePath) {
// 如果指定了文件路径,使用指定的路径 // If file path is specified, use the specified path
logFilePath = filePath; logFilePath = filePath;
} else { } else {
// 如果没有指定文件路径,使用默认的日志文件路径 // If file path is not specified, use default log file path
logFilePath = join(homedir(), ".claude-code-router", "logs", "app.log"); logFilePath = join(homedir(), ".claude-code-router", "logs", "app.log");
} }
@@ -143,17 +145,17 @@ export const createServer = async (config: any): Promise<any> => {
} }
}); });
// 清除日志内容端点 // Clear log content endpoint
app.delete("/api/logs", async (req: any, reply: any) => { app.delete("/api/logs", async (req: any, reply: any) => {
try { try {
const filePath = (req.query as any).file as string; const filePath = (req.query as any).file as string;
let logFilePath: string; let logFilePath: string;
if (filePath) { if (filePath) {
// 如果指定了文件路径,使用指定的路径 // If file path is specified, use the specified path
logFilePath = filePath; logFilePath = filePath;
} else { } else {
// 如果没有指定文件路径,使用默认的日志文件路径 // If file path is not specified, use default log file path
logFilePath = join(homedir(), ".claude-code-router", "logs", "app.log"); logFilePath = join(homedir(), ".claude-code-router", "logs", "app.log");
} }
@@ -168,7 +170,7 @@ export const createServer = async (config: any): Promise<any> => {
} }
}); });
// 获取预设列表 // Get presets list
app.get("/api/presets", async (req: any, reply: any) => { app.get("/api/presets", async (req: any, reply: any) => {
try { try {
const presetsDir = join(HOME_DIR, "presets"); const presetsDir = join(HOME_DIR, "presets");
@@ -189,11 +191,11 @@ export const createServer = async (config: any): Promise<any> => {
const content = readFileSync(manifestPath, 'utf-8'); const content = readFileSync(manifestPath, 'utf-8');
const manifest = JSON.parse(content); const manifest = JSON.parse(content);
// 提取 metadata 字段 // Extract metadata fields
const { Providers, Router, PORT, HOST, API_TIMEOUT_MS, PROXY_URL, LOG, LOG_LEVEL, StatusLine, NON_INTERACTIVE_MODE, requiredInputs, ...metadata } = manifest; const { Providers, Router, PORT, HOST, API_TIMEOUT_MS, PROXY_URL, LOG, LOG_LEVEL, StatusLine, NON_INTERACTIVE_MODE, requiredInputs, ...metadata } = manifest;
presets.push({ presets.push({
id: dirName, // 目录名作为唯一标识 id: dirName, // Use directory name as unique identifier
name: metadata.name || dirName, name: metadata.name || dirName,
version: metadata.version || '1.0.0', version: metadata.version || '1.0.0',
description: metadata.description, description: metadata.description,
@@ -220,7 +222,7 @@ export const createServer = async (config: any): Promise<any> => {
} }
}); });
// 获取预设详情 // Get preset details
app.get("/api/presets/:name", async (req: any, reply: any) => { app.get("/api/presets/:name", async (req: any, reply: any) => {
try { try {
const { name } = req.params; const { name } = req.params;
@@ -232,39 +234,44 @@ export const createServer = async (config: any): Promise<any> => {
} }
const manifest = await readManifestFromDir(presetDir); const manifest = await readManifestFromDir(presetDir);
const preset = manifestToPresetFile(manifest); const presetFile = manifestToPresetFile(manifest);
return preset; // Return preset info, config uses the applied userValues configuration
return {
...presetFile,
config: loadConfigFromManifest(manifest),
userValues: manifest.userValues || {},
};
} catch (error: any) { } catch (error: any) {
console.error("Failed to get preset:", error); console.error("Failed to get preset:", error);
reply.status(500).send({ error: error.message || "Failed to get preset" }); reply.status(500).send({ error: error.message || "Failed to get preset" });
} }
}); });
// 上传并安装预设(支持文件上传) // Upload and install preset (supports file upload)
app.post("/api/presets/install", async (req: any, reply: any) => { app.post("/api/presets/install", async (req: any, reply: any) => {
try { try {
const { source, name, url } = req.body; const { source, name, url } = req.body;
// 如果提供了 URL URL 下载 // If URL is provided, download from URL
if (url) { if (url) {
const tempFile = await downloadPresetToTemp(url); const tempFile = await downloadPresetToTemp(url);
const preset = await loadPresetFromZip(tempFile); const preset = await loadPresetFromZip(tempFile);
// 确定预设名称 // Determine preset name
const presetName = name || preset.metadata?.name || `preset-${Date.now()}`; const presetName = name || preset.metadata?.name || `preset-${Date.now()}`;
// 检查是否已安装 // Check if already installed
if (await isPresetInstalled(presetName)) { if (await isPresetInstalled(presetName)) {
reply.status(409).send({ error: "Preset already installed" }); reply.status(409).send({ error: "Preset already installed" });
return; return;
} }
// 解压到目标目录 // Extract to target directory
const targetDir = getPresetDir(presetName); const targetDir = getPresetDir(presetName);
await extractPreset(tempFile, targetDir); await extractPreset(tempFile, targetDir);
// 清理临时文件 // Clean up temp file
unlinkSync(tempFile); unlinkSync(tempFile);
return { return {
@@ -277,8 +284,8 @@ export const createServer = async (config: any): Promise<any> => {
}; };
} }
// 如果没有 URL需要处理文件上传使用 multipart/form-data // If no URL, need to handle file upload (using multipart/form-data)
// 这部分需要在客户端使用 FormData 上传 // This part requires FormData upload on client side
reply.status(400).send({ error: "Please provide a URL or upload a file" }); reply.status(400).send({ error: "Please provide a URL or upload a file" });
} catch (error: any) { } catch (error: any) {
console.error("Failed to install preset:", error); console.error("Failed to install preset:", error);
@@ -286,7 +293,7 @@ export const createServer = async (config: any): Promise<any> => {
} }
}); });
// 上传预设文件(multipart/form-data // Upload preset file (multipart/form-data)
app.post("/api/presets/upload", async (req: any, reply: any) => { app.post("/api/presets/upload", async (req: any, reply: any) => {
try { try {
const data = await req.file(); const data = await req.file();
@@ -300,28 +307,28 @@ export const createServer = async (config: any): Promise<any> => {
const tempFile = join(tempDir, `preset-${Date.now()}.ccrsets`); const tempFile = join(tempDir, `preset-${Date.now()}.ccrsets`);
// 保存上传的文件到临时位置 // Save uploaded file to temp location
const buffer = await data.toBuffer(); const buffer = await data.toBuffer();
writeFileSync(tempFile, buffer); writeFileSync(tempFile, buffer);
// 加载预设 // Load preset
const preset = await loadPresetFromZip(tempFile); const preset = await loadPresetFromZip(tempFile);
// 确定预设名称 // Determine preset name
const presetName = data.fields.name?.value || preset.metadata?.name || `preset-${Date.now()}`; const presetName = data.fields.name?.value || preset.metadata?.name || `preset-${Date.now()}`;
// 检查是否已安装 // Check if already installed
if (await isPresetInstalled(presetName)) { if (await isPresetInstalled(presetName)) {
unlinkSync(tempFile); unlinkSync(tempFile);
reply.status(409).send({ error: "Preset already installed" }); reply.status(409).send({ error: "Preset already installed" });
return; return;
} }
// 解压到目标目录 // Extract to target directory
const targetDir = getPresetDir(presetName); const targetDir = getPresetDir(presetName);
await extractPreset(tempFile, targetDir); await extractPreset(tempFile, targetDir);
// 清理临时文件 // Clean up temp file
unlinkSync(tempFile); unlinkSync(tempFile);
return { return {
@@ -338,7 +345,7 @@ export const createServer = async (config: any): Promise<any> => {
} }
}); });
// 应用预设(配置敏感信息) // 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 {
const { name } = req.params; const { name } = req.params;
@@ -351,27 +358,22 @@ export const createServer = async (config: any): Promise<any> => {
return; return;
} }
// 读取现有 manifest // Read existing manifest
const manifest = await readManifestFromDir(presetDir); const manifest = await readManifestFromDir(presetDir);
// 将 secrets 信息应用到 manifest 中 // Save user input to userValues (keep original config unchanged)
if (secrets) { const updatedManifest: ManifestFile = { ...manifest };
for (const [fieldPath, value] of Object.entries(secrets)) {
const keys = fieldPath.split(/[.\[\]]+/).filter(k => k !== ''); // Save or update userValues
let current = manifest as any; if (secrets && Object.keys(secrets).length > 0) {
for (let i = 0; i < keys.length - 1; i++) { updatedManifest.userValues = {
const key = keys[i]; ...updatedManifest.userValues,
if (!current[key]) { ...secrets,
current[key] = {}; };
}
current = current[key];
}
current[keys[keys.length - 1]] = value;
}
} }
// 保存更新后的 manifest // Save updated manifest
await saveManifest(name, manifest); await saveManifest(name, updatedManifest);
return { success: true, message: "Preset applied successfully" }; return { success: true, message: "Preset applied successfully" };
} catch (error: any) { } catch (error: any) {
@@ -380,7 +382,7 @@ export const createServer = async (config: any): Promise<any> => {
} }
}); });
// 删除预设 // Delete preset
app.delete("/api/presets/:name", async (req: any, reply: any) => { app.delete("/api/presets/:name", async (req: any, reply: any) => {
try { try {
const { name } = req.params; const { name } = req.params;
@@ -391,7 +393,7 @@ export const createServer = async (config: any): Promise<any> => {
return; return;
} }
// 递归删除整个目录 // Recursively delete entire directory
rmSync(presetDir, { recursive: true, force: true }); rmSync(presetDir, { recursive: true, force: true });
return { success: true, message: "Preset deleted successfully" }; return { success: true, message: "Preset deleted successfully" };
@@ -401,7 +403,7 @@ export const createServer = async (config: any): Promise<any> => {
} }
}); });
// 获取预设市场列表 // Get preset market list
app.get("/api/presets/market", async (req: any, reply: any) => { app.get("/api/presets/market", async (req: any, reply: any) => {
try { try {
const marketUrl = "https://pub-0dc3e1677e894f07bbea11b17a29e032.r2.dev/presets.json"; const marketUrl = "https://pub-0dc3e1677e894f07bbea11b17a29e032.r2.dev/presets.json";
@@ -419,7 +421,7 @@ export const createServer = async (config: any): Promise<any> => {
} }
}); });
// 从 GitHub 仓库安装预设 // Install preset from GitHub repository
app.post("/api/presets/install/github", async (req: any, reply: any) => { app.post("/api/presets/install/github", async (req: any, reply: any) => {
try { try {
const { repo, name } = req.body; const { repo, name } = req.body;
@@ -429,9 +431,9 @@ export const createServer = async (config: any): Promise<any> => {
return; return;
} }
// 解析 GitHub 仓库 URL // Parse GitHub repository URL
// 支持格式: // Supported formats:
// - owner/repo (简短格式,来自市场) // - owner/repo (short format, from market)
// - github.com/owner/repo // - github.com/owner/repo
// - https://github.com/owner/repo // - https://github.com/owner/repo
// - https://github.com/owner/repo.git // - https://github.com/owner/repo.git
@@ -444,28 +446,28 @@ export const createServer = async (config: any): Promise<any> => {
const [, owner, repoName] = githubRepoMatch; const [, owner, repoName] = githubRepoMatch;
// 下载 GitHub 仓库的 ZIP 文件 // Download GitHub repository ZIP file
const downloadUrl = `https://github.com/${owner}/${repoName}/archive/refs/heads/main.zip`; const downloadUrl = `https://github.com/${owner}/${repoName}/archive/refs/heads/main.zip`;
const tempFile = await downloadPresetToTemp(downloadUrl); const tempFile = await downloadPresetToTemp(downloadUrl);
// 加载预设 // Load preset
const preset = await loadPresetFromZip(tempFile); const preset = await loadPresetFromZip(tempFile);
// 确定预设名称 // Determine preset name
const presetName = name || preset.metadata?.name || repoName; const presetName = name || preset.metadata?.name || repoName;
// 检查是否已安装 // Check if already installed
if (await isPresetInstalled(presetName)) { if (await isPresetInstalled(presetName)) {
unlinkSync(tempFile); unlinkSync(tempFile);
reply.status(409).send({ error: "Preset already installed" }); reply.status(409).send({ error: "Preset already installed" });
return; return;
} }
// 解压到目标目录 // Extract to target directory
const targetDir = getPresetDir(presetName); const targetDir = getPresetDir(presetName);
await extractPreset(tempFile, targetDir); await extractPreset(tempFile, targetDir);
// 清理临时文件 // Clean up temp file
unlinkSync(tempFile); unlinkSync(tempFile);
return { return {
@@ -482,17 +484,17 @@ export const createServer = async (config: any): Promise<any> => {
} }
}); });
// 辅助函数:从 ZIP 加载预设 // Helper function: Load preset from ZIP
async function loadPresetFromZip(zipFile: string): Promise<PresetFile> { async function loadPresetFromZip(zipFile: string): Promise<PresetFile> {
const zip = new AdmZip(zipFile); const zip = new AdmZip(zipFile);
// 首先尝试在根目录查找 manifest.json // First try to find manifest.json in root directory
let entry = zip.getEntry('manifest.json'); let entry = zip.getEntry('manifest.json');
// 如果根目录没有,尝试在子目录中查找(处理 GitHub 仓库的压缩包结构) // If not in root, try to find in subdirectories (handle GitHub repo archive structure)
if (!entry) { if (!entry) {
const entries = zip.getEntries(); const entries = zip.getEntries();
// 查找任意 manifest.json 文件 // Find any manifest.json file
entry = entries.find(e => e.entryName.includes('manifest.json')) || null; entry = entries.find(e => e.entryName.includes('manifest.json')) || null;
} }

View File

@@ -1,37 +1,77 @@
/** /**
* 预设安装核心功能 * Core preset installation functionality
* 注意:这个模块不包含 CLI 交互逻辑,交互逻辑由调用者提供 * Note: This module does not contain CLI interaction logic, interaction logic is provided by the caller
*/ */
import * as fs from 'fs/promises'; 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 } from './types'; import { PresetFile, MergeStrategy, RequiredInput, ManifestFile, PresetInfo, PresetMetadata } from './types';
import { HOME_DIR, PRESETS_DIR } from '../constants'; import { HOME_DIR, PRESETS_DIR } from '../constants';
import { loadConfigFromManifest } from './schema';
/** /**
* 获取预设目录的完整路径 * Validate if preset name is safe (prevent path traversal attacks)
* @param presetName 预设名称 * @param presetName Preset name
*/
function validatePresetName(presetName: string): void {
if (!presetName || presetName.trim() === '') {
throw new Error('Preset name cannot be empty');
}
// Reject names containing path traversal sequences
if (presetName.includes('..') || presetName.includes('/') || presetName.includes('\\')) {
throw new Error('Invalid preset name: path traversal detected');
}
// Reject absolute paths
if (path.isAbsolute(presetName)) {
throw new Error('Invalid preset name: absolute path not allowed');
}
}
/**
* Get the full path of the preset directory
* @param presetName Preset name
*/ */
export function getPresetDir(presetName: string): string { export function getPresetDir(presetName: string): string {
validatePresetName(presetName);
return path.join(HOME_DIR, 'presets', presetName); return path.join(HOME_DIR, 'presets', presetName);
} }
/** /**
* 获取临时目录路径 * Get temporary directory path
*/ */
export function getTempDir(): string { export function getTempDir(): string {
return path.join(HOME_DIR, 'temp'); return path.join(HOME_DIR, 'temp');
} }
/** /**
* 解压预设文件到目标目录 * Validate and normalize file path, ensuring it's within the target directory
* @param sourceZip 源ZIP文件路径 * @param targetDir Target directory
* @param targetDir 目标目录 * @param entryPath ZIP entry path
* @returns Safe absolute path
*/
function validateAndResolvePath(targetDir: string, entryPath: string): string {
const resolvedTargetDir = path.resolve(targetDir);
const resolvedPath = path.resolve(targetDir, entryPath);
// Verify that the resolved path is within the target directory
if (!resolvedPath.startsWith(resolvedTargetDir)) {
throw new Error(`Path traversal detected: ${entryPath}`);
}
return resolvedPath;
}
/**
* Extract preset file to target directory
* @param sourceZip Source ZIP file path
* @param targetDir Target directory
*/ */
export async function extractPreset(sourceZip: string, targetDir: string): Promise<void> { export async function extractPreset(sourceZip: string, targetDir: string): Promise<void> {
// 检查目标目录是否已存在 // Check if target directory already exists
try { try {
await fs.access(targetDir); await fs.access(targetDir);
throw new Error(`Preset directory already exists: ${path.basename(targetDir)}`); throw new Error(`Preset directory already exists: ${path.basename(targetDir)}`);
@@ -39,19 +79,19 @@ export async function extractPreset(sourceZip: string, targetDir: string): Promi
if (error.code !== 'ENOENT') { if (error.code !== 'ENOENT') {
throw error; throw error;
} }
// ENOENT 表示目录不存在,可以继续 // ENOENT means directory does not exist, can continue
} }
// 创建目标目录 // Create target directory
await fs.mkdir(targetDir, { recursive: true }); await fs.mkdir(targetDir, { recursive: true });
// 解压文件 // Extract files
const zip = new AdmZip(sourceZip); const zip = new AdmZip(sourceZip);
const entries = zip.getEntries(); const entries = zip.getEntries();
// 检测是否有单一的根目录GitHub ZIP 文件通常有这个特征) // Detect if there's a single root directory (GitHub ZIP files usually have this characteristic)
if (entries.length > 0) { if (entries.length > 0) {
// 获取所有顶层目录 // Get all top-level directories
const rootDirs = new Set<string>(); const rootDirs = new Set<string>();
for (const entry of entries) { for (const entry of entries) {
const parts = entry.entryName.split('/'); const parts = entry.entryName.split('/');
@@ -60,35 +100,35 @@ export async function extractPreset(sourceZip: string, targetDir: string): Promi
} }
} }
// 如果只有一个根目录,则去除它 // If there's only one root directory, remove it
if (rootDirs.size === 1) { if (rootDirs.size === 1) {
const singleRoot = Array.from(rootDirs)[0]; const singleRoot = Array.from(rootDirs)[0];
// 检查 manifest.json 是否在根目录下 // Check if manifest.json is in root directory
const hasManifestInRoot = entries.some(e => const hasManifestInRoot = entries.some(e =>
e.entryName === 'manifest.json' || e.entryName.startsWith(`${singleRoot}/manifest.json`) e.entryName === 'manifest.json' || e.entryName.startsWith(`${singleRoot}/manifest.json`)
); );
if (hasManifestInRoot) { if (hasManifestInRoot) {
// 将所有文件从根目录下提取出来 // Extract all files from the root directory
for (const entry of entries) { for (const entry of entries) {
if (entry.isDirectory) { if (entry.isDirectory) {
continue; continue;
} }
// 去除根目录前缀 // Remove root directory prefix
let newPath = entry.entryName; let newPath = entry.entryName;
if (newPath.startsWith(`${singleRoot}/`)) { if (newPath.startsWith(`${singleRoot}/`)) {
newPath = newPath.substring(singleRoot.length + 1); newPath = newPath.substring(singleRoot.length + 1);
} }
// 跳过根目录本身 // Skip root directory itself
if (newPath === '' || newPath === singleRoot) { if (newPath === '' || newPath === singleRoot) {
continue; continue;
} }
// 提取文件 // Validate path safety and extract file
const targetPath = path.join(targetDir, newPath); const targetPath = validateAndResolvePath(targetDir, newPath);
await fs.mkdir(path.dirname(targetPath), { recursive: true }); await fs.mkdir(path.dirname(targetPath), { recursive: true });
await fs.writeFile(targetPath, entry.getData()); await fs.writeFile(targetPath, entry.getData());
} }
@@ -98,13 +138,22 @@ export async function extractPreset(sourceZip: string, targetDir: string): Promi
} }
} }
// 如果没有单一的根目录,直接解压 // If there's no single root directory, validate and extract files one by one
zip.extractAllTo(targetDir, true); for (const entry of entries) {
if (entry.isDirectory) {
continue;
}
// Validate path safety
const targetPath = validateAndResolvePath(targetDir, entry.entryName);
await fs.mkdir(path.dirname(targetPath), { recursive: true });
await fs.writeFile(targetPath, entry.getData());
}
} }
/** /**
* 从解压目录读取manifest * Read manifest from extracted directory
* @param presetDir 预设目录路径 * @param presetDir Preset directory path
*/ */
export async function readManifestFromDir(presetDir: string): Promise<ManifestFile> { export async function readManifestFromDir(presetDir: string): Promise<ManifestFile> {
const manifestPath = path.join(presetDir, 'manifest.json'); const manifestPath = path.join(presetDir, 'manifest.json');
@@ -113,21 +162,68 @@ export async function readManifestFromDir(presetDir: string): Promise<ManifestFi
} }
/** /**
* 将manifest转换为PresetFile格式 * List of known metadata fields
*/
const METADATA_FIELDS = [
'name',
'version',
'description',
'author',
'homepage',
'repository',
'license',
'keywords',
'ccrVersion',
'source',
'sourceType',
'checksum',
];
/**
* Dynamic configuration system field list
*/
const DYNAMIC_CONFIG_FIELDS = [
'schema',
'template',
'configMappings',
];
/**
* Convert manifest to PresetFile format
* Correctly separate metadata, config, and dynamic configuration system fields
*/ */
export function manifestToPresetFile(manifest: ManifestFile): PresetFile { export function manifestToPresetFile(manifest: ManifestFile): PresetFile {
const { Providers, Router, StatusLine, NON_INTERACTIVE_MODE, schema, ...metadata } = manifest; const metadata: any = {};
const config: any = {};
const dynamicConfig: any = {};
// Categorize all fields
for (const [key, value] of Object.entries(manifest)) {
if (METADATA_FIELDS.includes(key)) {
// metadata fields
metadata[key] = value;
} else if (DYNAMIC_CONFIG_FIELDS.includes(key)) {
// dynamic configuration system fields
dynamicConfig[key] = value;
} else {
// configuration fields
config[key] = value;
}
}
return { return {
metadata, metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
config: { Providers, Router, StatusLine, NON_INTERACTIVE_MODE }, config,
schema, schema: dynamicConfig.schema,
template: dynamicConfig.template,
configMappings: dynamicConfig.configMappings,
}; };
} }
/** /**
* 下载预设文件到临时位置 * Download preset file to temporary location
* @param url 下载URL * @param url Download URL
* @returns 临时文件路径 * @returns Temporary file path
*/ */
export async function downloadPresetToTemp(url: string): Promise<string> { export async function downloadPresetToTemp(url: string): Promise<string> {
const response = await fetch(url); const response = await fetch(url);
@@ -136,7 +232,7 @@ export async function downloadPresetToTemp(url: string): Promise<string> {
} }
const buffer = await response.arrayBuffer(); const buffer = await response.arrayBuffer();
// 创建临时文件 // Create temporary file
const tempDir = getTempDir(); const tempDir = getTempDir();
await fs.mkdir(tempDir, { recursive: true }); await fs.mkdir(tempDir, { recursive: true });
@@ -147,8 +243,8 @@ export async function downloadPresetToTemp(url: string): Promise<string> {
} }
/** /**
* 从本地ZIP文件加载预设 * Load preset from local ZIP file
* @param zipFile ZIP文件路径 * @param zipFile ZIP file path
* @returns PresetFile * @returns PresetFile
*/ */
export async function loadPresetFromZip(zipFile: string): Promise<PresetFile> { export async function loadPresetFromZip(zipFile: string): Promise<PresetFile> {
@@ -162,33 +258,33 @@ export async function loadPresetFromZip(zipFile: string): Promise<PresetFile> {
} }
/** /**
* 加载预设文件 * Load preset file
* @param source 预设来源文件路径、URL 或预设名称) * @param source Preset source (file path, URL, or preset name)
*/ */
export async function loadPreset(source: string): Promise<PresetFile> { export async function loadPreset(source: string): Promise<PresetFile> {
// 判断是否是 URL // Check if it's a URL
if (source.startsWith('http://') || source.startsWith('https://')) { if (source.startsWith('http://') || source.startsWith('https://')) {
const tempFile = await downloadPresetToTemp(source); const tempFile = await downloadPresetToTemp(source);
const preset = await loadPresetFromZip(tempFile); const preset = await loadPresetFromZip(tempFile);
// 删除临时文件 // Delete temp file
await fs.unlink(tempFile).catch(() => {}); await fs.unlink(tempFile).catch(() => {});
return preset; return preset;
} }
// 判断是否是绝对路径或相对路径(包含 / \ // Check if it's absolute or relative path (contains / or \)
if (source.includes('/') || source.includes('\\')) { if (source.includes('/') || source.includes('\\')) {
// 文件路径 // File path
return await loadPresetFromZip(source); return await loadPresetFromZip(source);
} }
// 否则作为预设名称处理(从解压目录读取) // Otherwise treat as preset name (read from extracted 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);
} }
/** /**
* 验证预设文件 * Validate preset file
*/ */
export async function validatePreset(preset: PresetFile): Promise<{ export async function validatePreset(preset: PresetFile): Promise<{
valid: boolean; valid: boolean;
@@ -198,7 +294,7 @@ export async function validatePreset(preset: PresetFile): Promise<{
const errors: string[] = []; const errors: string[] = [];
const warnings: string[] = []; const warnings: string[] = [];
// 验证元数据 // Validate metadata
if (!preset.metadata) { if (!preset.metadata) {
warnings.push('Missing metadata section'); warnings.push('Missing metadata section');
} else { } else {
@@ -210,12 +306,12 @@ export async function validatePreset(preset: PresetFile): Promise<{
} }
} }
// 验证配置部分 // Validate configuration section
if (!preset.config) { if (!preset.config) {
errors.push('Missing config section'); errors.push('Missing config section');
} }
// 验证 Providers // Validate Providers
if (preset.config.Providers) { if (preset.config.Providers) {
for (const provider of preset.config.Providers) { for (const provider of preset.config.Providers) {
if (!provider.name) { if (!provider.name) {
@@ -238,9 +334,35 @@ export async function validatePreset(preset: PresetFile): Promise<{
} }
/** /**
* 保存 manifest 到预设目录 * Extract metadata fields from manifest
* @param presetName 预设名称 * @param manifest Manifest object
* @param manifest manifest 对象 * @returns Metadata object
*/
export function extractMetadata(manifest: ManifestFile): PresetMetadata {
const metadata: PresetMetadata = {
name: manifest.name,
version: manifest.version,
};
// Optional fields
if (manifest.description !== undefined) metadata.description = manifest.description;
if (manifest.author !== undefined) metadata.author = manifest.author;
if (manifest.homepage !== undefined) metadata.homepage = manifest.homepage;
if (manifest.repository !== undefined) metadata.repository = manifest.repository;
if (manifest.license !== undefined) metadata.license = manifest.license;
if (manifest.keywords !== undefined) metadata.keywords = manifest.keywords;
if (manifest.ccrVersion !== undefined) metadata.ccrVersion = manifest.ccrVersion;
if (manifest.source !== undefined) metadata.source = manifest.source;
if (manifest.sourceType !== undefined) metadata.sourceType = manifest.sourceType;
if (manifest.checksum !== undefined) metadata.checksum = manifest.checksum;
return metadata;
}
/**
* Save manifest to preset directory
* @param presetName Preset name
* @param manifest Manifest object
*/ */
export async function saveManifest(presetName: string, manifest: ManifestFile): Promise<void> { export async function saveManifest(presetName: string, manifest: ManifestFile): Promise<void> {
const presetDir = getPresetDir(presetName); const presetDir = getPresetDir(presetName);
@@ -249,23 +371,23 @@ export async function saveManifest(presetName: string, manifest: ManifestFile):
} }
/** /**
* 查找预设文件 * Find preset file
* @param source 预设来源 * @param source Preset source
* @returns 文件路径或 null * @returns File path or null
*/ */
export async function findPresetFile(source: string): Promise<string | null> { export async function findPresetFile(source: string): Promise<string | null> {
// 当前目录文件 // Current directory file
const currentDirFile = path.join(process.cwd(), `${source}.ccrsets`); const currentDirFile = path.join(process.cwd(), `${source}.ccrsets`);
// presets 目录文件 // presets directory file
const presetsDirFile = path.join(HOME_DIR, 'presets', `${source}.ccrsets`); const presetsDirFile = path.join(HOME_DIR, 'presets', `${source}.ccrsets`);
// 检查当前目录 // Check current directory
try { try {
await fs.access(currentDirFile); await fs.access(currentDirFile);
return currentDirFile; return currentDirFile;
} catch { } catch {
// 检查presets目录 // Check presets directory
try { try {
await fs.access(presetsDirFile); await fs.access(presetsDirFile);
return presetsDirFile; return presetsDirFile;
@@ -276,8 +398,8 @@ export async function findPresetFile(source: string): Promise<string | null> {
} }
/** /**
* 检查预设是否已安装 * Check if preset is already installed
* @param presetName 预设名称 * @param presetName Preset name
*/ */
export async function isPresetInstalled(presetName: string): Promise<boolean> { export async function isPresetInstalled(presetName: string): Promise<boolean> {
const presetDir = getPresetDir(presetName); const presetDir = getPresetDir(presetName);
@@ -290,8 +412,8 @@ export async function isPresetInstalled(presetName: string): Promise<boolean> {
} }
/** /**
* 列出所有已安装的预设 * List all installed presets
* @returns PresetInfo 数组 * @returns Array of PresetInfo
*/ */
export async function listPresets(): Promise<PresetInfo[]> { export async function listPresets(): Promise<PresetInfo[]> {
const presetsDir = PRESETS_DIR; const presetsDir = PRESETS_DIR;
@@ -303,7 +425,7 @@ export async function listPresets(): Promise<PresetInfo[]> {
return presets; return presets;
} }
// 读取目录下的所有子目录 // Read all subdirectories in the directory
const entries = await fs.readdir(presetsDir, { withFileTypes: true }); const entries = await fs.readdir(presetsDir, { withFileTypes: true });
for (const entry of entries) { for (const entry of entries) {
@@ -313,14 +435,14 @@ export async function listPresets(): Promise<PresetInfo[]> {
const manifestPath = path.join(presetDir, 'manifest.json'); const manifestPath = path.join(presetDir, 'manifest.json');
try { try {
// 检查 manifest.json 是否存在 // Check if manifest.json exists
await fs.access(manifestPath); await fs.access(manifestPath);
// 读取 manifest.json // Read manifest.json
const content = await fs.readFile(manifestPath, 'utf-8'); const content = await fs.readFile(manifestPath, 'utf-8');
const manifest = JSON5.parse(content) as ManifestFile; const manifest = JSON5.parse(content) as ManifestFile;
// 获取目录创建时间 // Get directory creation time
const stats = await fs.stat(presetDir); const stats = await fs.stat(presetDir);
presets.push({ presets.push({
@@ -328,11 +450,11 @@ export async function listPresets(): Promise<PresetInfo[]> {
version: manifest.version, version: manifest.version,
description: manifest.description, description: manifest.description,
author: manifest.author, author: manifest.author,
config: manifestToPresetFile(manifest).config, config: loadConfigFromManifest(manifest),
}); });
} catch { } catch {
// 忽略无效的预设目录(没有 manifest.json 或读取失败) // Ignore invalid preset directories (no manifest.json or read failed)
// 可以选择跳过或者添加到列表中标记为错误 // Can choose to skip or add to list marked as error
continue; continue;
} }
} }

View File

@@ -1,24 +1,24 @@
/** /**
* 读取预设配置文件 * Read preset configuration file
* 用于 CLI 快速读取预设配置 * Used by CLI to quickly read preset configuration
*/ */
import * as fs from 'fs/promises'; 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 { HOME_DIR } from '../constants'; import { getPresetDir } from './install';
/** /**
* 读取 preset 配置文件 * Read preset configuration file
* @param name preset 名称 * @param name Preset name
* @returns preset 配置对象,如果文件不存在则返回 null * @returns Preset configuration object, or null if file does not exist
*/ */
export async function readPresetFile(name: string): Promise<any | null> { export async function readPresetFile(name: string): Promise<any | null> {
try { try {
const presetDir = path.join(HOME_DIR, 'presets', name); const presetDir = getPresetDir(name);
const manifestPath = path.join(presetDir, 'manifest.json'); const manifestPath = path.join(presetDir, 'manifest.json');
const manifest = JSON5.parse(await fs.readFile(manifestPath, 'utf-8')); const manifest = JSON5.parse(await fs.readFile(manifestPath, 'utf-8'));
// manifest已经是扁平化结构,直接返回 // manifest is already a flat structure, return directly
return manifest; return manifest;
} catch (error: any) { } catch (error: any) {
if (error.code === 'ENOENT') { if (error.code === 'ENOENT') {

View File

@@ -1,6 +1,6 @@
/** /**
* 动态配置 Schema 处理器 * Dynamic configuration Schema handler
* 负责解析和验证配置 schema处理条件逻辑和变量替换 * Responsible for parsing and validating configuration schema, handling conditional logic and variable replacement
*/ */
import { import {
@@ -12,16 +12,14 @@ import {
ConfigMapping, ConfigMapping,
TemplateConfig, TemplateConfig,
PresetConfigSection, PresetConfigSection,
PresetFile,
ManifestFile,
UserInputValues,
} from './types'; } from './types';
// 用户输入值集合
export interface UserInputValues {
[inputId: string]: any;
}
/** /**
* 解析字段路径(支持数组和嵌套) * Parse field path (supports arrays and nesting)
* 例如:Providers[0].name => ['Providers', '0', 'name'] * Example: Providers[0].name => ['Providers', '0', 'name']
*/ */
export function parseFieldPath(path: string): string[] { export function parseFieldPath(path: string): string[] {
const regex = /(\w+)|\[(\d+)\]/g; const regex = /(\w+)|\[(\d+)\]/g;
@@ -36,7 +34,7 @@ export function parseFieldPath(path: string): string[] {
} }
/** /**
* 根据字段路径获取对象中的值 * Get value from object by field path
*/ */
export function getValueByPath(obj: any, path: string): any { export function getValueByPath(obj: any, path: string): any {
const parts = parseFieldPath(path); const parts = parseFieldPath(path);
@@ -53,7 +51,7 @@ export function getValueByPath(obj: any, path: string): any {
} }
/** /**
* 根据字段路径设置对象中的值 * Set value in object by field path
*/ */
export function setValueByPath(obj: any, path: string, value: any): void { export function setValueByPath(obj: any, path: string, value: any): void {
const parts = parseFieldPath(path); const parts = parseFieldPath(path);
@@ -62,7 +60,7 @@ export function setValueByPath(obj: any, path: string, value: any): void {
for (const part of parts) { for (const part of parts) {
if (!(part in current)) { if (!(part in current)) {
// 判断是数组还是对象 // Determine if it's an array or object
const nextPart = parts[parts.indexOf(part) + 1]; const nextPart = parts[parts.indexOf(part) + 1];
if (nextPart && /^\d+$/.test(nextPart)) { if (nextPart && /^\d+$/.test(nextPart)) {
current[part] = []; current[part] = [];
@@ -77,7 +75,7 @@ export function setValueByPath(obj: any, path: string, value: any): void {
} }
/** /**
* 评估条件表达式 * Evaluate conditional expression
*/ */
export function evaluateCondition( export function evaluateCondition(
condition: Condition, condition: Condition,
@@ -85,22 +83,22 @@ export function evaluateCondition(
): boolean { ): boolean {
const actualValue = values[condition.field]; const actualValue = values[condition.field];
// 处理 exists 操作符 // Handle exists operator
if (condition.operator === 'exists') { if (condition.operator === 'exists') {
return actualValue !== undefined && actualValue !== null; return actualValue !== undefined && actualValue !== null;
} }
// 处理 in 操作符 // Handle in operator
if (condition.operator === 'in') { if (condition.operator === 'in') {
return Array.isArray(condition.value) && condition.value.includes(actualValue); return Array.isArray(condition.value) && condition.value.includes(actualValue);
} }
// 处理 nin 操作符 // Handle nin operator
if (condition.operator === 'nin') { if (condition.operator === 'nin') {
return Array.isArray(condition.value) && !condition.value.includes(actualValue); return Array.isArray(condition.value) && !condition.value.includes(actualValue);
} }
// 处理其他操作符 // Handle other operators
switch (condition.operator) { switch (condition.operator) {
case 'eq': case 'eq':
return actualValue === condition.value; return actualValue === condition.value;
@@ -115,13 +113,13 @@ export function evaluateCondition(
case 'lte': case 'lte':
return actualValue <= condition.value; return actualValue <= condition.value;
default: default:
// 默认使用 eq // Default to eq
return actualValue === condition.value; return actualValue === condition.value;
} }
} }
/** /**
* 评估多个条件AND 逻辑) * Evaluate multiple conditions (AND logic)
*/ */
export function evaluateConditions( export function evaluateConditions(
conditions: Condition | Condition[], conditions: Condition | Condition[],
@@ -135,12 +133,12 @@ export function evaluateConditions(
return evaluateCondition(conditions, values); return evaluateCondition(conditions, values);
} }
// 如果是数组,使用 AND 逻辑(所有条件都必须满足) // If array, use AND logic (all conditions must be satisfied)
return conditions.every(condition => evaluateCondition(condition, values)); return conditions.every(condition => evaluateCondition(condition, values));
} }
/** /**
* 判断字段是否应该显示 * Determine if field should be displayed
*/ */
export function shouldShowField( export function shouldShowField(
field: RequiredInput, field: RequiredInput,
@@ -154,7 +152,7 @@ export function shouldShowField(
} }
/** /**
* 获取动态选项列表 * Get dynamic options list
*/ */
export function getDynamicOptions( export function getDynamicOptions(
dynamicOptions: DynamicOptions, dynamicOptions: DynamicOptions,
@@ -166,7 +164,7 @@ export function getDynamicOptions(
return dynamicOptions.options || []; return dynamicOptions.options || [];
case 'providers': { case 'providers': {
// 从预设的 Providers 中提取选项 // Extract options from preset's Providers
const providers = presetConfig.Providers || []; const providers = presetConfig.Providers || [];
return providers.map((p: any) => ({ return providers.map((p: any) => ({
label: p.name || p.id || String(p), label: p.name || p.id || String(p),
@@ -176,13 +174,13 @@ export function getDynamicOptions(
} }
case 'models': { case 'models': {
// 从指定 provider models 中提取 // Extract from specified provider's models
const providerField = dynamicOptions.providerField; const providerField = dynamicOptions.providerField;
if (!providerField) { if (!providerField) {
return []; return [];
} }
// 解析 provider 引用(如 {{selectedProvider}} // Parse provider reference (e.g. {{selectedProvider}})
const providerId = String(providerField).replace(/^{{(.+)}}$/, '$1'); const providerId = String(providerField).replace(/^{{(.+)}}$/, '$1');
const selectedProvider = values[providerId]; const selectedProvider = values[providerId];
@@ -190,7 +188,7 @@ export function getDynamicOptions(
return []; return [];
} }
// 查找对应的 provider // Find corresponding provider
const provider = presetConfig.Providers.find( const provider = presetConfig.Providers.find(
(p: any) => p.name === selectedProvider || p.id === selectedProvider (p: any) => p.name === selectedProvider || p.id === selectedProvider
); );
@@ -206,7 +204,7 @@ export function getDynamicOptions(
} }
case 'custom': case 'custom':
// 预留,暂未实现 // Reserved, not implemented yet
return []; return [];
default: default:
@@ -215,7 +213,7 @@ export function getDynamicOptions(
} }
/** /**
* 解析选项(支持静态和动态选项) * Resolve options (supports static and dynamic options)
*/ */
export function resolveOptions( export function resolveOptions(
field: RequiredInput, field: RequiredInput,
@@ -226,16 +224,16 @@ export function resolveOptions(
return []; return [];
} }
// 判断是静态选项还是动态选项 // Determine if static or dynamic options
const options = field.options as any; const options = field.options as any;
if (Array.isArray(options)) { if (Array.isArray(options)) {
// 静态选项数组 // Static options array
return options as InputOption[]; return options as InputOption[];
} }
if (options.type) { if (options.type) {
// 动态选项 // Dynamic options
return getDynamicOptions(options, presetConfig, values); return getDynamicOptions(options, presetConfig, values);
} }
@@ -243,8 +241,8 @@ export function resolveOptions(
} }
/** /**
* 模板变量替换 * Template variable replacement
* 支持 {{variable}} 语法 * Supports {{variable}} syntax
*/ */
export function replaceTemplateVariables( export function replaceTemplateVariables(
template: any, template: any,
@@ -254,19 +252,19 @@ export function replaceTemplateVariables(
return template; return template;
} }
// 处理字符串 // Handle strings
if (typeof template === 'string') { if (typeof template === 'string') {
return template.replace(/\{\{(\w+)\}\}/g, (_, key) => { return template.replace(/\{\{(\w+)\}\}/g, (_, key) => {
return values[key] !== undefined ? String(values[key]) : ''; return values[key] !== undefined ? String(values[key]) : '';
}); });
} }
// 处理数组 // Handle arrays
if (Array.isArray(template)) { if (Array.isArray(template)) {
return template.map(item => replaceTemplateVariables(item, values)); return template.map(item => replaceTemplateVariables(item, values));
} }
// 处理对象 // Handle objects
if (typeof template === 'object') { if (typeof template === 'object') {
const result: any = {}; const result: any = {};
for (const [key, value] of Object.entries(template)) { for (const [key, value] of Object.entries(template)) {
@@ -275,12 +273,12 @@ export function replaceTemplateVariables(
return result; return result;
} }
// 其他类型直接返回 // Return other types directly
return template; return template;
} }
/** /**
* 应用配置映射 * Apply configuration mappings
*/ */
export function applyConfigMappings( export function applyConfigMappings(
mappings: ConfigMapping[], mappings: ConfigMapping[],
@@ -290,23 +288,23 @@ export function applyConfigMappings(
const result = { ...config }; const result = { ...config };
for (const mapping of mappings) { for (const mapping of mappings) {
// 检查条件 // Check condition
if (mapping.when && !evaluateConditions(mapping.when, values)) { if (mapping.when && !evaluateConditions(mapping.when, values)) {
continue; continue;
} }
// 解析值 // Resolve value
let value: any; let value: any;
if (typeof mapping.value === 'string' && mapping.value.startsWith('{{')) { if (typeof mapping.value === 'string' && mapping.value.startsWith('{{')) {
// 变量引用 // Variable reference
const varName = mapping.value.replace(/^{{(.+)}}$/, '$1'); const varName = mapping.value.replace(/^{{(.+)}}$/, '$1');
value = values[varName]; value = values[varName];
} else { } else {
// 固定值 // Fixed value
value = mapping.value; value = mapping.value;
} }
// 应用到目标路径 // Apply to target path
setValueByPath(result, mapping.target, value); setValueByPath(result, mapping.target, value);
} }
@@ -314,13 +312,74 @@ export function applyConfigMappings(
} }
/** /**
* 验证用户输入 * Get all field ids defined in schema
*/
function getSchemaFields(schema?: RequiredInput[]): Set<string> {
if (!schema) return new Set();
return new Set(schema.map(field => field.id));
}
/**
* Apply user inputs to preset configuration
* This is the core function of the preset configuration system, uniformly handling
* configuration application for both CLI and UI layers
*
* @param presetFile Preset file object
* @param values User input values (schema id -> value)
* @returns Applied configuration object
*/
export function applyUserInputs(
presetFile: PresetFile,
values: UserInputValues
): PresetConfigSection {
let config: PresetConfigSection = {};
// Get field ids defined in schema, for subsequent filtering
const schemaFields = getSchemaFields(presetFile.schema);
// 1. First apply template (if exists)
// template completely defines configuration structure, using {{variable}} placeholders
if (presetFile.template) {
config = replaceTemplateVariables(presetFile.template, values) as any;
} else {
// If no template, start from preset's existing config
// Keep all fields, including schema's id fields (because they may contain placeholders)
// These fields will be updated or replaced in subsequent configMappings
config = presetFile.config ? { ...presetFile.config } : {};
// Replace placeholders in config (e.g. {{apiKey}} -> actual value)
config = replaceTemplateVariables(config, values) as any;
// Finally, remove schema id fields (they should not appear in final configuration)
for (const schemaField of schemaFields) {
delete config[schemaField];
}
}
// 2. Then apply configMappings (if exists)
// Map user inputs to specific configuration paths
if (presetFile.configMappings && presetFile.configMappings.length > 0) {
config = applyConfigMappings(presetFile.configMappings, values, config);
}
// 3. Compatible with legacy: apply to keys containing paths (e.g. "Providers[0].api_key")
for (const [key, value] of Object.entries(values)) {
if (key.includes('.') || key.includes('[')) {
setValueByPath(config, key, value);
}
}
return config;
}
/**
* Validate user input
*/ */
export function validateInput( export function validateInput(
field: RequiredInput, field: RequiredInput,
value: any value: any
): { valid: boolean; error?: string } { ): { valid: boolean; error?: string } {
// 检查必填 // Check required
if (field.required !== false && (value === undefined || value === null || value === '')) { if (field.required !== false && (value === undefined || value === null || value === '')) {
return { return {
valid: false, valid: false,
@@ -328,12 +387,12 @@ export function validateInput(
}; };
} }
// 如果值为空且非必填,跳过验证 // If value is empty and not required, skip validation
if (!value && field.required === false) { if (!value && field.required === false) {
return { valid: true }; return { valid: true };
} }
// 类型检查 // Type check
switch (field.type) { switch (field.type) {
case InputType.NUMBER: case InputType.NUMBER:
if (isNaN(Number(value))) { if (isNaN(Number(value))) {
@@ -359,12 +418,12 @@ export function validateInput(
case InputType.SELECT: case InputType.SELECT:
case InputType.MULTISELECT: case InputType.MULTISELECT:
// 检查值是否在选项中 // Check if value is in options
// 这里暂时跳过,因为需要动态获取选项 // Skip here for now, as options need to be dynamically retrieved
break; break;
} }
// 自定义验证器 // Custom validator
if (field.validator) { if (field.validator) {
if (field.validator instanceof RegExp) { if (field.validator instanceof RegExp) {
if (!field.validator.test(String(value))) { if (!field.validator.test(String(value))) {
@@ -401,14 +460,14 @@ export function validateInput(
} }
/** /**
* 获取字段的默认值 * Get field default value
*/ */
export function getDefaultValue(field: RequiredInput): any { export function getDefaultValue(field: RequiredInput): any {
if (field.defaultValue !== undefined) { if (field.defaultValue !== undefined) {
return field.defaultValue; return field.defaultValue;
} }
// 根据类型返回默认值 // Return default value based on type
switch (field.type) { switch (field.type) {
case InputType.CONFIRM: case InputType.CONFIRM:
return false; return false;
@@ -422,8 +481,8 @@ export function getDefaultValue(field: RequiredInput): any {
} }
/** /**
* 根据依赖关系排序字段 * Sort fields by dependency
* 确保被依赖的字段排在前面 * Ensure dependent fields are arranged first
*/ */
export function sortFieldsByDependencies( export function sortFieldsByDependencies(
fields: RequiredInput[] fields: RequiredInput[]
@@ -438,7 +497,7 @@ export function sortFieldsByDependencies(
visited.add(field.id); visited.add(field.id);
// 先处理依赖的字段 // First handle dependent fields
const dependencies = field.dependsOn || []; const dependencies = field.dependsOn || [];
for (const depId of dependencies) { for (const depId of dependencies) {
const depField = fields.find(f => f.id === depId); const depField = fields.find(f => f.id === depId);
@@ -447,7 +506,7 @@ export function sortFieldsByDependencies(
} }
} }
// 从 when 条件中提取依赖 // Extract dependencies from when conditions
if (field.when) { if (field.when) {
const conditions = Array.isArray(field.when) ? field.when : [field.when]; const conditions = Array.isArray(field.when) ? field.when : [field.when];
for (const cond of conditions) { for (const cond of conditions) {
@@ -469,7 +528,7 @@ export function sortFieldsByDependencies(
} }
/** /**
* 构建字段依赖图(用于优化更新顺序) * Build field dependency graph (for optimizing update order)
*/ */
export function buildDependencyGraph( export function buildDependencyGraph(
fields: RequiredInput[] fields: RequiredInput[]
@@ -479,14 +538,14 @@ export function buildDependencyGraph(
for (const field of fields) { for (const field of fields) {
const deps = new Set<string>(); const deps = new Set<string>();
// dependsOn 提取依赖 // Extract from dependsOn
if (field.dependsOn) { if (field.dependsOn) {
for (const dep of field.dependsOn) { for (const dep of field.dependsOn) {
deps.add(dep); deps.add(dep);
} }
} }
// 从 when 条件提取依赖 // Extract dependencies from when conditions
if (field.when) { if (field.when) {
const conditions = Array.isArray(field.when) ? field.when : [field.when]; const conditions = Array.isArray(field.when) ? field.when : [field.when];
for (const cond of conditions) { for (const cond of conditions) {
@@ -494,7 +553,7 @@ export function buildDependencyGraph(
} }
} }
// 从动态选项提取依赖 // Extract dependencies from dynamic options
if (field.options) { if (field.options) {
const options = field.options as any; const options = field.options as any;
if (options.type === 'models' && options.providerField) { if (options.type === 'models' && options.providerField) {
@@ -510,7 +569,7 @@ export function buildDependencyGraph(
} }
/** /**
* 获取受影响字段(当某个字段值变化时,哪些字段需要重新计算) * Get affected fields (when a field value changes, which fields need to be recalculated)
*/ */
export function getAffectedFields( export function getAffectedFields(
changedFieldId: string, changedFieldId: string,
@@ -519,7 +578,7 @@ export function getAffectedFields(
const affected = new Set<string>(); const affected = new Set<string>();
const graph = buildDependencyGraph(fields); const graph = buildDependencyGraph(fields);
// 找出所有依赖于 changedFieldId 的字段 // Find all fields that depend on changedFieldId
for (const [fieldId, deps] of graph.entries()) { for (const [fieldId, deps] of graph.entries()) {
if (deps.has(changedFieldId)) { if (deps.has(changedFieldId)) {
affected.add(fieldId); affected.add(fieldId);
@@ -528,3 +587,55 @@ export function getAffectedFields(
return affected; return affected;
} }
/**
* Load configuration from Manifest and apply userValues
* Used when reading installed presets, applying user configuration values at runtime
*
* @param manifest Manifest object (contains original configuration and userValues)
* @returns Applied configuration object
*/
export function loadConfigFromManifest(manifest: ManifestFile): PresetConfigSection {
// Convert manifest to PresetFile format
const presetFile: PresetFile = {
metadata: {
name: manifest.name,
version: manifest.version,
description: manifest.description,
author: manifest.author,
homepage: manifest.homepage,
repository: manifest.repository,
license: manifest.license,
keywords: manifest.keywords,
ccrVersion: manifest.ccrVersion,
source: manifest.source,
sourceType: manifest.sourceType,
checksum: manifest.checksum,
},
config: {},
schema: manifest.schema,
template: manifest.template,
configMappings: manifest.configMappings,
};
// Extract configuration section from manifest (exclude metadata and dynamic configuration fields)
const METADATA_FIELDS = [
'name', 'version', 'description', 'author', 'homepage', 'repository',
'license', 'keywords', 'ccrVersion', 'source', 'sourceType', 'checksum',
];
const DYNAMIC_CONFIG_FIELDS = ['schema', 'template', 'configMappings', 'userValues'];
for (const [key, value] of Object.entries(manifest)) {
if (!METADATA_FIELDS.includes(key) && !DYNAMIC_CONFIG_FIELDS.includes(key)) {
presetFile.config[key] = value;
}
}
// If userValues exist, apply them
if (manifest.userValues && Object.keys(manifest.userValues).length > 0) {
return applyUserInputs(presetFile, manifest.userValues);
}
// If no userValues, return original configuration directly
return presetFile.config;
}

View File

@@ -1,95 +1,100 @@
/** /**
* 预设功能的类型定义 * Type definitions for preset functionality
*/ */
// 输入类型枚举 // Collection of user input values
export interface UserInputValues {
[inputId: string]: any;
}
// Input type enumeration
export enum InputType { export enum InputType {
PASSWORD = 'password', // 密码输入(隐藏) PASSWORD = 'password', // Password input (hidden)
INPUT = 'input', // 文本输入 INPUT = 'input', // Text input
SELECT = 'select', // 单选 SELECT = 'select', // Single selection
MULTISELECT = 'multiselect', // 多选 MULTISELECT = 'multiselect', // Multiple selection
CONFIRM = 'confirm', // 确认框 CONFIRM = 'confirm', // Confirmation checkbox
EDITOR = 'editor', // 多行文本编辑器 EDITOR = 'editor', // Multi-line text editor
NUMBER = 'number', // 数字输入 NUMBER = 'number', // Number input
} }
// 选项定义 // Option definition
export interface InputOption { export interface InputOption {
label: string; // 显示文本 label: string; // Display text
value: string | number | boolean; // 实际值 value: string | number | boolean; // Actual value
description?: string; // 选项描述 description?: string; // Option description
disabled?: boolean; // 是否禁用 disabled?: boolean; // Whether disabled
icon?: string; // 图标 icon?: string; // Icon
} }
// 动态选项源 // Dynamic option source
export interface DynamicOptions { export interface DynamicOptions {
type: 'static' | 'providers' | 'models' | 'custom'; type: 'static' | 'providers' | 'models' | 'custom';
// static: 使用固定的 options 数组 // static: Use fixed options array
// providers: 从 Providers 配置中动态获取 // providers: Dynamically retrieve from Providers configuration
// models: 从指定 provider models 中获取 // models: Retrieve from specified provider's models
// custom: 自定义函数(暂未实现,预留) // custom: Custom function (reserved, not implemented yet)
// type 'static' 时使用 // Used when type is 'static'
options?: InputOption[]; options?: InputOption[];
// type 'providers' 时使用 // Used when type is 'providers'
// 自动从预设的 Providers 中提取 name 和相关配置 // Automatically extract name and related configuration from preset's Providers
// type 'models' 时使用 // Used when type is 'models'
providerField?: string; // 指向 provider 选择器的字段路径(如 "{{selectedProvider}}" providerField?: string; // Point to provider selector field path (e.g. "{{selectedProvider}}")
// type 'custom' 时使用(预留) // Used when type is 'custom' (reserved)
source?: string; // 自定义数据源 source?: string; // Custom data source
} }
// 条件表达式 // Conditional expression
export interface Condition { export interface Condition {
field: string; // 依赖的字段路径 field: string; // Dependent field path
operator?: 'eq' | 'ne' | 'in' | 'nin' | 'gt' | 'lt' | 'gte' | 'lte' | 'exists'; operator?: 'eq' | 'ne' | 'in' | 'nin' | 'gt' | 'lt' | 'gte' | 'lte' | 'exists';
value?: any; // 比较值 value?: any; // Comparison value
// eq: 等于 // eq: equals
// ne: 不等于 // ne: not equals
// in: 包含于(数组) // in: included in (array)
// nin: 不包含于(数组) // nin: not included in (array)
// gt: 大于 // gt: greater than
// lt: 小于 // lt: less than
// gte: 大于等于 // gte: greater than or equal to
// lte: 小于等于 // lte: less than or equal to
// exists: 字段存在(不检查值) // exists: field exists (doesn't check value)
} }
// 复杂的字段输入配置 // Complex field input configuration
export interface RequiredInput { export interface RequiredInput {
id: string; // 唯一标识符(用于变量引用) id: string; // Unique identifier (for variable reference)
type?: InputType; // 输入类型,默认为 password type?: InputType; // Input type, defaults to password
label?: string; // 显示标签 label?: string; // Display label
prompt?: string; // 提示信息/描述 prompt?: string; // Prompt information/description
placeholder?: string; // 占位符 placeholder?: string; // Placeholder
// 选项配置(用于 select/multiselect // Option configuration (for select/multiselect)
options?: InputOption[] | DynamicOptions; options?: InputOption[] | DynamicOptions;
// 条件显示 // Conditional display
when?: Condition | Condition[]; // 满足条件时才显示此字段(支持 AND/OR 逻辑) when?: Condition | Condition[]; // Show this field only when conditions are met (supports AND/OR logic)
// 默认值 // Default value
defaultValue?: any; defaultValue?: any;
// 验证规则 // Validation rules
required?: boolean; // 是否必填,默认 true required?: boolean; // Whether required, defaults to true
validator?: RegExp | string | ((value: any) => boolean | string); validator?: RegExp | string | ((value: any) => boolean | string);
// UI 配置 // UI configuration
min?: number; // 最小值(用于 number min?: number; // Minimum value (for number)
max?: number; // 最大值(用于 number max?: number; // Maximum value (for number)
rows?: number; // 行数(用于 editor rows?: number; // Number of rows (for editor)
// 高级配置 // Advanced configuration
dependsOn?: string[]; // 显式声明依赖的字段(用于优化更新顺序) dependsOn?: string[]; // Explicitly declare dependent fields (for optimizing update order)
} }
// Provider 配置 // Provider configuration
export interface ProviderConfig { export interface ProviderConfig {
name: string; name: string;
api_base_url: string; api_base_url: string;
@@ -99,7 +104,7 @@ export interface ProviderConfig {
[key: string]: any; [key: string]: any;
} }
// Router 配置 // Router configuration
export interface RouterConfig { export interface RouterConfig {
default?: string; default?: string;
background?: string; background?: string;
@@ -111,7 +116,7 @@ export interface RouterConfig {
[key: string]: string | number | undefined; [key: string]: string | number | undefined;
} }
// Transformer 配置 // Transformer configuration
export interface TransformerConfig { export interface TransformerConfig {
path?: string; path?: string;
use: Array<string | [string, any]>; use: Array<string | [string, any]>;
@@ -119,23 +124,23 @@ export interface TransformerConfig {
[key: string]: any; [key: string]: any;
} }
// 预设元数据(扁平化结构,用于manifest.json // Preset metadata (flattened structure, for manifest.json)
export interface PresetMetadata { export interface PresetMetadata {
name: string; // 预设名称 name: string; // Preset name
version: string; // 版本号 (semver) version: string; // Version number (semver)
description?: string; // 描述 description?: string; // Description
author?: string; // 作者 author?: string; // Author
homepage?: string; // 主页 homepage?: string; // Homepage
repository?: string; // 源码仓库 repository?: string; // Source repository
license?: string; // 许可证 license?: string; // License
keywords?: string[]; // 关键词 keywords?: string[]; // Keywords
ccrVersion?: string; // 兼容的 CCR 版本 ccrVersion?: string; // Compatible CCR version
source?: string; // 预设来源 URL source?: string; // Preset source URL
sourceType?: 'local' | 'gist' | 'registry'; sourceType?: 'local' | 'gist' | 'registry';
checksum?: string; // 预设内容校验和 checksum?: string; // Preset content checksum
} }
// 预设配置部分 // Preset configuration section
export interface PresetConfigSection { export interface PresetConfigSection {
Providers?: ProviderConfig[]; Providers?: ProviderConfig[];
Router?: RouterConfig; Router?: RouterConfig;
@@ -145,103 +150,108 @@ export interface PresetConfigSection {
[key: string]: any; [key: string]: any;
} }
// 模板配置(用于根据用户输入动态生成配置) // Template configuration (for dynamically generating configuration based on user input)
export interface TemplateConfig { export interface TemplateConfig {
// 使用 {{variable}} 语法的模板配置 // Template configuration using {{variable}} syntax
// 例如:{ "Providers": [{ "name": "{{providerName}}", "api_key": "{{apiKey}}" }] } // Example: { "Providers": [{ "name": "{{providerName}}", "api_key": "{{apiKey}}" }] }
[key: string]: any; [key: string]: any;
} }
// 配置映射(将用户输入的值映射到配置的具体位置) // Configuration mapping (maps user input values to specific configuration locations)
export interface ConfigMapping { export interface ConfigMapping {
// 字段路径(支持数组语法,如 "Providers[0].api_key" // Field path (supports array syntax, e.g. "Providers[0].api_key")
target: string; target: string;
// 值来源(引用用户输入的 id或使用固定值 // Value source (references user input id, or uses fixed value)
value: string | any; // 如果是 string 且以 {{ 开头,则作为变量引用 value: string | any; // If string and starts with {{, treated as variable reference
// 条件(可选,满足条件时才应用此映射) // Condition (optional, apply this mapping only when condition is met)
when?: Condition | Condition[]; when?: Condition | Condition[];
} }
// 完整的预设文件格式 // Complete preset file format
export interface PresetFile { export interface PresetFile {
metadata?: PresetMetadata; metadata?: PresetMetadata;
config: PresetConfigSection; config: PresetConfigSection;
secrets?: { secrets?: {
// 敏感信息存储,格式:字段路径 -> 值 // Sensitive information storage, format: field path -> value
// 例如:{ "Providers[0].api_key": "sk-xxx", "APIKEY": "my-secret" } // Example: { "Providers[0].api_key": "sk-xxx", "APIKEY": "my-secret" }
[fieldPath: string]: string; [fieldPath: string]: string;
}; };
// === 动态配置系统 === // === Dynamic configuration system ===
// 配置输入schema // Configuration input schema
schema?: RequiredInput[]; schema?: RequiredInput[];
// 配置模板(使用变量替换) // Configuration template (uses variable replacement)
template?: TemplateConfig; template?: TemplateConfig;
// 配置映射(将用户输入映射到配置) // Configuration mappings (maps user input to configuration)
configMappings?: ConfigMapping[]; configMappings?: ConfigMapping[];
} }
// manifest.json 格式(压缩包内的文件) // manifest.json format (file inside ZIP archive)
export interface ManifestFile extends PresetMetadata, PresetConfigSection { export interface ManifestFile extends PresetMetadata, PresetConfigSection {
// === 动态配置系统 === // === Dynamic configuration system ===
schema?: RequiredInput[]; schema?: RequiredInput[];
template?: TemplateConfig; template?: TemplateConfig;
configMappings?: ConfigMapping[]; configMappings?: ConfigMapping[];
// === User configuration value storage ===
// User-filled configuration values are stored separately from original configuration
// Values collected during installation are stored here, applied at runtime
userValues?: UserInputValues;
} }
// 在线预设索引条目 // Online preset index entry
export interface PresetIndexEntry { export interface PresetIndexEntry {
id: string; // 唯一标识 id: string; // Unique identifier
name: string; // 显示名称 name: string; // Display name
description?: string; // 简短描述 description?: string; // Short description
version: string; // 最新版本 version: string; // Latest version
author?: string; // 作者 author?: string; // Author
downloads?: number; // 下载次数 downloads?: number; // Download count
stars?: number; // 点赞数 stars?: number; // Star count
tags?: string[]; // 标签 tags?: string[]; // Tags
url: string; // 下载地址 url: string; // Download address
checksum?: string; // SHA256 校验和 checksum?: string; // SHA256 checksum
ccrVersion?: string; // 兼容版本 ccrVersion?: string; // Compatible version
} }
// 在线预设仓库索引 // Online preset repository index
export interface PresetRegistry { export interface PresetRegistry {
version: string; // 索引格式版本 version: string; // Index format version
lastUpdated: string; // 最后更新时间 lastUpdated: string; // Last update time
presets: PresetIndexEntry[]; presets: PresetIndexEntry[];
} }
// 配置验证结果 // Configuration validation result
export interface ValidationResult { export interface ValidationResult {
valid: boolean; valid: boolean;
errors: string[]; errors: string[];
warnings: string[]; warnings: string[];
} }
// 合并策略枚举 // Merge strategy enumeration
export enum MergeStrategy { export enum MergeStrategy {
ASK = 'ask', // 交互式询问 ASK = 'ask', // Interactive prompt
OVERWRITE = 'overwrite', // 覆盖现有 OVERWRITE = 'overwrite', // Overwrite existing
MERGE = 'merge', // 智能合并 MERGE = 'merge', // Intelligent merge
SKIP = 'skip', // 跳过冲突项 SKIP = 'skip', // Skip conflicting items
} }
// 脱敏结果 // Sanitization result
export interface SanitizeResult { export interface SanitizeResult {
sanitizedConfig: any; sanitizedConfig: any;
requiredInputs: RequiredInput[]; requiredInputs: RequiredInput[];
sanitizedCount: number; sanitizedCount: number;
} }
// Preset 信息(用于列表展示) // Preset information (for list display)
export interface PresetInfo { export interface PresetInfo {
name: string; // 预设名称 name: string; // Preset name
version?: string; // 版本号 version?: string; // Version number
description?: string; // 描述 description?: string; // Description
author?: string; // 作者 author?: string; // Author
config: PresetConfigSection; config: PresetConfigSection;
} }

View File

@@ -18,7 +18,7 @@ import { Upload, Link, Trash2, Info, Download, CheckCircle2, AlertCircle, Loader
import { Toast } from "@/components/ui/toast"; import { Toast } from "@/components/ui/toast";
import { DynamicConfigForm } from "./preset/DynamicConfigForm"; import { DynamicConfigForm } from "./preset/DynamicConfigForm";
// Schema 类型 // Schema types
interface InputOption { interface InputOption {
label: string; label: string;
value: string | number | boolean; value: string | number | boolean;
@@ -87,6 +87,7 @@ interface PresetDetail extends PresetMetadata {
schema?: RequiredInput[]; schema?: RequiredInput[];
template?: any; template?: any;
configMappings?: any[]; configMappings?: any[];
userValues?: Record<string, any>;
} }
interface MarketPreset { interface MarketPreset {
@@ -126,7 +127,7 @@ export function Presets() {
navigate('/dashboard'); navigate('/dashboard');
}; };
// 加载市场预设 // Load market presets
const loadMarketPresets = async () => { const loadMarketPresets = async () => {
setMarketLoading(true); setMarketLoading(true);
try { try {
@@ -140,44 +141,51 @@ export function Presets() {
} }
}; };
// 从市场安装预设 // Install preset from market
const handleInstallFromMarket = async (preset: MarketPreset) => { const handleInstallFromMarket = async (preset: MarketPreset) => {
try { try {
setInstallingFromMarket(preset.id); setInstallingFromMarket(preset.id);
// 第一步:安装预设(解压到目录) // Step 1: Install preset (extract to directory)
await api.installPresetFromGitHub(preset.repo, preset.name); const installResult = await api.installPresetFromGitHub(preset.repo);
// 第二步:获取预设详情(检查是否需要配置) // Step 2: Get preset details (check if configuration is required)
try { try {
const detail = await api.getPreset(preset.name); const installedPresetName = installResult.presetName || preset.name;
const presetDetail: PresetDetail = { ...preset, ...detail }; const detail = await api.getPreset(installedPresetName);
const presetDetail: PresetDetail = { ...preset, ...detail, id: installedPresetName };
// 检查是否需要配置 // Check if configuration is required
if (detail.schema && detail.schema.length > 0) { if (detail.schema && detail.schema.length > 0) {
// 需要配置,打开配置对话框 // Configuration required, open configuration dialog
setSelectedPreset(presetDetail); setSelectedPreset(presetDetail);
// 初始化默认值 // Initialize form values: prefer saved userValues, otherwise use defaultValue
const initialValues: Record<string, any> = {}; const initialValues: Record<string, any> = {};
for (const input of detail.schema) { for (const input of detail.schema) {
initialValues[input.id] = input.defaultValue ?? ''; // Prefer saved values
if (detail.userValues && detail.userValues[input.id] !== undefined) {
initialValues[input.id] = detail.userValues[input.id];
} else {
// Otherwise use default value
initialValues[input.id] = input.defaultValue ?? '';
}
} }
setSecrets(initialValues); setSecrets(initialValues);
// 关闭市场对话框,打开详情对话框 // Close market dialog, open details dialog
setMarketDialogOpen(false); setMarketDialogOpen(false);
setDetailDialogOpen(true); setDetailDialogOpen(true);
setToast({ message: t('presets.preset_installed_config_required'), type: 'warning' }); setToast({ message: t('presets.preset_installed_config_required'), type: 'warning' });
} else { } else {
// 不需要配置,直接完成 // No configuration required, complete directly
setToast({ message: t('presets.preset_installed'), type: 'success' }); setToast({ message: t('presets.preset_installed'), type: 'success' });
setMarketDialogOpen(false); setMarketDialogOpen(false);
await loadPresets(); await loadPresets();
} }
} catch (error) { } catch (error) {
// 获取详情失败,但安装成功了,刷新列表 // Failed to get details, but installation succeeded, refresh list
console.error('Failed to get preset details after installation:', error); console.error('Failed to get preset details after installation:', error);
setToast({ message: t('presets.preset_installed'), type: 'success' }); setToast({ message: t('presets.preset_installed'), type: 'success' });
setMarketDialogOpen(false); setMarketDialogOpen(false);
@@ -191,21 +199,21 @@ export function Presets() {
} }
}; };
// 打开市场对话框时加载预设 // Load presets when opening market dialog
useEffect(() => { useEffect(() => {
if (marketDialogOpen && marketPresets.length === 0) { if (marketDialogOpen && marketPresets.length === 0) {
loadMarketPresets(); loadMarketPresets();
} }
}, [marketDialogOpen]); }, [marketDialogOpen]);
// 过滤市场预设 // Filter market 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())
); );
// 加载预设列表 // Load presets list
const loadPresets = async () => { const loadPresets = async () => {
try { try {
setLoading(true); setLoading(true);
@@ -223,18 +231,24 @@ export function Presets() {
loadPresets(); loadPresets();
}, []); }, []);
// 查看预设详情 // View preset details
const handleViewDetail = async (preset: PresetMetadata) => { const handleViewDetail = async (preset: PresetMetadata) => {
try { try {
const detail = await api.getPreset(preset.id); const detail = await api.getPreset(preset.id);
setSelectedPreset({ ...preset, ...detail }); setSelectedPreset({ ...preset, ...detail });
setDetailDialogOpen(true); setDetailDialogOpen(true);
// 初始化默认值 // 初始化表单值:优先使用已保存的 userValues否则使用 defaultValue
if (detail.schema && detail.schema.length > 0) { if (detail.schema && detail.schema.length > 0) {
const initialValues: Record<string, any> = {}; const initialValues: Record<string, any> = {};
for (const input of detail.schema) { for (const input of detail.schema) {
initialValues[input.id] = input.defaultValue ?? ''; // 优先使用已保存的值
if (detail.userValues && detail.userValues[input.id] !== undefined) {
initialValues[input.id] = detail.userValues[input.id];
} else {
// Otherwise use default value
initialValues[input.id] = input.defaultValue ?? '';
}
} }
setSecrets(initialValues); setSecrets(initialValues);
} }
@@ -266,38 +280,47 @@ export function Presets() {
: installUrl!.split('/').pop()!.replace('.ccrsets', '') : installUrl!.split('/').pop()!.replace('.ccrsets', '')
); );
// 第一步:安装预设(解压到目录) // Step 1: Install preset (extract to directory)
let installResult;
if (installMethod === 'url' && installUrl) { if (installMethod === 'url' && installUrl) {
await api.installPresetFromUrl(installUrl, presetName); installResult = await api.installPresetFromUrl(installUrl, presetName);
} else if (installMethod === 'file' && installFile) { } else if (installMethod === 'file' && installFile) {
await api.uploadPresetFile(installFile, presetName); installResult = await api.uploadPresetFile(installFile, presetName);
} else { } else {
return; return;
} }
// 第二步:获取预设详情(检查是否需要配置) // Step 2: Get preset details (check if configuration is required)
try { try {
const detail = await api.getPreset(presetName); // 使用服务器返回的实际预设名称
const actualPresetName = installResult?.presetName || presetName;
const detail = await api.getPreset(actualPresetName);
// 检查是否需要配置 // Check if configuration is required
if (detail.schema && detail.schema.length > 0) { if (detail.schema && detail.schema.length > 0) {
// 需要配置,打开配置对话框 // Configuration required, open configuration dialog
setSelectedPreset({ setSelectedPreset({
id: presetName, id: actualPresetName,
name: presetName, name: detail.name || actualPresetName,
version: detail.version || '1.0.0', version: detail.version || '1.0.0',
installed: true, installed: true,
...detail ...detail
}); });
// 初始化默认值 // Initialize form values: prefer saved userValues, otherwise use defaultValue
const initialValues: Record<string, any> = {}; const initialValues: Record<string, any> = {};
for (const input of detail.schema) { for (const input of detail.schema) {
initialValues[input.id] = input.defaultValue ?? ''; // Prefer saved values
if (detail.userValues && detail.userValues[input.id] !== undefined) {
initialValues[input.id] = detail.userValues[input.id];
} else {
// Otherwise use default value
initialValues[input.id] = input.defaultValue ?? '';
}
} }
setSecrets(initialValues); setSecrets(initialValues);
// 关闭安装对话框,打开详情对话框 // Close installation dialog, open details dialog
setInstallDialogOpen(false); setInstallDialogOpen(false);
setInstallUrl(''); setInstallUrl('');
setInstallFile(null); setInstallFile(null);
@@ -306,7 +329,7 @@ export function Presets() {
setToast({ message: t('presets.preset_installed_config_required'), type: 'warning' }); setToast({ message: t('presets.preset_installed_config_required'), type: 'warning' });
} else { } else {
// 不需要配置,直接完成 // No configuration required, complete directly
setToast({ message: t('presets.preset_installed'), type: 'success' }); setToast({ message: t('presets.preset_installed'), type: 'success' });
setInstallDialogOpen(false); setInstallDialogOpen(false);
setInstallUrl(''); setInstallUrl('');
@@ -315,7 +338,7 @@ export function Presets() {
await loadPresets(); await loadPresets();
} }
} catch (error) { } catch (error) {
// 获取详情失败,但安装成功了,刷新列表 // Failed to get details, but installation succeeded, refresh list
console.error('Failed to get preset details after installation:', error); console.error('Failed to get preset details after installation:', error);
setToast({ message: t('presets.preset_installed'), type: 'success' }); setToast({ message: t('presets.preset_installed'), type: 'success' });
setInstallDialogOpen(false); setInstallDialogOpen(false);
@@ -332,20 +355,24 @@ export function Presets() {
} }
}; };
// 应用预设(配置敏感信息) // Apply preset (configure sensitive information)
const handleApplyPreset = async (values?: Record<string, any>) => { const handleApplyPreset = async (values?: Record<string, any>) => {
try { try {
setIsApplying(true); setIsApplying(true);
// 使用传入的values或现有的secrets // Use passed values or existing secrets
const inputValues = values || secrets; const inputValues = values || secrets;
// 验证所有必填项都已填写 // Verify all required fields are filled
if (selectedPreset?.schema && selectedPreset.schema.length > 0) { if (selectedPreset?.schema && selectedPreset.schema.length > 0) {
// 验证在 DynamicConfigForm 中已完成 // Validation completed in DynamicConfigForm
// 这里只做简单检查 // 这里只做简单检查(对于 confirm 类型false 是有效值)
for (const input of selectedPreset.schema) { for (const input of selectedPreset.schema) {
if (input.required !== false && !inputValues[input.id]) { const value = inputValues[input.id];
const isEmpty = value === undefined || value === null || value === '' ||
(Array.isArray(value) && value.length === 0);
if (input.required !== false && isEmpty) {
setToast({ message: t('presets.please_fill_field', { field: input.label || input.id }), type: 'warning' }); setToast({ message: t('presets.please_fill_field', { field: input.label || input.id }), type: 'warning' });
setIsApplying(false); setIsApplying(false);
return; return;
@@ -353,11 +380,11 @@ export function Presets() {
} }
} }
await api.applyPreset(selectedPreset!.name, inputValues); await api.applyPreset(selectedPreset!.id, inputValues);
setToast({ message: t('presets.preset_applied'), type: 'success' }); setToast({ message: t('presets.preset_applied'), type: 'success' });
setDetailDialogOpen(false); setDetailDialogOpen(false);
setSecrets({}); setSecrets({});
// 刷新预设列表 // Refresh presets list
await loadPresets(); await loadPresets();
} catch (error: any) { } catch (error: any) {
console.error('Failed to apply preset:', error); console.error('Failed to apply preset:', error);
@@ -367,7 +394,7 @@ export function Presets() {
} }
}; };
// 删除预设 // Delete preset
const handleDelete = async () => { const handleDelete = async () => {
if (!presetToDelete) return; if (!presetToDelete) return;
@@ -576,7 +603,7 @@ export function Presets() {
</div> </div>
)} )}
{/* 配置表单 */} {/* Configuration form */}
{selectedPreset?.schema && selectedPreset.schema.length > 0 && ( {selectedPreset?.schema && selectedPreset.schema.length > 0 && (
<div className="mt-6"> <div className="mt-6">
<h4 className="font-medium text-sm mb-4">{t('presets.required_information')}</h4> <h4 className="font-medium text-sm mb-4">{t('presets.required_information')}</h4>
@@ -591,11 +618,6 @@ export function Presets() {
</div> </div>
)} )}
</div> </div>
<DialogFooter>
<Button variant="outline" onClick={() => setDetailDialogOpen(false)}>
{t('presets.close')}
</Button>
</DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>

View File

@@ -1,4 +1,5 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
@@ -11,9 +12,9 @@ import {
SelectValue, SelectValue,
} from '@/components/ui/select'; } from '@/components/ui/select';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
import { CheckCircle2, Loader2 } from 'lucide-react'; import { Loader2 } from 'lucide-react';
// 类型定义 // Type definitions
interface InputOption { interface InputOption {
label: string; label: string;
value: string | number | boolean; value: string | number | boolean;
@@ -77,11 +78,12 @@ export function DynamicConfigForm({
isSubmitting = false, isSubmitting = false,
initialValues = {}, initialValues = {},
}: DynamicConfigFormProps) { }: DynamicConfigFormProps) {
const { t } = useTranslation();
const [values, setValues] = useState<Record<string, any>>(initialValues); const [values, setValues] = useState<Record<string, any>>(initialValues);
const [errors, setErrors] = useState<Record<string, string>>({}); const [errors, setErrors] = useState<Record<string, string>>({});
const [visibleFields, setVisibleFields] = useState<Set<string>>(new Set()); const [visibleFields, setVisibleFields] = useState<Set<string>>(new Set());
// 计算可见字段 // Calculate visible fields
useEffect(() => { useEffect(() => {
const updateVisibility = () => { const updateVisibility = () => {
const visible = new Set<string>(); const visible = new Set<string>();
@@ -98,7 +100,7 @@ export function DynamicConfigForm({
updateVisibility(); updateVisibility();
}, [values, schema]); }, [values, schema]);
// 评估条件 // Evaluate condition
const evaluateCondition = (condition: Condition): boolean => { const evaluateCondition = (condition: Condition): boolean => {
const actualValue = values[condition.field]; const actualValue = values[condition.field];
@@ -132,7 +134,7 @@ export function DynamicConfigForm({
} }
}; };
// 判断字段是否应该显示 // Determine if field should be displayed
const shouldShowField = (field: RequiredInput): boolean => { const shouldShowField = (field: RequiredInput): boolean => {
if (!field.when) { if (!field.when) {
return true; return true;
@@ -142,7 +144,7 @@ export function DynamicConfigForm({
return conditions.every(condition => evaluateCondition(condition)); return conditions.every(condition => evaluateCondition(condition));
}; };
// 获取选项列表 // Get options list
const getOptions = (field: RequiredInput): InputOption[] => { const getOptions = (field: RequiredInput): InputOption[] => {
if (!field.options) { if (!field.options) {
return []; return [];
@@ -197,13 +199,13 @@ export function DynamicConfigForm({
return []; return [];
}; };
// 更新字段值 // Update field value
const updateValue = (fieldId: string, value: any) => { const updateValue = (fieldId: string, value: any) => {
setValues((prev) => ({ setValues((prev) => ({
...prev, ...prev,
[fieldId]: value, [fieldId]: value,
})); }));
// 清除该字段的错误 // Clear errors for this field
setErrors((prev) => { setErrors((prev) => {
const newErrors = { ...prev }; const newErrors = { ...prev };
delete newErrors[fieldId]; delete newErrors[fieldId];
@@ -211,44 +213,44 @@ export function DynamicConfigForm({
}); });
}; };
// 验证单个字段 // Validate single field
const validateField = (field: RequiredInput): string | null => { const validateField = (field: RequiredInput): string | null => {
const value = values[field.id]; const value = values[field.id];
const fieldName = field.label || field.id;
// 检查必填 // Check required (for confirm type, false is a valid value)
if (field.required !== false && (value === undefined || value === null || value === '')) { const isEmpty = value === undefined || value === null || value === '' ||
return `${field.label || field.id} is required`; (Array.isArray(value) && value.length === 0);
if (field.required !== false && isEmpty) {
return t('presets.form.field_required', { field: fieldName });
} }
if (!value && field.required === false) { // Type check
return null; if (field.type === 'number' && value !== '' && isNaN(Number(value))) {
} return t('presets.form.must_be_number', { field: fieldName });
// 类型检查
if (field.type === 'number' && isNaN(Number(value))) {
return `${field.label || field.id} must be a number`;
} }
if (field.type === 'number') { if (field.type === 'number') {
const numValue = Number(value); const numValue = Number(value);
if (field.min !== undefined && numValue < field.min) { if (field.min !== undefined && numValue < field.min) {
return `${field.label || field.id} must be at least ${field.min}`; return t('presets.form.must_be_at_least', { field: fieldName, min: field.min });
} }
if (field.max !== undefined && numValue > field.max) { if (field.max !== undefined && numValue > field.max) {
return `${field.label || field.id} must be at most ${field.max}`; return t('presets.form.must_be_at_most', { field: fieldName, max: field.max });
} }
} }
// 自定义验证器 // Custom validator
if (field.validator) { if (field.validator && value !== '') {
if (field.validator instanceof RegExp) { if (field.validator instanceof RegExp) {
if (!field.validator.test(String(value))) { if (!field.validator.test(String(value))) {
return `${field.label || field.id} format is invalid`; return t('presets.form.format_invalid', { field: fieldName });
} }
} else if (typeof field.validator === 'string') { } else if (typeof field.validator === 'string') {
const regex = new RegExp(field.validator); const regex = new RegExp(field.validator);
if (!regex.test(String(value))) { if (!regex.test(String(value))) {
return `${field.label || field.id} format is invalid`; return t('presets.form.format_invalid', { field: fieldName });
} }
} }
} }
@@ -256,11 +258,11 @@ export function DynamicConfigForm({
return null; return null;
}; };
// 提交表单 // Submit form
const handleSubmit = (e: React.FormEvent) => { const handleSubmit = (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
// 验证所有可见字段 // Validate all visible fields
const newErrors: Record<string, string> = {}; const newErrors: Record<string, string> = {};
for (const field of schema) { for (const field of schema) {
@@ -338,7 +340,7 @@ export function DynamicConfigForm({
disabled={isSubmitting} disabled={isSubmitting}
> >
<SelectTrigger id={`field-${field.id}`}> <SelectTrigger id={`field-${field.id}`}>
<SelectValue placeholder={field.placeholder || `Select ${label}`} /> <SelectValue placeholder={field.placeholder || t('presets.form.select', { label })} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{getOptions(field).map((option) => ( {getOptions(field).map((option) => (
@@ -432,19 +434,16 @@ export function DynamicConfigForm({
onClick={onCancel} onClick={onCancel}
disabled={isSubmitting} disabled={isSubmitting}
> >
Cancel {t('app.cancel')}
</Button> </Button>
<Button type="submit" disabled={isSubmitting}> <Button type="submit" disabled={isSubmitting}>
{isSubmitting ? ( {isSubmitting ? (
<> <>
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> <Loader2 className="mr-2 h-4 w-4 animate-spin" />
Applying... {t('presets.form.saving')}
</> </>
) : ( ) : (
<> t('app.save')
<CheckCircle2 className="mr-2 h-4 w-4" />
Apply
</>
)} )}
</Button> </Button>
</div> </div>

View File

@@ -43,12 +43,12 @@ export function Toast({ message, type, onClose }: ToastProps) {
}; };
return ( return (
<div className={`fixed top-4 right-4 z-50 flex items-center justify-between p-4 rounded-lg border shadow-lg ${getBackgroundColor()} transition-all duration-300 ease-in-out`}> <div className={`fixed top-4 right-4 z-[100] flex items-center justify-between p-4 rounded-lg border shadow-lg ${getBackgroundColor()} transition-all duration-300 ease-in-out`}>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
{getIcon()} {getIcon()}
<span className="text-sm font-medium">{message}</span> <span className="text-sm font-medium">{message}</span>
</div> </div>
<button <button
onClick={onClose} onClick={onClose}
className="ml-4 text-gray-500 hover:text-gray-700 focus:outline-none" className="ml-4 text-gray-500 hover:text-gray-700 focus:outline-none"
> >

View File

@@ -285,6 +285,17 @@
"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 file",
"please_provide_url": "Please provide a preset URL" "please_provide_url": "Please provide a preset URL",
"form": {
"field_required": "{{field}} is required",
"must_be_number": "{{field}} must be a number",
"must_be_at_least": "{{field}} must be at least {{min}}",
"must_be_at_most": "{{field}} must be at most {{max}}",
"format_invalid": "{{field}} format is invalid",
"select": "Select {{label}}",
"applying": "Applying...",
"apply": "Apply",
"cancel": "Cancel"
}
} }
} }

View File

@@ -285,6 +285,17 @@
"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": "请提供预设 URL",
"form": {
"field_required": "{{field}} 为必填项",
"must_be_number": "{{field}} 必须是数字",
"must_be_at_least": "{{field}} 至少为 {{min}}",
"must_be_at_most": "{{field}} 最多为 {{max}}",
"format_invalid": "{{field}} 格式无效",
"select": "选择 {{label}}",
"applying": "应用中...",
"apply": "应用",
"cancel": "取消"
}
} }
} }