diff --git a/docs/docs/cli/config/basic.md b/docs/docs/cli/config/basic.md index 710f2c8..187f86a 100644 --- a/docs/docs/cli/config/basic.md +++ b/docs/docs/cli/config/basic.md @@ -1,44 +1,48 @@ -# CLI 基础配置 +--- +title: Basic Configuration +--- -CLI 使用与 Server 相同的配置文件:`~/.claude-code-router/config.json` +# Basic Configuration -## 配置文件位置 +CLI uses the same configuration file as Server: `~/.claude-code-router/config.json` + +## Configuration File Location ```bash ~/.claude-code-router/config.json ``` -## 快速配置 +## Quick Configuration -使用交互式命令配置: +Use interactive command to configure: ```bash ccr model ``` -这将引导你完成: -1. 选择 LLM 提供商 -2. 配置 API Key -3. 选择模型 -4. 设置路由规则 +This will guide you through: +1. Select LLM provider +2. Configure API Key +3. Select model +4. Set routing rules -## 手动配置 +## Manual Configuration -### 编辑配置文件 +### Edit Configuration File ```bash -# 打开配置文件 +# Open configuration file nano ~/.claude-code-router/config.json ``` -### 最小配置示例 +### Minimal Configuration Example ```json5 { - // API 密钥(可选,用于保护服务) + // API key (optional, used to protect service) "APIKEY": "your-api-key-here", - // LLM 提供商 + // LLM providers "Providers": [ { "name": "openai", @@ -48,55 +52,55 @@ nano ~/.claude-code-router/config.json } ], - // 默认路由 + // Default routing "Router": { "default": "openai,gpt-4" } } ``` -## 环境变量 +## Environment Variables -配置支持环境变量插值: +Configuration supports environment variable interpolation: ```json5 { "Providers": [ { - "apiKey": "$OPENAI_API_KEY" // 从环境变量读取 + "apiKey": "$OPENAI_API_KEY" // Read from environment variable } ] } ``` -在 `.bashrc` 或 `.zshrc` 中设置: +Set in `.bashrc` or `.zshrc`: ```bash export OPENAI_API_KEY="sk-..." export ANTHROPIC_API_KEY="sk-ant-..." ``` -## 常用配置项 +## Common Configuration Options -### HOST 和 PORT +### HOST and PORT ```json5 { - "HOST": "127.0.0.1", // 监听地址 - "PORT": 3456 // 监听端口 + "HOST": "127.0.0.1", // Listen address + "PORT": 3456 // Listen port } ``` -### 日志配置 +### Logging Configuration ```json5 { - "LOG": true, // 启用日志 - "LOG_LEVEL": "info" // 日志级别 + "LOG": true, // Enable logging + "LOG_LEVEL": "info" // Log level } ``` -### 路由配置 +### Routing Configuration ```json5 { @@ -109,41 +113,41 @@ export ANTHROPIC_API_KEY="sk-ant-..." } ``` -## 配置验证 +## Configuration Validation -配置文件会自动验证。常见错误: +Configuration file is automatically validated. Common errors: -- **缺少 Providers**:必须至少配置一个提供商 -- **API Key 缺失**:如果配置了 Providers,必须提供 API Key -- **模型不存在**:确保模型在提供商的 models 列表中 +- **Missing Providers**: Must configure at least one provider +- **Missing API Key**: If Providers are configured, must provide API Key +- **Model doesn't exist**: Ensure model is in provider's models list -## 配置备份 +## Configuration Backup -每次更新配置时会自动备份: +Configuration is automatically backed up on each update: ``` ~/.claude-code-router/config.backup.{timestamp}.json ``` -## 重新加载配置 +## Reload Configuration -修改配置后需要重启服务: +Restart service after modifying configuration: ```bash ccr restart ``` -## 查看当前配置 +## View Current Configuration ```bash -# 通过 API 查看 +# View via API curl http://localhost:3456/api/config -# 或查看配置文件 +# Or view configuration file cat ~/.claude-code-router/config.json ``` -## 示例配置 +## Example Configurations ### OpenAI @@ -181,7 +185,7 @@ cat ~/.claude-code-router/config.json } ``` -### 多提供商 +### Multiple Providers ```json5 { diff --git a/docs/docs/cli/config/project-level.md b/docs/docs/cli/config/project-level.md index 9b2f402..880c82b 100644 --- a/docs/docs/cli/config/project-level.md +++ b/docs/docs/cli/config/project-level.md @@ -1,18 +1,22 @@ -# 项目级配置 +--- +title: Project-Level Configuration +--- -除了全局配置,`ccr` 还支持为特定项目设置不同的路由规则。 +# Project-Level Configuration -## 项目配置文件 +In addition to global configuration, `ccr` also supports setting different routing rules for specific projects. -项目配置文件位于: +## Project Configuration File + +Project configuration file is located at: ``` ~/.claude/projects//claude-code-router.json ``` -其中 `` 是 Claude Code 项目的唯一标识符。 +Where `` is the unique identifier of the Claude Code project. -## 项目配置结构 +## Project Configuration Structure ```json5 { @@ -23,28 +27,28 @@ } ``` -## 查找项目 ID +## Finding Project ID -### 方法一:使用 CLI +### Method 1: Using CLI ```bash -# 在项目目录中运行 +# Run in project directory ccr status ``` -输出会显示当前项目 ID: +Output will show current project ID: ``` Project: my-project (abc123def456) ``` -### 方法二:查看 Claude Code 配置 +### Method 2: Check Claude Code Configuration ```bash cat ~/.claude.json ``` -找到你的项目 ID: +Find your project ID: ```json { @@ -57,15 +61,15 @@ cat ~/.claude.json } ``` -## 创建项目配置 +## Creating Project Configuration -### 手动创建 +### Manual Creation ```bash -# 创建项目配置目录 +# Create project configuration directory mkdir -p ~/.claude/projects/abc123def456 -# 创建配置文件 +# Create configuration file cat > ~/.claude/projects/abc123def456/claude-code-router.json << 'EOF' { "Router": { @@ -76,29 +80,29 @@ cat > ~/.claude/projects/abc123def456/claude-code-router.json << 'EOF' EOF ``` -### 使用 ccr model 命令 +### Using ccr model Command ```bash -# 在项目目录中运行 +# Run in project directory cd /path/to/your/project ccr model --project ``` -## 配置优先级 +## Configuration Priority -路由配置的优先级(从高到低): +Routing configuration priority (from high to low): -1. **自定义路由函数** (`CUSTOM_ROUTER_PATH`) -2. **项目级配置** (`~/.claude/projects//claude-code-router.json`) -3. **全局配置** (`~/.claude-code-router/config.json`) -4. **内置路由规则** +1. **Custom routing function** (`CUSTOM_ROUTER_PATH`) +2. **Project-level configuration** (`~/.claude/projects//claude-code-router.json`) +3. **Global configuration** (`~/.claude-code-router/config.json`) +4. **Built-in routing rules** -## 使用场景 +## Use Cases -### 场景一:不同项目使用不同模型 +### Scenario 1: Different Projects Use Different Models ```json5 -// Web 项目使用 GPT-4 +// Web project uses GPT-4 ~/.claude/projects/web-project-id/claude-code-router.json: { "Router": { @@ -106,7 +110,7 @@ ccr model --project } } -// AI 项目使用 Claude +// AI project uses Claude ~/.claude/projects/ai-project-id/claude-code-router.json: { "Router": { @@ -115,7 +119,7 @@ ccr model --project } ``` -### 场景二:测试项目使用低成本模型 +### Scenario 2: Test Projects Use Low-Cost Models ```json5 ~/.claude/projects/test-project-id/claude-code-router.json: @@ -127,7 +131,7 @@ ccr model --project } ``` -### 场景三:长上下文项目 +### Scenario 3: Long Context Projects ```json5 ~/.claude/projects/long-context-project-id/claude-code-router.json: @@ -139,29 +143,29 @@ ccr model --project } ``` -## 验证项目配置 +## Verify Project Configuration ```bash -# 查看当前项目使用的路由 +# View routing used by current project ccr status -# 查看日志确认路由决策 +# Check logs to confirm routing decisions tail -f ~/.claude-code-router/claude-code-router.log ``` -## 删除项目配置 +## Delete Project Configuration ```bash rm ~/.claude/projects//claude-code-router.json ``` -删除后会回退到全局配置。 +After deletion, falls back to global configuration. -## 完整示例 +## Complete Example -假设你有两个项目: +Assume you have two projects: -### 全局配置(`~/.claude-code-router/config.json`) +### Global Configuration (`~/.claude-code-router/config.json`) ```json5 { @@ -186,7 +190,7 @@ rm ~/.claude/projects//claude-code-router.json } ``` -### Web 项目配置 +### Web Project Configuration ```json5 { @@ -196,7 +200,7 @@ rm ~/.claude/projects//claude-code-router.json } ``` -### AI 项目配置 +### AI Project Configuration ```json5 { @@ -207,7 +211,7 @@ rm ~/.claude/projects//claude-code-router.json } ``` -这样: -- Web 项目使用 GPT-4 -- AI 项目使用 Claude -- 所有项目的后台任务使用 GPT-3.5-turbo(继承全局配置) +This way: +- Web project uses GPT-4 +- AI project uses Claude +- All projects' background tasks use GPT-3.5-turbo (inherit global configuration) diff --git a/docs/docs/cli/intro.md b/docs/docs/cli/intro.md index b50f69c..87d83eb 100644 --- a/docs/docs/cli/intro.md +++ b/docs/docs/cli/intro.md @@ -1,83 +1,87 @@ -# CLI 简介 +--- +title: CLI Introduction +--- -Claude Code Router CLI (`ccr`) 是一个命令行工具,用于管理和控制 Claude Code Router 服务。 +# CLI Introduction -## 功能概述 +Claude Code Router CLI (`ccr`) is a command-line tool for managing and controlling the Claude Code Router service. -`ccr` 提供以下功能: +## Feature Overview -- **服务管理**:启动、停止、重启服务 -- **配置管理**:交互式配置模型选择 -- **状态查看**:查看服务运行状态 -- **代码执行**:直接执行 `claude` 命令 -- **环境集成**:输出环境变量用于 shell 集成 -- **Web UI**:打开 Web 管理界面 -- **状态栏**:集成到编辑器状态栏 +`ccr` provides the following functionality: -## 安装 +- **Service Management**: Start, stop, restart service +- **Configuration Management**: Interactive model selection configuration +- **Status Viewing**: View service running status +- **Code Execution**: Directly execute `claude` command +- **Environment Integration**: Output environment variables for shell integration +- **Web UI**: Open Web management interface +- **Status Bar**: Integration into editor status bar + +## Installation ```bash npm install -g @musistudio/claude-code-router-cli ``` -或使用项目别名: +Or using project alias: ```bash npm install -g claude-code-router ``` -## 基本使用 +## Basic Usage -### 启动服务 +### Start Service ```bash ccr start ``` -### 查看状态 +### View Status ```bash ccr status ``` -### 停止服务 +### Stop Service ```bash ccr stop ``` -### 查看模型 +### View Models ```bash ccr model ``` -## 与 Claude Code 集成 +## Integration with Claude Code -`ccr` 可以与 Claude Code 无缝集成,将请求路由到你选择的 LLM 提供商。 +`ccr` integrates seamlessly with Claude Code to route requests to your chosen LLM provider. -### 方式一:设置 API 地址 +### Method 1: Set API Address ```bash export ANTHROPIC_BASE_URL="http://localhost:3456/v1" export ANTHROPIC_API_KEY="your-api-key" ``` -### 方式二:使用 activate 命令 +### Method 2: Use activate Command ```bash eval "$(ccr activate)" ``` -## 配置文件 +## Configuration File -`ccr` 使用与 Server 相同的配置文件:`~/.claude-code-router/config.json` +`ccr` uses the same configuration file as Server: `~/.claude-code-router/config.json` -配置一次,CLI 和 Server 都会使用。 +Configure once, and both CLI and Server will use it. -## 下一步 +## Next Steps -- [安装指南](/docs/cli/installation) - 详细安装说明 -- [快速开始](/docs/cli/quick-start) - 5 分钟上手 -- [命令参考](/docs/category/cli-commands) - 完整命令列表 -- [配置说明](/docs/category/cli-config) - 配置文件详解 +- [Installation Guide](/docs/cli/installation) - Detailed installation instructions +- [Quick Start](/docs/cli/quick-start) - Get started in 5 minutes +- [Command Reference](/docs/category/cli-commands) - Complete command list +- [Configuration Guide](/docs/category/cli-config) - Configuration file details diff --git a/docs/docs/server/advanced/presets.md b/docs/docs/server/advanced/presets.md index 0c513b7..ce7cf77 100644 --- a/docs/docs/server/advanced/presets.md +++ b/docs/docs/server/advanced/presets.md @@ -10,6 +10,216 @@ Use predefined configurations for quick setup. Presets are pre-configured settings that include provider configurations, routing rules, and transformers optimized for specific use cases. +## Dynamic Configuration System (v2.0+) + +CCR 2.0 introduces a powerful dynamic configuration system that supports: + +- **Multiple Input Types**: Selectors, multi-select, confirm boxes, text input, number input, etc. +- **Conditional Logic**: Dynamically show/hide configuration fields based on user input +- **Variable References**: Configuration fields can reference each other +- **Dynamic Options**: Option lists can be dynamically generated from preset configuration or user input + +### Schema Field Types + +| Type | Description | Example | +|------|-------------|---------| +| `password` | Password input (hidden) | API Key | +| `input` | Single-line text input | Base URL | +| `number` | Number input | Max tokens | +| `select` | Single-select dropdown | Choose Provider | +| `multiselect` | Multi-select | Enable features | +| `confirm` | Confirmation box | Use proxy | +| `editor` | Multi-line text editor | Custom config | + +### Condition Operators + +| Operator | Description | Example | +|----------|-------------|---------| +| `eq` | Equals | `{"field": "provider", "operator": "eq", "value": "openai"}` | +| `ne` | Not equals | `{"field": "advanced", "operator": "ne", "value": true}` | +| `in` | In (array) | `{"field": "feature", "operator": "in", "value": ["a", "b"]}` | +| `nin` | Not in (array) | `{"field": "type", "operator": "nin", "value": ["x", "y"]}` | +| `exists` | Field exists | `{"field": "apiKey", "operator": "exists"}` | +| `gt/lt/gte/lte` | Greater/less than (or equal) | For number comparisons | + +### Dynamic Options Types + +#### static - Static Options +```json +"options": { + "type": "static", + "options": [ + {"label": "Option 1", "value": "value1"}, + {"label": "Option 2", "value": "value2"} + ] +} +``` + +#### providers - Extract from Providers Configuration +```json +"options": { + "type": "providers" +} +``` +Automatically extracts names from the `Providers` array as options. + +#### models - Extract from Specified Provider's Models +```json +"options": { + "type": "models", + "providerField": "{{selectedProvider}}" +} +``` +Dynamically displays models based on the user-selected provider. + +### Template Variables + +Use `{{variableName}}` syntax to reference user input in the template: + +```json +"template": { + "Providers": [ + { + "name": "{{providerName}}", + "api_key": "{{apiKey}}" + } + ] +} +``` + +### Configuration Mappings + +For complex configuration needs, use `configMappings` to precisely control value placement: + +```json +"configMappings": [ + { + "target": "Providers[0].api_key", + "value": "{{apiKey}}" + }, + { + "target": "PROXY_URL", + "value": "{{proxyUrl}}", + "when": { + "field": "useProxy", + "operator": "eq", + "value": true + } + } +] +``` + +### Complete Example + +```json +{ + "name": "multi-provider-example", + "version": "1.0.0", + "description": "Multi-provider configuration example - Switch between OpenAI and DeepSeek", + "author": "CCR Team", + "keywords": ["openai", "deepseek", "multi-provider"], + "ccrVersion": "2.0.0", + "schema": [ + { + "id": "primaryProvider", + "type": "select", + "label": "Primary Provider", + "prompt": "Select your primary LLM provider", + "options": { + "type": "static", + "options": [ + { + "label": "OpenAI", + "value": "openai", + "description": "Use OpenAI's GPT models" + }, + { + "label": "DeepSeek", + "value": "deepseek", + "description": "Use DeepSeek's cost-effective models" + } + ] + }, + "required": true, + "defaultValue": "openai" + }, + { + "id": "apiKey", + "type": "password", + "label": "API Key", + "prompt": "Enter your API Key", + "placeholder": "sk-...", + "required": true + }, + { + "id": "defaultModel", + "type": "select", + "label": "Default Model", + "prompt": "Select the default model to use", + "options": { + "type": "static", + "options": [ + {"label": "GPT-4o", "value": "gpt-4o"}, + {"label": "GPT-4o-mini", "value": "gpt-4o-mini"} + ] + }, + "required": true, + "defaultValue": "gpt-4o", + "when": { + "field": "primaryProvider", + "operator": "eq", + "value": "openai" + } + }, + { + "id": "enableProxy", + "type": "confirm", + "label": "Enable Proxy", + "prompt": "Access API through a proxy?", + "defaultValue": false + }, + { + "id": "proxyUrl", + "type": "input", + "label": "Proxy URL", + "prompt": "Enter proxy server address", + "placeholder": "http://127.0.0.1:7890", + "required": true, + "when": { + "field": "enableProxy", + "operator": "eq", + "value": true + } + } + ], + "template": { + "Providers": [ + { + "name": "{{primaryProvider}}", + "api_base_url": "https://api.openai.com/v1", + "api_key": "{{apiKey}}", + "models": ["{{defaultModel}}"] + } + ], + "Router": { + "default": "{{primaryProvider}}/{{defaultModel}}" + }, + "PROXY_URL": "{{proxyUrl}}" + }, + "configMappings": [ + { + "target": "PROXY_URL", + "value": "{{proxyUrl}}", + "when": { + "field": "enableProxy", + "operator": "eq", + "value": true + } + } + ] +} +``` + ## Available Presets ### Development @@ -49,7 +259,44 @@ ccr preset list ## Creating Custom Presets -You can create custom presets by saving your configuration and reloading it later: +### Using the Dynamic Configuration System + +Create a preset file with `schema` and `template`: + +```bash +# Create preset directory +mkdir -p ~/.claude-code-router/presets/my-preset + +# Create manifest.json +cat > ~/.claude-code-router/presets/my-preset/manifest.json << 'EOF' +{ + "name": "my-preset", + "version": "1.0.0", + "description": "My custom preset", + "schema": [ + { + "id": "apiKey", + "type": "password", + "label": "API Key", + "required": true + } + ], + "template": { + "Providers": [ + { + "name": "my-provider", + "api_key": "{{apiKey}}" + } + ] + } +} +EOF + +# Apply preset (will prompt for API Key) +ccr my-preset +``` + +You can also save and reload your current configuration: ```bash # Save current configuration as a preset @@ -59,6 +306,74 @@ ccr preset save my-preset ccr preset apply my-preset ``` +## Preset Management + +### List All Presets + +```bash +ccr preset list +``` + +Output example: + +``` +Available presets: + development - Development optimized configuration + research - Research optimized configuration + balanced - Balanced configuration + my-preset - Custom preset +``` + +### Apply a Preset + +```bash +ccr preset apply +``` + +The server will automatically restart to load the new configuration. + +### Delete a Preset + +```bash +ccr preset delete +``` + +## Preset File Location + +Presets are stored in: + +``` +~/.claude-code-router/presets/ +``` + +Each preset is a directory containing a `manifest.json` file. + +## Exporting and Importing Presets + +### Export Current Configuration + +```bash +ccr config show > my-config.json +``` + +### Import Configuration + +```bash +ccr config edit +# Then paste the imported configuration +``` + +## Best Practices + +1. **Use Dynamic Configuration**: Use the schema system for configuration items that require user input +2. **Provide Defaults**: Set reasonable defaults for optional fields +3. **Conditional Display**: Use `when` conditions to avoid unnecessary inputs +4. **Clear Labels**: Provide clear `label` and `prompt` for each field +5. **Validate Input**: Use `validator` to ensure input validity +6. **Version Control**: Keep commonly used presets in version control +7. **Document**: Add descriptions and version info for custom presets + ## Next Steps - [CLI Reference](/docs/cli/start) - Complete CLI command reference +- [Configuration](/docs/config/basic) - Detailed configuration guide diff --git a/docs/docs/server/api/config-api.md b/docs/docs/server/api/config-api.md index e0baf88..dc11108 100644 --- a/docs/docs/server/api/config-api.md +++ b/docs/docs/server/api/config-api.md @@ -1,17 +1,21 @@ -# 配置 API +--- +title: Configuration API +--- + +# Configuration API ## GET /api/config -获取当前服务器配置。 +Get current server configuration. -### 请求示例 +### Request Example ```bash curl http://localhost:3456/api/config \ -H "x-api-key: your-api-key" ``` -### 响应示例 +### Response Example ```json { @@ -37,9 +41,9 @@ curl http://localhost:3456/api/config \ ## POST /api/config -更新服务器配置。更新后会自动备份旧配置。 +Update server configuration. Old configuration is automatically backed up before updating. -### 请求示例 +### Request Example ```bash curl -X POST http://localhost:3456/api/config \ @@ -62,19 +66,19 @@ curl -X POST http://localhost:3456/api/config \ }' ``` -### 配置对象结构 +### Configuration Object Structure -#### 基础配置 +#### Basic Configuration -| 字段 | 类型 | 必需 | 说明 | -|------|------|------|------| -| `HOST` | string | 否 | 监听地址(默认 127.0.0.1) | -| `PORT` | integer | 否 | 监听端口(默认 3456) | -| `APIKEY` | string | 否 | API 密钥 | -| `LOG` | boolean | 否 | 是否启用日志(默认 true) | -| `LOG_LEVEL` | string | 否 | 日志级别(debug/info/warn/error) | +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `HOST` | string | No | Listen address (default 127.0.0.1) | +| `PORT` | integer | No | Listen port (default 3456) | +| `APIKEY` | string | No | API key | +| `LOG` | boolean | No | Enable logging (default true) | +| `LOG_LEVEL` | string | No | Log level (debug/info/warn/error) | -#### Providers 配置 +#### Providers Configuration ```json { @@ -89,14 +93,14 @@ curl -X POST http://localhost:3456/api/config \ } ``` -| 字段 | 类型 | 必需 | 说明 | -|------|------|------|------| -| `name` | string | 是 | 提供商名称 | -| `baseUrl` | string | 是 | API 基础 URL | -| `apiKey` | string | 是 | API 密钥 | -| `models` | array | 是 | 支持的模型列表 | +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `name` | string | Yes | Provider name | +| `baseUrl` | string | Yes | API base URL | +| `apiKey` | string | Yes | API key | +| `models` | array | Yes | List of supported models | -#### Router 配置 +#### Router Configuration ```json { @@ -114,7 +118,7 @@ curl -X POST http://localhost:3456/api/config \ } ``` -#### Transformers 配置 +#### Transformers Configuration ```json { @@ -129,9 +133,9 @@ curl -X POST http://localhost:3456/api/config \ } ``` -### 响应示例 +### Response Example -成功: +Success: ```json { @@ -140,28 +144,28 @@ curl -X POST http://localhost:3456/api/config \ } ``` -### 配置备份 +### Configuration Backup -每次更新配置时,旧配置会自动备份到: +Every time configuration is updated, old configuration is automatically backed up to: ``` ~/.claude-code-router/config.backup.{timestamp}.json ``` -保留最近 3 个备份。 +Keeps the last 3 backups. ## GET /api/transformers -获取服务器加载的所有转换器列表。 +Get list of all transformers loaded by the server. -### 请求示例 +### Request Example ```bash curl http://localhost:3456/api/transformers \ -H "x-api-key: your-api-key" ``` -### 响应示例 +### Response Example ```json { @@ -182,24 +186,24 @@ curl http://localhost:3456/api/transformers \ } ``` -### 转换器列表 +### Transformer List -内置转换器: +Built-in transformers: -- `anthropic` - Anthropic Claude 格式 -- `openai` - OpenAI 格式 -- `deepseek` - DeepSeek 格式 -- `gemini` - Google Gemini 格式 -- `openrouter` - OpenRouter 格式 -- `groq` - Groq 格式 -- `maxtoken` - 调整 max_tokens 参数 -- `tooluse` - 工具使用转换 -- `reasoning` - 推理模式转换 -- `enhancetool` - 增强工具功能 +- `anthropic` - Anthropic Claude format +- `openai` - OpenAI format +- `deepseek` - DeepSeek format +- `gemini` - Google Gemini format +- `openrouter` - OpenRouter format +- `groq` - Groq format +- `maxtoken` - Adjust max_tokens parameter +- `tooluse` - Tool use conversion +- `reasoning` - Reasoning mode conversion +- `enhancetool` - Enhance tool functionality -## 环境变量插值 +## Environment Variable Interpolation -配置支持环境变量插值: +Configuration supports environment variable interpolation: ```json { @@ -211,7 +215,7 @@ curl http://localhost:3456/api/transformers \ } ``` -或使用 `${VAR_NAME}` 格式: +Or use `${VAR_NAME}` format: ```json { diff --git a/docs/docs/server/api/logs-api.md b/docs/docs/server/api/logs-api.md index 5dc6ec2..5ebffed 100644 --- a/docs/docs/server/api/logs-api.md +++ b/docs/docs/server/api/logs-api.md @@ -1,17 +1,21 @@ -# 日志 API +--- +title: Logs API +--- + +# Logs API ## GET /api/logs/files -获取所有可用的日志文件列表。 +Get list of all available log files. -### 请求示例 +### Request Example ```bash curl http://localhost:3456/api/logs/files \ -H "x-api-key: your-api-key" ``` -### 响应示例 +### Response Example ```json [ @@ -30,42 +34,42 @@ curl http://localhost:3456/api/logs/files \ ] ``` -### 字段说明 +### Field Description -| 字段 | 类型 | 说明 | -|------|------|------| -| `name` | string | 文件名 | -| `path` | string | 完整文件路径 | -| `size` | integer | 文件大小(字节) | -| `lastModified` | string | 最后修改时间(ISO 8601) | +| Field | Type | Description | +|-------|------|-------------| +| `name` | string | File name | +| `path` | string | Complete file path | +| `size` | integer | File size (bytes) | +| `lastModified` | string | Last modification time (ISO 8601) | -文件按修改时间倒序排列。 +Files are sorted by modification time in descending order. ## GET /api/logs -获取指定日志文件的内容。 +Get content of specified log file. -### 查询参数 +### Query Parameters -| 参数 | 类型 | 必需 | 说明 | -|------|------|------|------| -| `file` | string | 否 | 日志文件路径(默认使用 app.log) | +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `file` | string | No | Log file path (default uses app.log) | -### 请求示例(获取默认日志) +### Request Example (Get Default Log) ```bash curl "http://localhost:3456/api/logs" \ -H "x-api-key: your-api-key" ``` -### 请求示例(获取指定文件) +### Request Example (Get Specific File) ```bash curl "http://localhost:3456/api/logs?file=/home/user/.claude-code-router/logs/ccr-20241226143022.log" \ -H "x-api-key: your-api-key" ``` -### 响应示例 +### Response Example ```json [ @@ -75,11 +79,11 @@ curl "http://localhost:3456/api/logs?file=/home/user/.claude-code-router/logs/cc ] ``` -返回的是日志行数组,每行是一个 JSON 字符串。 +Returns an array of log lines, each line is a JSON string. -### 日志格式 +### Log Format -日志使用 Pino 格式: +Logs use Pino format: ```json { @@ -97,42 +101,42 @@ curl "http://localhost:3456/api/logs?file=/home/user/.claude-code-router/logs/cc } ``` -### 日志级别 +### Log Levels -| 级别 | 值 | 说明 | -|------|------|------| -| `trace` | 10 | 最详细的日志 | -| `debug` | 20 | 调试信息 | -| `info` | 30 | 一般信息 | -| `warn` | 40 | 警告信息 | -| `error` | 50 | 错误信息 | -| `fatal` | 60 | 致命错误 | +| Level | Value | Description | +|-------|-------|-------------| +| `trace` | 10 | Most verbose logs | +| `debug` | 20 | Debug information | +| `info` | 30 | General information | +| `warn` | 40 | Warning information | +| `error` | 50 | Error information | +| `fatal` | 60 | Fatal error | ## DELETE /api/logs -清除指定日志文件的内容。 +Clear content of specified log file. -### 查询参数 +### Query Parameters -| 参数 | 类型 | 必需 | 说明 | -|------|------|------|------| -| `file` | string | 否 | 日志文件路径(默认使用 app.log) | +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `file` | string | No | Log file path (default uses app.log) | -### 请求示例(清除默认日志) +### Request Example (Clear Default Log) ```bash curl -X DELETE "http://localhost:3456/api/logs" \ -H "x-api-key: your-api-key" ``` -### 请求示例(清除指定文件) +### Request Example (Clear Specific File) ```bash curl -X DELETE "http://localhost:3456/api/logs?file=/home/user/.claude-code-router/logs/ccr-20241226143022.log" \ -H "x-api-key: your-api-key" ``` -### 响应示例 +### Response Example ```json { @@ -141,50 +145,50 @@ curl -X DELETE "http://localhost:3456/api/logs?file=/home/user/.claude-code-rout } ``` -## 日志位置 +## Log Locations -### 服务器日志 +### Server Logs -位置:`~/.claude-code-router/logs/` +Location: `~/.claude-code-router/logs/` -文件命名:`ccr-{YYYYMMDD}{HH}{MM}{SS}.log` +File naming: `ccr-{YYYYMMDD}{HH}{MM}{SS}.log` -内容:HTTP 请求、API 调用、服务器事件 +Content: HTTP requests, API calls, server events -### 应用日志 +### Application Logs -位置:`~/.claude-code-router/claude-code-router.log` +Location: `~/.claude-code-router/claude-code-router.log` -内容:路由决策、业务逻辑事件 +Content: Routing decisions, business logic events -## 日志轮转 +## Log Rotation -服务器日志使用 rotating-file-stream 自动轮转: +Server logs use rotating-file-stream for automatic rotation: -- **maxFiles**: 3 - 保留最近 3 个日志文件 -- **interval**: 1d - 每天轮转 -- **maxSize**: 50M - 单个文件最大 50MB +- **maxFiles**: 3 - Keep last 3 log files +- **interval**: 1d - Rotate daily +- **maxSize**: 50M - Maximum 50MB per file -## 日志分析 +## Log Analysis -### 使用 jq 分析日志 +### Analyze Logs with jq ```bash -# 查看所有错误日志 +# View all error logs curl "http://localhost:3456/api/logs" \ -H "x-api-key: your-api-key" | \ jq -r '.[] | fromjson | select(.level >= 40)' -# 统计请求次数 +# Count requests curl "http://localhost:3456/api/logs" \ -H "x-api-key: your-api-key" | \ jq -r '.[] | fromjson | .req.method' | \ sort | uniq -c ``` -### 实时监控日志 +### Real-time Log Monitoring ```bash -# 通过 API 实时获取最新日志 +# Get latest logs in real-time via API watch -n 5 'curl -s "http://localhost:3456/api/logs" -H "x-api-key: your-api-key" | jq -r ".[-10:]"' ``` diff --git a/docs/docs/server/api/messages-api.md b/docs/docs/server/api/messages-api.md index c5ffcd2..520a547 100644 --- a/docs/docs/server/api/messages-api.md +++ b/docs/docs/server/api/messages-api.md @@ -1,10 +1,14 @@ -# 消息 API +--- +title: Messages API +--- + +# Messages API ## POST /v1/messages -发送消息到 LLM,兼容 Anthropic Claude API 格式。 +Send messages to LLM, compatible with Anthropic Claude API format. -### 请求格式 +### Request Format ```bash curl -X POST http://localhost:3456/v1/messages \ @@ -22,19 +26,19 @@ curl -X POST http://localhost:3456/v1/messages \ }' ``` -### 请求参数 +### Request Parameters -| 参数 | 类型 | 必需 | 说明 | -|------|------|------|------| -| `model` | string | 是 | 模型名称(会被路由到实际提供商) | -| `messages` | array | 是 | 消息数组 | -| `max_tokens` | integer | 是 | 最大生成 Token 数 | -| `system` | string | 否 | 系统提示词 | -| `tools` | array | 否 | 可用工具列表 | -| `stream` | boolean | 否 | 是否使用流式响应(默认 false) | -| `temperature` | number | 否 | 温度参数(0-1) | +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `model` | string | Yes | Model name (will be routed to actual provider) | +| `messages` | array | Yes | Array of messages | +| `max_tokens` | integer | Yes | Maximum tokens to generate | +| `system` | string | No | System prompt | +| `tools` | array | No | List of available tools | +| `stream` | boolean | No | Whether to use streaming response (default false) | +| `temperature` | number | No | Temperature parameter (0-1) | -### 消息对象格式 +### Message Object Format ```json { @@ -43,7 +47,7 @@ curl -X POST http://localhost:3456/v1/messages \ } ``` -### 响应格式(非流式) +### Response Format (Non-streaming) ```json { @@ -65,9 +69,9 @@ curl -X POST http://localhost:3456/v1/messages \ } ``` -### 流式响应 +### Streaming Response -设置 `stream: true` 启用流式响应: +Set `stream: true` to enable streaming response: ```json { @@ -78,18 +82,18 @@ curl -X POST http://localhost:3456/v1/messages \ } ``` -流式响应事件类型: +Streaming response event types: -- `message_start` - 消息开始 -- `content_block_start` - 内容块开始 -- `content_block_delta` - 内容增量 -- `content_block_stop` - 内容块结束 -- `message_delta` - 消息元数据(usage) -- `message_stop` - 消息结束 +- `message_start` - Message start +- `content_block_start` - Content block start +- `content_block_delta` - Content increment +- `content_block_stop` - Content block end +- `message_delta` - Message metadata (usage) +- `message_stop` - Message end -### 工具使用 +### Tool Use -支持函数调用(Tool Use): +Supports function calling (Tool Use): ```json { @@ -120,9 +124,9 @@ curl -X POST http://localhost:3456/v1/messages \ } ``` -### 多模态支持 +### Multimodal Support -支持图片输入: +Supports image input: ```json { @@ -146,9 +150,9 @@ curl -X POST http://localhost:3456/v1/messages \ ## POST /v1/messages/count_tokens -计算消息的 Token 数量。 +Count tokens in messages. -### 请求格式 +### Request Format ```bash curl -X POST http://localhost:3456/v1/messages/count_tokens \ @@ -167,16 +171,16 @@ curl -X POST http://localhost:3456/v1/messages/count_tokens \ }' ``` -### 请求参数 +### Request Parameters -| 参数 | 类型 | 必需 | 说明 | -|------|------|------|------| -| `model` | string | 是 | 模型名称 | -| `messages` | array | 是 | 消息数组 | -| `tools` | array | 否 | 工具列表 | -| `system` | string | 否 | 系统提示词 | +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `model` | string | Yes | Model name | +| `messages` | array | Yes | Array of messages | +| `tools` | array | No | List of tools | +| `system` | string | No | System prompt | -### 响应格式 +### Response Format ```json { @@ -184,7 +188,7 @@ curl -X POST http://localhost:3456/v1/messages/count_tokens \ } ``` -## 错误响应 +## Error Responses ### 400 Bad Request diff --git a/docs/docs/server/api/overview.md b/docs/docs/server/api/overview.md index 0fb0ef5..5875c56 100644 --- a/docs/docs/server/api/overview.md +++ b/docs/docs/server/api/overview.md @@ -1,81 +1,85 @@ -# API 概览 +--- +title: API Overview +--- -Claude Code Router Server 提供了完整的 HTTP API,支持: +# API Overview -- **消息 API**:兼容 Anthropic Claude API 的消息接口 -- **配置 API**:读取和更新服务器配置 -- **日志 API**:查看和管理服务日志 -- **工具 API**:计算 Token 数量 +Claude Code Router Server provides a complete HTTP API with support for: -## 基础信息 +- **Messages API**: Message interface compatible with Anthropic Claude API +- **Configuration API**: Read and update server configuration +- **Logs API**: View and manage service logs +- **Tools API**: Calculate token counts + +## Basic Information **Base URL**: `http://localhost:3456` -**认证方式**: API Key(通过 `x-api-key` 请求头) +**Authentication**: API Key (via `x-api-key` header) ```bash curl -H "x-api-key: your-api-key" http://localhost:3456/api/config ``` -## API 端点列表 +## API Endpoints -### 消息相关 +### Messages -| 端点 | 方法 | 描述 | -|------|------|------| -| `/v1/messages` | POST | 发送消息(兼容 Anthropic API) | -| `/v1/messages/count_tokens` | POST | 计算消息的 Token 数量 | +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/v1/messages` | POST | Send message (compatible with Anthropic API) | +| `/v1/messages/count_tokens` | POST | Count tokens in messages | -### 配置管理 +### Configuration Management -| 端点 | 方法 | 描述 | -|------|------|------| -| `/api/config` | GET | 获取当前配置 | -| `/api/config` | POST | 更新配置 | -| `/api/transformers` | GET | 获取可用的转换器列表 | +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/api/config` | GET | Get current configuration | +| `/api/config` | POST | Update configuration | +| `/api/transformers` | GET | Get list of available transformers | -### 日志管理 +### Log Management -| 端点 | 方法 | 描述 | -|------|------|------| -| `/api/logs/files` | GET | 获取日志文件列表 | -| `/api/logs` | GET | 获取日志内容 | -| `/api/logs` | DELETE | 清除日志 | +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/api/logs/files` | GET | Get list of log files | +| `/api/logs` | GET | Get log content | +| `/api/logs` | DELETE | Clear logs | -### 服务管理 +### Service Management -| 端点 | 方法 | 描述 | -|------|------|------| -| `/api/restart` | POST | 重启服务 | -| `/ui` | GET | Web 管理界面 | -| `/ui/` | GET | Web 管理界面(重定向) | +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/api/restart` | POST | Restart service | +| `/ui` | GET | Web management interface | +| `/ui/` | GET | Web management interface (redirect) | -## 错误响应 +## Error Responses -所有 API 在发生错误时返回统一的错误格式: +All APIs return a unified error format when errors occur: ```json { "error": { "type": "invalid_request_error", - "message": "错误描述" + "message": "Error description" } } ``` -常见 HTTP 状态码: +Common HTTP status codes: -- `200` - 成功 -- `400` - 请求参数错误 -- `401` - 未授权(API Key 无效) -- `404` - 资源不存在 -- `500` - 服务器内部错误 +- `200` - Success +- `400` - Invalid request parameters +- `401` - Unauthorized (invalid API Key) +- `404` - Resource not found +- `500` - Internal server error -## 认证 +## Authentication -### API Key 认证 +### API Key Authentication -在请求头中添加 API Key: +Add API Key in request header: ```bash curl -X POST http://localhost:3456/v1/messages \ @@ -84,9 +88,9 @@ curl -X POST http://localhost:3456/v1/messages \ -d '...' ``` -### 无认证模式 +### No Authentication Mode -当没有配置 Providers 时,服务器会监听在 `0.0.0.0` 且无需认证: +When no Providers are configured, the server listens on `0.0.0.0` without authentication: ```json5 { @@ -94,9 +98,9 @@ curl -X POST http://localhost:3456/v1/messages \ } ``` -## 流式响应 +## Streaming Responses -消息 API 支持流式响应(Server-Sent Events): +The Messages API supports streaming responses (Server-Sent Events): ```bash curl -X POST http://localhost:3456/v1/messages \ @@ -105,7 +109,7 @@ curl -X POST http://localhost:3456/v1/messages \ -d '{"stream": true, ...}' ``` -流式响应格式: +Streaming response format: ``` event: message_start @@ -118,12 +122,12 @@ event: message_stop data: {"type":"message_stop"} ``` -## 速率限制 +## Rate Limiting -服务器本身不实现速率限制,建议通过反向代理(如 Nginx)配置。 +The server itself does not implement rate limiting. It's recommended to configure it through a reverse proxy (e.g., Nginx). -## 版本管理 +## Version Management -当前 API 版本:`v1` +Current API version: `v1` -所有 `/v1/*` 端点保持向后兼容。 +All `/v1/*` endpoints maintain backward compatibility. diff --git a/docs/docs/server/deployment.md b/docs/docs/server/deployment.md index dde5e65..2b6c1b0 100644 --- a/docs/docs/server/deployment.md +++ b/docs/docs/server/deployment.md @@ -1,10 +1,14 @@ -# Server 部署 +--- +title: Server Deployment +--- -Claude Code Router Server 支持多种部署方式,从本地开发到生产环境。 +# Server Deployment -## Docker 部署(推荐) +Claude Code Router Server supports multiple deployment methods, from local development to production environments. -### 使用 Docker Hub 镜像 +## Docker Deployment (Recommended) + +### Using Docker Hub Image ```bash docker run -d \ @@ -14,9 +18,9 @@ docker run -d \ musistudio/claude-code-router:latest ``` -### 使用 Docker Compose +### Using Docker Compose -创建 `docker-compose.yml`: +Create `docker-compose.yml`: ```yaml version: '3.8' @@ -35,15 +39,15 @@ services: restart: unless-stopped ``` -启动服务: +Start the service: ```bash docker-compose up -d ``` -### 自定义构建 +### Custom Build -从源码构建 Docker 镜像: +Build Docker image from source: ```bash git clone https://github.com/musistudio/claude-code-router.git @@ -51,9 +55,9 @@ cd claude-code-router docker build -t claude-code-router:latest . ``` -## 配置文件挂载 +## Configuration File Mounting -将配置文件挂载到容器中: +Mount configuration file into container: ```bash docker run -d \ @@ -63,20 +67,20 @@ docker run -d \ musistudio/claude-code-router:latest ``` -配置文件示例: +Configuration file example: ```json5 { - // 服务器配置 + // Server configuration "HOST": "0.0.0.0", "PORT": 3456, "APIKEY": "your-api-key-here", - // 日志配置 + // Logging configuration "LOG": true, "LOG_LEVEL": "info", - // LLM 提供商配置 + // LLM provider configuration "Providers": [ { "name": "openai", @@ -86,30 +90,30 @@ docker run -d \ } ], - // 路由配置 + // Routing configuration "Router": { "default": "openai,gpt-4" } } ``` -## 环境变量 +## Environment Variables -支持通过环境变量覆盖配置: +Override configuration through environment variables: -| 变量名 | 说明 | 默认值 | -|--------|------|--------| -| `HOST` | 监听地址 | `127.0.0.1` | -| `PORT` | 监听端口 | `3456` | -| `APIKEY` | API 密钥 | - | -| `LOG_LEVEL` | 日志级别 | `debug` | -| `LOG` | 是否启用日志 | `true` | +| Variable | Description | Default | +|----------|-------------|---------| +| `HOST` | Listen address | `127.0.0.1` | +| `PORT` | Listen port | `3456` | +| `APIKEY` | API key | - | +| `LOG_LEVEL` | Log level | `debug` | +| `LOG` | Enable logging | `true` | -## 生产环境建议 +## Production Recommendations -### 1. 使用反向代理 +### 1. Use Reverse Proxy -使用 Nginx 作为反向代理: +Use Nginx as reverse proxy: ```nginx server { @@ -129,17 +133,17 @@ server { } ``` -### 2. 配置 HTTPS +### 2. Configure HTTPS -使用 Let's Encrypt 获取免费证书: +Use Let's Encrypt to obtain free certificate: ```bash sudo certbot --nginx -d your-domain.com ``` -### 3. 日志管理 +### 3. Log Management -配置日志轮转和持久化: +Configure log rotation and persistence: ```yaml version: '3.8' @@ -152,9 +156,9 @@ services: - LOG_LEVEL=warn ``` -### 4. 健康检查 +### 4. Health Check -配置 Docker 健康检查: +Configure Docker health check: ```yaml healthcheck: @@ -164,19 +168,19 @@ healthcheck: retries: 3 ``` -## 访问 Web UI +## Access Web UI -部署完成后,访问 Web UI: +After deployment is complete, access the Web UI: ``` http://localhost:3456/ui/ ``` -通过 Web UI 可以: -- 查看和管理配置 -- 监控日志 -- 查看服务状态 +Through the Web UI you can: +- View and manage configuration +- Monitor logs +- Check service status -## 二次开发 +## Secondary Development -如果需要基于 CCR Server 进行二次开发,请查看 [API 参考](/docs/category/api)。 +If you need to develop based on CCR Server, please see [API Reference](/docs/category/api). diff --git a/docs/docs/server/intro.md b/docs/docs/server/intro.md index ddb3ff3..f1ab94e 100644 --- a/docs/docs/server/intro.md +++ b/docs/docs/server/intro.md @@ -1,14 +1,18 @@ -# Server 简介 +--- +title: Server Introduction +--- -Claude Code Router Server 是一个核心服务组件,负责将 Claude Code 的 API 请求路由到不同的 LLM 提供商。它提供了完整的 HTTP API,支持: +# Server Introduction -- **API 请求路由**:将 Anthropic 格式的请求转换为各种提供商的 API 格式 -- **认证与授权**:支持 API Key 认证 -- **配置管理**:动态配置提供商、路由规则和转换器 -- **Web UI**:内置管理界面 -- **日志系统**:完整的请求日志记录 +Claude Code Router Server is a core service component responsible for routing Claude Code API requests to different LLM providers. It provides a complete HTTP API with support for: -## 架构概述 +- **API Request Routing**: Convert Anthropic-format requests to various provider API formats +- **Authentication & Authorization**: Support API Key authentication +- **Configuration Management**: Dynamic configuration of providers, routing rules, and transformers +- **Web UI**: Built-in management interface +- **Logging System**: Complete request logging + +## Architecture Overview ``` ┌─────────────┐ ┌──────────────────┐ ┌──────────────┐ @@ -22,47 +26,47 @@ Claude Code Router Server 是一个核心服务组件,负责将 Claude Code └─ Logs API ``` -## 核心功能 +## Core Features -### 1. 请求路由 -- 基于 Token 数量的智能路由 -- 项目级路由配置 -- 自定义路由函数 -- 场景化路由(background、think、longContext 等) +### 1. Request Routing +- Token-count-based intelligent routing +- Project-level routing configuration +- Custom routing functions +- Scenario-based routing (background, think, longContext, etc.) -### 2. 请求转换 -- 支持多种 LLM 提供商的 API 格式转换 -- 内置转换器:Anthropic、DeepSeek、Gemini、OpenRouter、Groq 等 -- 可扩展的转换器系统 +### 2. Request Transformation +- Supports API format conversion for multiple LLM providers +- Built-in transformers: Anthropic, DeepSeek, Gemini, OpenRouter, Groq, etc. +- Extensible transformer system -### 3. Agent 系统 -- 插件式的 Agent 架构 -- 内置图片处理 Agent -- 自定义 Agent 支持 +### 3. Agent System +- Plugin-based Agent architecture +- Built-in image processing Agent +- Custom Agent support -### 4. 配置管理 -- JSON5 格式配置文件 -- 环境变量插值 -- 配置热更新(需重启服务) +### 4. Configuration Management +- JSON5 format configuration file +- Environment variable interpolation +- Hot configuration reload (requires service restart) -## 使用场景 +## Use Cases -### 场景一:个人本地服务 -在本地运行服务,供个人 Claude Code 使用: +### Scenario 1: Personal Local Service +Run the service locally for personal Claude Code use: ```bash ccr start ``` -### 场景二:团队共享服务 -使用 Docker 部署,为团队成员提供共享服务: +### Scenario 2: Team Shared Service +Deploy using Docker to provide shared service for team members: ```bash docker run -d -p 3456:3456 musistudio/claude-code-router ``` -### 场景三:二次开发 -基于暴露的 API 构建自定义应用: +### Scenario 3: Secondary Development +Build custom applications based on exposed APIs: ```bash GET /api/config @@ -70,8 +74,8 @@ POST /v1/messages GET /api/logs ``` -## 下一步 +## Next Steps -- [Docker 部署指南](/docs/server/deployment) - 学习如何部署服务 -- [API 参考](/docs/category/api) - 查看完整的 API 文档 -- [配置说明](/docs/category/server-config) - 了解服务器配置选项 +- [Docker Deployment Guide](/docs/server/deployment) - Learn how to deploy the service +- [API Reference](/docs/category/api) - View complete API documentation +- [Configuration Guide](/docs/category/server-config) - Understand server configuration options diff --git a/docs/i18n/zh-CN/docusaurus-plugin-content-docs/advanced/presets.md b/docs/i18n/zh-CN/docusaurus-plugin-content-docs/advanced/presets.md index fe483bd..91bf70b 100644 --- a/docs/i18n/zh-CN/docusaurus-plugin-content-docs/advanced/presets.md +++ b/docs/i18n/zh-CN/docusaurus-plugin-content-docs/advanced/presets.md @@ -12,6 +12,216 @@ sidebar_position: 3 预设是预配置的设置,包括针对特定用例优化的提供商配置、路由规则和转换器。 +## 动态配置系统 (v2.0+) + +CCR 2.0 引入了强大的动态配置系统,支持: + +- **多种输入类型**:选择器、多选、确认框、文本输入、数字输入等 +- **条件逻辑**:根据用户输入动态显示/隐藏配置项 +- **变量引用**:配置项之间可以互相引用 +- **动态选项**:选项列表可以从预设配置或用户输入中动态生成 + +### Schema 字段类型 + +| 类型 | 说明 | 示例 | +|------|------|------| +| `password` | 密码输入(隐藏显示) | API Key | +| `input` | 单行文本输入 | Base URL | +| `number` | 数字输入 | 最大Token数 | +| `select` | 单选下拉框 | 选择Provider | +| `multiselect` | 多选框 | 启用功能 | +| `confirm` | 确认框 | 是否使用代理 | +| `editor` | 多行文本编辑器 | 自定义配置 | + +### 条件运算符 + +| 运算符 | 说明 | 示例 | +|--------|------|------| +| `eq` | 等于 | `{"field": "provider", "operator": "eq", "value": "openai"}` | +| `ne` | 不等于 | `{"field": "advanced", "operator": "ne", "value": true}` | +| `in` | 包含于 | `{"field": "feature", "operator": "in", "value": ["a", "b"]}` | +| `nin` | 不包含于 | `{"field": "type", "operator": "nin", "value": ["x", "y"]}` | +| `exists` | 字段存在 | `{"field": "apiKey", "operator": "exists"}` | +| `gt/lt/gte/lte` | 大于/小于/大于等于/小于等于 | 用于数字比较 | + +### 动态选项类型 + +#### static - 静态选项 +```json +"options": { + "type": "static", + "options": [ + {"label": "选项1", "value": "value1"}, + {"label": "选项2", "value": "value2"} + ] +} +``` + +#### providers - 从 Providers 配置提取 +```json +"options": { + "type": "providers" +} +``` +自动从 `Providers` 数组中提取 name 作为选项。 + +#### models - 从指定 Provider 的 models 提取 +```json +"options": { + "type": "models", + "providerField": "{{selectedProvider}}" +} +``` +根据用户选择的 Provider,动态显示该 Provider 的 models。 + +### 模板变量 + +使用 `{{变量名}}` 语法在 template 中引用用户输入: + +```json +"template": { + "Providers": [ + { + "name": "{{providerName}}", + "api_key": "{{apiKey}}" + } + ] +} +``` + +### 配置映射 + +对于复杂的配置需求,使用 `configMappings` 精确控制值的位置: + +```json +"configMappings": [ + { + "target": "Providers[0].api_key", + "value": "{{apiKey}}" + }, + { + "target": "PROXY_URL", + "value": "{{proxyUrl}}", + "when": { + "field": "useProxy", + "operator": "eq", + "value": true + } + } +] +``` + +### 完整示例 + +```json +{ + "name": "multi-provider-example", + "version": "1.0.0", + "description": "多Provider配置示例 - 支持OpenAI和DeepSeek切换", + "author": "CCR Team", + "keywords": ["openai", "deepseek", "multi-provider"], + "ccrVersion": "2.0.0", + "schema": [ + { + "id": "primaryProvider", + "type": "select", + "label": "主要Provider", + "prompt": "选择您主要使用的LLM提供商", + "options": { + "type": "static", + "options": [ + { + "label": "OpenAI", + "value": "openai", + "description": "使用OpenAI的GPT模型" + }, + { + "label": "DeepSeek", + "value": "deepseek", + "description": "使用DeepSeek的高性价比模型" + } + ] + }, + "required": true, + "defaultValue": "openai" + }, + { + "id": "apiKey", + "type": "password", + "label": "API Key", + "prompt": "请输入您的API Key", + "placeholder": "sk-...", + "required": true + }, + { + "id": "defaultModel", + "type": "select", + "label": "默认模型", + "prompt": "选择默认使用的模型", + "options": { + "type": "static", + "options": [ + {"label": "GPT-4o", "value": "gpt-4o"}, + {"label": "GPT-4o-mini", "value": "gpt-4o-mini"} + ] + }, + "required": true, + "defaultValue": "gpt-4o", + "when": { + "field": "primaryProvider", + "operator": "eq", + "value": "openai" + } + }, + { + "id": "enableProxy", + "type": "confirm", + "label": "启用代理", + "prompt": "是否通过代理访问API?", + "defaultValue": false + }, + { + "id": "proxyUrl", + "type": "input", + "label": "代理地址", + "prompt": "输入代理服务器地址", + "placeholder": "http://127.0.0.1:7890", + "required": true, + "when": { + "field": "enableProxy", + "operator": "eq", + "value": true + } + } + ], + "template": { + "Providers": [ + { + "name": "{{primaryProvider}}", + "api_base_url": "https://api.openai.com/v1", + "api_key": "{{apiKey}}", + "models": ["{{defaultModel}}"] + } + ], + "Router": { + "default": "{{primaryProvider}}/{{defaultModel}}" + }, + "PROXY_URL": "{{proxyUrl}}" + }, + "configMappings": [ + { + "target": "PROXY_URL", + "value": "{{proxyUrl}}", + "when": { + "field": "enableProxy", + "operator": "eq", + "value": true + } + } + ] +} +``` + ## 可用预设 ### Development(开发) @@ -66,7 +276,42 @@ ccr preset list ## 创建自定义预设 -您可以通过保存配置并稍后重新加载来创建自定义预设: +创建包含 `schema` 和 `template` 的预设文件: + +```bash +# 创建预设目录 +mkdir -p ~/.claude-code-router/presets/my-preset + +# 创建 manifest.json +cat > ~/.claude-code-router/presets/my-preset/manifest.json << 'EOF' +{ + "name": "my-preset", + "version": "1.0.0", + "description": "我的自定义预设", + "schema": [ + { + "id": "apiKey", + "type": "password", + "label": "API Key", + "required": true + } + ], + "template": { + "Providers": [ + { + "name": "my-provider", + "api_key": "{{apiKey}}" + } + ] + } +} +EOF + +# 应用预设(会提示输入API Key) +ccr my-preset +``` + +您也可以通过保存配置并稍后重新加载来创建自定义预设: ```bash # 将当前配置保存为预设 @@ -116,35 +361,7 @@ ccr preset delete <预设名称> ~/.claude-code-router/presets/ ``` -每个预设都是一个 JSON 文件,包含完整的配置。 - -## 预设文件示例 - -```json -{ - "name": "development", - "description": "针对软件开发优化的配置", - "Providers": [ - { - "name": "deepseek", - "api_base_url": "https://api.deepseek.com/chat/completions", - "api_key": "$DEEPSEEK_API_KEY", - "models": ["deepseek-chat", "deepseek-coder"] - }, - { - "name": "groq", - "api_base_url": "https://api.groq.com/openai/v1/chat/completions", - "api_key": "$GROQ_API_KEY", - "models": ["llama-3.3-70b-versatile"] - } - ], - "Router": { - "default": "deepseek,deepseek-chat", - "background": "groq,llama-3.3-70b-versatile", - "think": "deepseek,deepseek-chat" - } -} -``` +每个预设都是一个目录,包含 `manifest.json` 文件。 ## 导出和导入预设 @@ -163,10 +380,13 @@ ccr config edit ## 最佳实践 -1. **为不同项目创建预设**:为不同的工作流程创建专门的预设 -2. **版本控制**:将常用预设保存在版本控制中 -3. **文档化**:为自定义预设添加描述 -4. **测试**:在应用预设后验证配置 +1. **使用动态配置**:为需要用户输入的配置项使用schema系统 +2. **提供默认值**:为非必填项提供合理的默认值 +3. **条件显示**:使用when条件避免不必要的输入 +4. **清晰的标签**:为每个字段提供清晰的label和prompt +5. **验证输入**:使用validator确保输入的有效性 +6. **版本控制**:将常用预设保存在版本控制中 +7. **文档化**:为自定义预设添加描述和版本信息 ## 下一步 diff --git a/docs/i18n/zh-CN/docusaurus-plugin-content-docs/cli/config/basic.md b/docs/i18n/zh-CN/docusaurus-plugin-content-docs/cli/config/basic.md new file mode 100644 index 0000000..710f2c8 --- /dev/null +++ b/docs/i18n/zh-CN/docusaurus-plugin-content-docs/cli/config/basic.md @@ -0,0 +1,208 @@ +# CLI 基础配置 + +CLI 使用与 Server 相同的配置文件:`~/.claude-code-router/config.json` + +## 配置文件位置 + +```bash +~/.claude-code-router/config.json +``` + +## 快速配置 + +使用交互式命令配置: + +```bash +ccr model +``` + +这将引导你完成: +1. 选择 LLM 提供商 +2. 配置 API Key +3. 选择模型 +4. 设置路由规则 + +## 手动配置 + +### 编辑配置文件 + +```bash +# 打开配置文件 +nano ~/.claude-code-router/config.json +``` + +### 最小配置示例 + +```json5 +{ + // API 密钥(可选,用于保护服务) + "APIKEY": "your-api-key-here", + + // LLM 提供商 + "Providers": [ + { + "name": "openai", + "baseUrl": "https://api.openai.com/v1", + "apiKey": "$OPENAI_API_KEY", + "models": ["gpt-4", "gpt-3.5-turbo"] + } + ], + + // 默认路由 + "Router": { + "default": "openai,gpt-4" + } +} +``` + +## 环境变量 + +配置支持环境变量插值: + +```json5 +{ + "Providers": [ + { + "apiKey": "$OPENAI_API_KEY" // 从环境变量读取 + } + ] +} +``` + +在 `.bashrc` 或 `.zshrc` 中设置: + +```bash +export OPENAI_API_KEY="sk-..." +export ANTHROPIC_API_KEY="sk-ant-..." +``` + +## 常用配置项 + +### HOST 和 PORT + +```json5 +{ + "HOST": "127.0.0.1", // 监听地址 + "PORT": 3456 // 监听端口 +} +``` + +### 日志配置 + +```json5 +{ + "LOG": true, // 启用日志 + "LOG_LEVEL": "info" // 日志级别 +} +``` + +### 路由配置 + +```json5 +{ + "Router": { + "default": "openai,gpt-4", + "background": "openai,gpt-3.5-turbo", + "think": "openai,gpt-4", + "longContext": "anthropic,claude-3-opus" + } +} +``` + +## 配置验证 + +配置文件会自动验证。常见错误: + +- **缺少 Providers**:必须至少配置一个提供商 +- **API Key 缺失**:如果配置了 Providers,必须提供 API Key +- **模型不存在**:确保模型在提供商的 models 列表中 + +## 配置备份 + +每次更新配置时会自动备份: + +``` +~/.claude-code-router/config.backup.{timestamp}.json +``` + +## 重新加载配置 + +修改配置后需要重启服务: + +```bash +ccr restart +``` + +## 查看当前配置 + +```bash +# 通过 API 查看 +curl http://localhost:3456/api/config + +# 或查看配置文件 +cat ~/.claude-code-router/config.json +``` + +## 示例配置 + +### OpenAI + +```json5 +{ + "Providers": [ + { + "name": "openai", + "baseUrl": "https://api.openai.com/v1", + "apiKey": "$OPENAI_API_KEY", + "models": ["gpt-4", "gpt-3.5-turbo"] + } + ], + "Router": { + "default": "openai,gpt-4" + } +} +``` + +### Anthropic + +```json5 +{ + "Providers": [ + { + "name": "anthropic", + "baseUrl": "https://api.anthropic.com/v1", + "apiKey": "$ANTHROPIC_API_KEY", + "models": ["claude-3-5-sonnet-20241022", "claude-3-opus-20240229"] + } + ], + "Router": { + "default": "anthropic,claude-3-5-sonnet-20241022" + } +} +``` + +### 多提供商 + +```json5 +{ + "Providers": [ + { + "name": "openai", + "baseUrl": "https://api.openai.com/v1", + "apiKey": "$OPENAI_API_KEY", + "models": ["gpt-4", "gpt-3.5-turbo"] + }, + { + "name": "anthropic", + "baseUrl": "https://api.anthropic.com/v1", + "apiKey": "$ANTHROPIC_API_KEY", + "models": ["claude-3-5-sonnet-20241022", "claude-3-opus-20240229"] + } + ], + "Router": { + "default": "openai,gpt-4", + "think": "anthropic,claude-3-5-sonnet-20241022", + "background": "openai,gpt-3.5-turbo" + } +} +``` diff --git a/docs/i18n/zh-CN/docusaurus-plugin-content-docs/cli/config/project-level.md b/docs/i18n/zh-CN/docusaurus-plugin-content-docs/cli/config/project-level.md new file mode 100644 index 0000000..9b2f402 --- /dev/null +++ b/docs/i18n/zh-CN/docusaurus-plugin-content-docs/cli/config/project-level.md @@ -0,0 +1,213 @@ +# 项目级配置 + +除了全局配置,`ccr` 还支持为特定项目设置不同的路由规则。 + +## 项目配置文件 + +项目配置文件位于: + +``` +~/.claude/projects//claude-code-router.json +``` + +其中 `` 是 Claude Code 项目的唯一标识符。 + +## 项目配置结构 + +```json5 +{ + "Router": { + "default": "openai,gpt-4", + "background": "openai,gpt-3.5-turbo" + } +} +``` + +## 查找项目 ID + +### 方法一:使用 CLI + +```bash +# 在项目目录中运行 +ccr status +``` + +输出会显示当前项目 ID: + +``` +Project: my-project (abc123def456) +``` + +### 方法二:查看 Claude Code 配置 + +```bash +cat ~/.claude.json +``` + +找到你的项目 ID: + +```json +{ + "projects": { + "abc123def456": { + "path": "/path/to/your/project", + "name": "my-project" + } + } +} +``` + +## 创建项目配置 + +### 手动创建 + +```bash +# 创建项目配置目录 +mkdir -p ~/.claude/projects/abc123def456 + +# 创建配置文件 +cat > ~/.claude/projects/abc123def456/claude-code-router.json << 'EOF' +{ + "Router": { + "default": "anthropic,claude-3-5-sonnet-20241022", + "background": "openai,gpt-3.5-turbo" + } +} +EOF +``` + +### 使用 ccr model 命令 + +```bash +# 在项目目录中运行 +cd /path/to/your/project +ccr model --project +``` + +## 配置优先级 + +路由配置的优先级(从高到低): + +1. **自定义路由函数** (`CUSTOM_ROUTER_PATH`) +2. **项目级配置** (`~/.claude/projects//claude-code-router.json`) +3. **全局配置** (`~/.claude-code-router/config.json`) +4. **内置路由规则** + +## 使用场景 + +### 场景一:不同项目使用不同模型 + +```json5 +// Web 项目使用 GPT-4 +~/.claude/projects/web-project-id/claude-code-router.json: +{ + "Router": { + "default": "openai,gpt-4" + } +} + +// AI 项目使用 Claude +~/.claude/projects/ai-project-id/claude-code-router.json: +{ + "Router": { + "default": "anthropic,claude-3-5-sonnet-20241022" + } +} +``` + +### 场景二:测试项目使用低成本模型 + +```json5 +~/.claude/projects/test-project-id/claude-code-router.json: +{ + "Router": { + "default": "openai,gpt-3.5-turbo", + "background": "openai,gpt-3.5-turbo" + } +} +``` + +### 场景三:长上下文项目 + +```json5 +~/.claude/projects/long-context-project-id/claude-code-router.json: +{ + "Router": { + "default": "anthropic,claude-3-opus-20240229", + "longContext": "anthropic,claude-3-opus-20240229" + } +} +``` + +## 验证项目配置 + +```bash +# 查看当前项目使用的路由 +ccr status + +# 查看日志确认路由决策 +tail -f ~/.claude-code-router/claude-code-router.log +``` + +## 删除项目配置 + +```bash +rm ~/.claude/projects//claude-code-router.json +``` + +删除后会回退到全局配置。 + +## 完整示例 + +假设你有两个项目: + +### 全局配置(`~/.claude-code-router/config.json`) + +```json5 +{ + "Providers": [ + { + "name": "openai", + "baseUrl": "https://api.openai.com/v1", + "apiKey": "$OPENAI_API_KEY", + "models": ["gpt-4", "gpt-3.5-turbo"] + }, + { + "name": "anthropic", + "baseUrl": "https://api.anthropic.com/v1", + "apiKey": "$ANTHROPIC_API_KEY", + "models": ["claude-3-5-sonnet-20241022"] + } + ], + "Router": { + "default": "openai,gpt-4", + "background": "openai,gpt-3.5-turbo" + } +} +``` + +### Web 项目配置 + +```json5 +{ + "Router": { + "default": "openai,gpt-4" + } +} +``` + +### AI 项目配置 + +```json5 +{ + "Router": { + "default": "anthropic,claude-3-5-sonnet-20241022", + "think": "anthropic,claude-3-5-sonnet-20241022" + } +} +``` + +这样: +- Web 项目使用 GPT-4 +- AI 项目使用 Claude +- 所有项目的后台任务使用 GPT-3.5-turbo(继承全局配置) diff --git a/docs/i18n/zh-CN/docusaurus-plugin-content-docs/cli/intro.md b/docs/i18n/zh-CN/docusaurus-plugin-content-docs/cli/intro.md new file mode 100644 index 0000000..b50f69c --- /dev/null +++ b/docs/i18n/zh-CN/docusaurus-plugin-content-docs/cli/intro.md @@ -0,0 +1,83 @@ +# CLI 简介 + +Claude Code Router CLI (`ccr`) 是一个命令行工具,用于管理和控制 Claude Code Router 服务。 + +## 功能概述 + +`ccr` 提供以下功能: + +- **服务管理**:启动、停止、重启服务 +- **配置管理**:交互式配置模型选择 +- **状态查看**:查看服务运行状态 +- **代码执行**:直接执行 `claude` 命令 +- **环境集成**:输出环境变量用于 shell 集成 +- **Web UI**:打开 Web 管理界面 +- **状态栏**:集成到编辑器状态栏 + +## 安装 + +```bash +npm install -g @musistudio/claude-code-router-cli +``` + +或使用项目别名: + +```bash +npm install -g claude-code-router +``` + +## 基本使用 + +### 启动服务 + +```bash +ccr start +``` + +### 查看状态 + +```bash +ccr status +``` + +### 停止服务 + +```bash +ccr stop +``` + +### 查看模型 + +```bash +ccr model +``` + +## 与 Claude Code 集成 + +`ccr` 可以与 Claude Code 无缝集成,将请求路由到你选择的 LLM 提供商。 + +### 方式一:设置 API 地址 + +```bash +export ANTHROPIC_BASE_URL="http://localhost:3456/v1" +export ANTHROPIC_API_KEY="your-api-key" +``` + +### 方式二:使用 activate 命令 + +```bash +eval "$(ccr activate)" +``` + +## 配置文件 + +`ccr` 使用与 Server 相同的配置文件:`~/.claude-code-router/config.json` + +配置一次,CLI 和 Server 都会使用。 + +## 下一步 + +- [安装指南](/docs/cli/installation) - 详细安装说明 +- [快速开始](/docs/cli/quick-start) - 5 分钟上手 +- [命令参考](/docs/category/cli-commands) - 完整命令列表 +- [配置说明](/docs/category/cli-config) - 配置文件详解 diff --git a/docs/i18n/zh-CN/docusaurus-plugin-content-docs/current.json b/docs/i18n/zh-CN/docusaurus-plugin-content-docs/current.json index 385eb20..3bebc6f 100644 --- a/docs/i18n/zh-CN/docusaurus-plugin-content-docs/current.json +++ b/docs/i18n/zh-CN/docusaurus-plugin-content-docs/current.json @@ -3,52 +3,76 @@ "message": "Next", "description": "The label for version current" }, - "sidebar.tutorialSidebar.category.Getting Started": { - "message": "Getting Started", - "description": "The label for category 'Getting Started' in sidebar 'tutorialSidebar'" + "sidebar.tutorialSidebar.category.Server": { + "message": "服务器", + "description": "The label for category 'Server' in sidebar 'tutorialSidebar'" }, - "sidebar.tutorialSidebar.category.Getting Started.link.generated-index.title": { - "message": "Getting Started", - "description": "The generated-index page title for category 'Getting Started' in sidebar 'tutorialSidebar'" + "sidebar.tutorialSidebar.category.Server.link.generated-index.title": { + "message": "Claude Code Router 服务器", + "description": "The generated-index page title for category 'Server' in sidebar 'tutorialSidebar'" }, - "sidebar.tutorialSidebar.category.Getting Started.link.generated-index.description": { - "message": "Learn the basics of Claude Code Router", - "description": "The generated-index page description for category 'Getting Started' in sidebar 'tutorialSidebar'" + "sidebar.tutorialSidebar.category.Server.link.generated-index.description": { + "message": "部署和管理 Claude Code Router 服务器", + "description": "The generated-index page description for category 'Server' in sidebar 'tutorialSidebar'" + }, + "sidebar.tutorialSidebar.category.API Reference": { + "message": "API 参考", + "description": "The label for category 'API Reference' in sidebar 'tutorialSidebar'" + }, + "sidebar.tutorialSidebar.category.API Reference.link.generated-index.title": { + "message": "API 参考", + "description": "The generated-index page title for category 'API Reference' in sidebar 'tutorialSidebar'" + }, + "sidebar.tutorialSidebar.category.API Reference.link.generated-index.description": { + "message": "服务器 API 接口文档", + "description": "The generated-index page description for category 'API Reference' in sidebar 'tutorialSidebar'" }, "sidebar.tutorialSidebar.category.Configuration": { - "message": "Configuration", + "message": "配置", "description": "The label for category 'Configuration' in sidebar 'tutorialSidebar'" }, "sidebar.tutorialSidebar.category.Configuration.link.generated-index.title": { - "message": "Configuration", + "message": "服务器配置", "description": "The generated-index page title for category 'Configuration' in sidebar 'tutorialSidebar'" }, "sidebar.tutorialSidebar.category.Configuration.link.generated-index.description": { - "message": "Configure Claude Code Router to suit your needs", + "message": "服务器配置说明", "description": "The generated-index page description for category 'Configuration' in sidebar 'tutorialSidebar'" }, "sidebar.tutorialSidebar.category.Advanced": { - "message": "Advanced", + "message": "高级", "description": "The label for category 'Advanced' in sidebar 'tutorialSidebar'" }, "sidebar.tutorialSidebar.category.Advanced.link.generated-index.title": { - "message": "Advanced Topics", + "message": "高级主题", "description": "The generated-index page title for category 'Advanced' in sidebar 'tutorialSidebar'" }, "sidebar.tutorialSidebar.category.Advanced.link.generated-index.description": { - "message": "Advanced features and customization", + "message": "高级功能和自定义", "description": "The generated-index page description for category 'Advanced' in sidebar 'tutorialSidebar'" }, - "sidebar.tutorialSidebar.category.CLI Reference": { - "message": "CLI Reference", - "description": "The label for category 'CLI Reference' in sidebar 'tutorialSidebar'" + "sidebar.tutorialSidebar.category.CLI": { + "message": "CLI", + "description": "The label for category 'CLI' in sidebar 'tutorialSidebar'" }, - "sidebar.tutorialSidebar.category.CLI Reference.link.generated-index.title": { - "message": "CLI Commands", - "description": "The generated-index page title for category 'CLI Reference' in sidebar 'tutorialSidebar'" + "sidebar.tutorialSidebar.category.CLI.link.generated-index.title": { + "message": "Claude Code Router CLI", + "description": "The generated-index page title for category 'CLI' in sidebar 'tutorialSidebar'" }, - "sidebar.tutorialSidebar.category.CLI Reference.link.generated-index.description": { - "message": "Complete reference for all CLI commands", - "description": "The generated-index page description for category 'CLI Reference' in sidebar 'tutorialSidebar'" + "sidebar.tutorialSidebar.category.CLI.link.generated-index.description": { + "message": "命令行工具使用指南", + "description": "The generated-index page description for category 'CLI' in sidebar 'tutorialSidebar'" + }, + "sidebar.tutorialSidebar.category.Commands": { + "message": "命令", + "description": "The label for category 'Commands' in sidebar 'tutorialSidebar'" + }, + "sidebar.tutorialSidebar.category.Commands.link.generated-index.title": { + "message": "CLI 命令", + "description": "The generated-index page title for category 'Commands' in sidebar 'tutorialSidebar'" + }, + "sidebar.tutorialSidebar.category.Commands.link.generated-index.description": { + "message": "完整的命令参考", + "description": "The generated-index page description for category 'Commands' in sidebar 'tutorialSidebar'" } } diff --git a/docs/i18n/zh-CN/docusaurus-plugin-content-docs/server/api/config-api.md b/docs/i18n/zh-CN/docusaurus-plugin-content-docs/server/api/config-api.md new file mode 100644 index 0000000..e0baf88 --- /dev/null +++ b/docs/i18n/zh-CN/docusaurus-plugin-content-docs/server/api/config-api.md @@ -0,0 +1,220 @@ +# 配置 API + +## GET /api/config + +获取当前服务器配置。 + +### 请求示例 + +```bash +curl http://localhost:3456/api/config \ + -H "x-api-key: your-api-key" +``` + +### 响应示例 + +```json +{ + "HOST": "0.0.0.0", + "PORT": 3456, + "APIKEY": "sk-xxxxx", + "Providers": [ + { + "name": "openai", + "baseUrl": "https://api.openai.com/v1", + "apiKey": "sk-...", + "models": ["gpt-4", "gpt-3.5-turbo"] + } + ], + "Router": { + "default": "openai,gpt-4" + }, + "transformers": [ + "anthropic" + ] +} +``` + +## POST /api/config + +更新服务器配置。更新后会自动备份旧配置。 + +### 请求示例 + +```bash +curl -X POST http://localhost:3456/api/config \ + -H "x-api-key: your-api-key" \ + -H "content-type: application/json" \ + -d '{ + "HOST": "0.0.0.0", + "PORT": 3456, + "Providers": [ + { + "name": "openai", + "baseUrl": "https://api.openai.com/v1", + "apiKey": "$OPENAI_API_KEY", + "models": ["gpt-4"] + } + ], + "Router": { + "default": "openai,gpt-4" + } + }' +``` + +### 配置对象结构 + +#### 基础配置 + +| 字段 | 类型 | 必需 | 说明 | +|------|------|------|------| +| `HOST` | string | 否 | 监听地址(默认 127.0.0.1) | +| `PORT` | integer | 否 | 监听端口(默认 3456) | +| `APIKEY` | string | 否 | API 密钥 | +| `LOG` | boolean | 否 | 是否启用日志(默认 true) | +| `LOG_LEVEL` | string | 否 | 日志级别(debug/info/warn/error) | + +#### Providers 配置 + +```json +{ + "Providers": [ + { + "name": "provider-name", + "baseUrl": "https://api.example.com/v1", + "apiKey": "your-api-key", + "models": ["model-1", "model-2"] + } + ] +} +``` + +| 字段 | 类型 | 必需 | 说明 | +|------|------|------|------| +| `name` | string | 是 | 提供商名称 | +| `baseUrl` | string | 是 | API 基础 URL | +| `apiKey` | string | 是 | API 密钥 | +| `models` | array | 是 | 支持的模型列表 | + +#### Router 配置 + +```json +{ + "Router": { + "default": "provider,model", + "longContextThreshold": 100000, + "routes": { + "background": "lightweight-model", + "think": "powerful-model", + "longContext": "long-context-model", + "webSearch": "search-model", + "image": "vision-model" + } + } +} +``` + +#### Transformers 配置 + +```json +{ + "transformers": [ + { + "name": "anthropic", + "provider": "provider-name", + "models": ["model-1"], + "options": {} + } + ] +} +``` + +### 响应示例 + +成功: + +```json +{ + "success": true, + "message": "Config saved successfully" +} +``` + +### 配置备份 + +每次更新配置时,旧配置会自动备份到: + +``` +~/.claude-code-router/config.backup.{timestamp}.json +``` + +保留最近 3 个备份。 + +## GET /api/transformers + +获取服务器加载的所有转换器列表。 + +### 请求示例 + +```bash +curl http://localhost:3456/api/transformers \ + -H "x-api-key: your-api-key" +``` + +### 响应示例 + +```json +{ + "transformers": [ + { + "name": "anthropic", + "endpoint": null + }, + { + "name": "openai", + "endpoint": null + }, + { + "name": "gemini", + "endpoint": "https://generativelanguage.googleapis.com" + } + ] +} +``` + +### 转换器列表 + +内置转换器: + +- `anthropic` - Anthropic Claude 格式 +- `openai` - OpenAI 格式 +- `deepseek` - DeepSeek 格式 +- `gemini` - Google Gemini 格式 +- `openrouter` - OpenRouter 格式 +- `groq` - Groq 格式 +- `maxtoken` - 调整 max_tokens 参数 +- `tooluse` - 工具使用转换 +- `reasoning` - 推理模式转换 +- `enhancetool` - 增强工具功能 + +## 环境变量插值 + +配置支持环境变量插值: + +```json +{ + "Providers": [ + { + "apiKey": "$OPENAI_API_KEY" + } + ] +} +``` + +或使用 `${VAR_NAME}` 格式: + +```json +{ + "baseUrl": "${API_BASE_URL}" +} +``` diff --git a/docs/i18n/zh-CN/docusaurus-plugin-content-docs/server/api/logs-api.md b/docs/i18n/zh-CN/docusaurus-plugin-content-docs/server/api/logs-api.md new file mode 100644 index 0000000..5dc6ec2 --- /dev/null +++ b/docs/i18n/zh-CN/docusaurus-plugin-content-docs/server/api/logs-api.md @@ -0,0 +1,190 @@ +# 日志 API + +## GET /api/logs/files + +获取所有可用的日志文件列表。 + +### 请求示例 + +```bash +curl http://localhost:3456/api/logs/files \ + -H "x-api-key: your-api-key" +``` + +### 响应示例 + +```json +[ + { + "name": "ccr-20241226143022.log", + "path": "/home/user/.claude-code-router/logs/ccr-20241226143022.log", + "size": 1024000, + "lastModified": "2024-12-26T14:30:22.000Z" + }, + { + "name": "ccr-20241226143021.log", + "path": "/home/user/.claude-code-router/logs/ccr-20241226143021.log", + "size": 980000, + "lastModified": "2024-12-26T14:30:21.000Z" + } +] +``` + +### 字段说明 + +| 字段 | 类型 | 说明 | +|------|------|------| +| `name` | string | 文件名 | +| `path` | string | 完整文件路径 | +| `size` | integer | 文件大小(字节) | +| `lastModified` | string | 最后修改时间(ISO 8601) | + +文件按修改时间倒序排列。 + +## GET /api/logs + +获取指定日志文件的内容。 + +### 查询参数 + +| 参数 | 类型 | 必需 | 说明 | +|------|------|------|------| +| `file` | string | 否 | 日志文件路径(默认使用 app.log) | + +### 请求示例(获取默认日志) + +```bash +curl "http://localhost:3456/api/logs" \ + -H "x-api-key: your-api-key" +``` + +### 请求示例(获取指定文件) + +```bash +curl "http://localhost:3456/api/logs?file=/home/user/.claude-code-router/logs/ccr-20241226143022.log" \ + -H "x-api-key: your-api-key" +``` + +### 响应示例 + +```json +[ + "{\"level\":30,\"time\":1703550622000,\"pid\":12345,\"hostname\":\"server\",\"msg\":\"Incoming request\",\"req\":{\"id\":1,\"method\":\"POST\",\"url\":\"/v1/messages\",\"remoteAddress\":\"127.0.0.1\"}}", + "{\"level\":30,\"time\":1703550622500,\"pid\":12345,\"hostname\":\"server\",\"msg\":\"Request completed\",\"res\":{\"statusCode\":200,\"responseTime\":500}}", + "..." +] +``` + +返回的是日志行数组,每行是一个 JSON 字符串。 + +### 日志格式 + +日志使用 Pino 格式: + +```json +{ + "level": 30, + "time": 1703550622000, + "pid": 12345, + "hostname": "server", + "msg": "Incoming request", + "req": { + "id": 1, + "method": "POST", + "url": "/v1/messages", + "remoteAddress": "127.0.0.1" + } +} +``` + +### 日志级别 + +| 级别 | 值 | 说明 | +|------|------|------| +| `trace` | 10 | 最详细的日志 | +| `debug` | 20 | 调试信息 | +| `info` | 30 | 一般信息 | +| `warn` | 40 | 警告信息 | +| `error` | 50 | 错误信息 | +| `fatal` | 60 | 致命错误 | + +## DELETE /api/logs + +清除指定日志文件的内容。 + +### 查询参数 + +| 参数 | 类型 | 必需 | 说明 | +|------|------|------|------| +| `file` | string | 否 | 日志文件路径(默认使用 app.log) | + +### 请求示例(清除默认日志) + +```bash +curl -X DELETE "http://localhost:3456/api/logs" \ + -H "x-api-key: your-api-key" +``` + +### 请求示例(清除指定文件) + +```bash +curl -X DELETE "http://localhost:3456/api/logs?file=/home/user/.claude-code-router/logs/ccr-20241226143022.log" \ + -H "x-api-key: your-api-key" +``` + +### 响应示例 + +```json +{ + "success": true, + "message": "Logs cleared successfully" +} +``` + +## 日志位置 + +### 服务器日志 + +位置:`~/.claude-code-router/logs/` + +文件命名:`ccr-{YYYYMMDD}{HH}{MM}{SS}.log` + +内容:HTTP 请求、API 调用、服务器事件 + +### 应用日志 + +位置:`~/.claude-code-router/claude-code-router.log` + +内容:路由决策、业务逻辑事件 + +## 日志轮转 + +服务器日志使用 rotating-file-stream 自动轮转: + +- **maxFiles**: 3 - 保留最近 3 个日志文件 +- **interval**: 1d - 每天轮转 +- **maxSize**: 50M - 单个文件最大 50MB + +## 日志分析 + +### 使用 jq 分析日志 + +```bash +# 查看所有错误日志 +curl "http://localhost:3456/api/logs" \ + -H "x-api-key: your-api-key" | \ + jq -r '.[] | fromjson | select(.level >= 40)' + +# 统计请求次数 +curl "http://localhost:3456/api/logs" \ + -H "x-api-key: your-api-key" | \ + jq -r '.[] | fromjson | .req.method' | \ + sort | uniq -c +``` + +### 实时监控日志 + +```bash +# 通过 API 实时获取最新日志 +watch -n 5 'curl -s "http://localhost:3456/api/logs" -H "x-api-key: your-api-key" | jq -r ".[-10:]"' +``` diff --git a/docs/i18n/zh-CN/docusaurus-plugin-content-docs/server/api/messages-api.md b/docs/i18n/zh-CN/docusaurus-plugin-content-docs/server/api/messages-api.md new file mode 100644 index 0000000..c5ffcd2 --- /dev/null +++ b/docs/i18n/zh-CN/docusaurus-plugin-content-docs/server/api/messages-api.md @@ -0,0 +1,220 @@ +# 消息 API + +## POST /v1/messages + +发送消息到 LLM,兼容 Anthropic Claude API 格式。 + +### 请求格式 + +```bash +curl -X POST http://localhost:3456/v1/messages \ + -H "x-api-key: your-api-key" \ + -H "content-type: application/json" \ + -d '{ + "model": "claude-3-5-sonnet-20241022", + "max_tokens": 1024, + "messages": [ + { + "role": "user", + "content": "Hello, Claude!" + } + ] + }' +``` + +### 请求参数 + +| 参数 | 类型 | 必需 | 说明 | +|------|------|------|------| +| `model` | string | 是 | 模型名称(会被路由到实际提供商) | +| `messages` | array | 是 | 消息数组 | +| `max_tokens` | integer | 是 | 最大生成 Token 数 | +| `system` | string | 否 | 系统提示词 | +| `tools` | array | 否 | 可用工具列表 | +| `stream` | boolean | 否 | 是否使用流式响应(默认 false) | +| `temperature` | number | 否 | 温度参数(0-1) | + +### 消息对象格式 + +```json +{ + "role": "user|assistant", + "content": "string | array" +} +``` + +### 响应格式(非流式) + +```json +{ + "id": "msg_xxx", + "type": "message", + "role": "assistant", + "content": [ + { + "type": "text", + "text": "Hello! How can I help you today?" + } + ], + "model": "claude-3-5-sonnet-20241022", + "stop_reason": "end_turn", + "usage": { + "input_tokens": 10, + "output_tokens": 20 + } +} +``` + +### 流式响应 + +设置 `stream: true` 启用流式响应: + +```json +{ + "model": "claude-3-5-sonnet-20241022", + "max_tokens": 1024, + "messages": [...], + "stream": true +} +``` + +流式响应事件类型: + +- `message_start` - 消息开始 +- `content_block_start` - 内容块开始 +- `content_block_delta` - 内容增量 +- `content_block_stop` - 内容块结束 +- `message_delta` - 消息元数据(usage) +- `message_stop` - 消息结束 + +### 工具使用 + +支持函数调用(Tool Use): + +```json +{ + "model": "claude-3-5-sonnet-20241022", + "max_tokens": 1024, + "messages": [ + { + "role": "user", + "content": "What's the weather like?" + } + ], + "tools": [ + { + "name": "get_weather", + "description": "Get the current weather", + "input_schema": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "City name" + } + }, + "required": ["location"] + } + } + ] +} +``` + +### 多模态支持 + +支持图片输入: + +```json +{ + "role": "user", + "content": [ + { + "type": "image", + "source": { + "type": "base64", + "media_type": "image/png", + "data": "iVBORw0KGgo..." + } + }, + { + "type": "text", + "text": "Describe this image" + } + ] +} +``` + +## POST /v1/messages/count_tokens + +计算消息的 Token 数量。 + +### 请求格式 + +```bash +curl -X POST http://localhost:3456/v1/messages/count_tokens \ + -H "x-api-key: your-api-key" \ + -H "content-type: application/json" \ + -d '{ + "model": "claude-3-5-sonnet-20241022", + "messages": [ + { + "role": "user", + "content": "Hello!" + } + ], + "tools": [], + "system": "You are a helpful assistant." + }' +``` + +### 请求参数 + +| 参数 | 类型 | 必需 | 说明 | +|------|------|------|------| +| `model` | string | 是 | 模型名称 | +| `messages` | array | 是 | 消息数组 | +| `tools` | array | 否 | 工具列表 | +| `system` | string | 否 | 系统提示词 | + +### 响应格式 + +```json +{ + "input_tokens": 42 +} +``` + +## 错误响应 + +### 400 Bad Request + +```json +{ + "error": { + "type": "invalid_request_error", + "message": "messages is required" + } +} +``` + +### 401 Unauthorized + +```json +{ + "error": { + "type": "authentication_error", + "message": "Invalid API key" + } +} +``` + +### 500 Internal Server Error + +```json +{ + "error": { + "type": "api_error", + "message": "Failed to connect to provider" + } +} +``` diff --git a/docs/i18n/zh-CN/docusaurus-plugin-content-docs/server/api/overview.md b/docs/i18n/zh-CN/docusaurus-plugin-content-docs/server/api/overview.md new file mode 100644 index 0000000..0fb0ef5 --- /dev/null +++ b/docs/i18n/zh-CN/docusaurus-plugin-content-docs/server/api/overview.md @@ -0,0 +1,129 @@ +# API 概览 + +Claude Code Router Server 提供了完整的 HTTP API,支持: + +- **消息 API**:兼容 Anthropic Claude API 的消息接口 +- **配置 API**:读取和更新服务器配置 +- **日志 API**:查看和管理服务日志 +- **工具 API**:计算 Token 数量 + +## 基础信息 + +**Base URL**: `http://localhost:3456` + +**认证方式**: API Key(通过 `x-api-key` 请求头) + +```bash +curl -H "x-api-key: your-api-key" http://localhost:3456/api/config +``` + +## API 端点列表 + +### 消息相关 + +| 端点 | 方法 | 描述 | +|------|------|------| +| `/v1/messages` | POST | 发送消息(兼容 Anthropic API) | +| `/v1/messages/count_tokens` | POST | 计算消息的 Token 数量 | + +### 配置管理 + +| 端点 | 方法 | 描述 | +|------|------|------| +| `/api/config` | GET | 获取当前配置 | +| `/api/config` | POST | 更新配置 | +| `/api/transformers` | GET | 获取可用的转换器列表 | + +### 日志管理 + +| 端点 | 方法 | 描述 | +|------|------|------| +| `/api/logs/files` | GET | 获取日志文件列表 | +| `/api/logs` | GET | 获取日志内容 | +| `/api/logs` | DELETE | 清除日志 | + +### 服务管理 + +| 端点 | 方法 | 描述 | +|------|------|------| +| `/api/restart` | POST | 重启服务 | +| `/ui` | GET | Web 管理界面 | +| `/ui/` | GET | Web 管理界面(重定向) | + +## 错误响应 + +所有 API 在发生错误时返回统一的错误格式: + +```json +{ + "error": { + "type": "invalid_request_error", + "message": "错误描述" + } +} +``` + +常见 HTTP 状态码: + +- `200` - 成功 +- `400` - 请求参数错误 +- `401` - 未授权(API Key 无效) +- `404` - 资源不存在 +- `500` - 服务器内部错误 + +## 认证 + +### API Key 认证 + +在请求头中添加 API Key: + +```bash +curl -X POST http://localhost:3456/v1/messages \ + -H "x-api-key: your-api-key" \ + -H "content-type: application/json" \ + -d '...' +``` + +### 无认证模式 + +当没有配置 Providers 时,服务器会监听在 `0.0.0.0` 且无需认证: + +```json5 +{ + "Providers": [] +} +``` + +## 流式响应 + +消息 API 支持流式响应(Server-Sent Events): + +```bash +curl -X POST http://localhost:3456/v1/messages \ + -H "x-api-key: your-api-key" \ + -H "content-type: application/json" \ + -d '{"stream": true, ...}' +``` + +流式响应格式: + +``` +event: message_start +data: {"type":"message_start","message":{...}} + +event: content_block_delta +data: {"type":"content_block_delta","delta":{"type":"text_delta","text":"Hello"}} + +event: message_stop +data: {"type":"message_stop"} +``` + +## 速率限制 + +服务器本身不实现速率限制,建议通过反向代理(如 Nginx)配置。 + +## 版本管理 + +当前 API 版本:`v1` + +所有 `/v1/*` 端点保持向后兼容。 diff --git a/docs/i18n/zh-CN/docusaurus-plugin-content-docs/server/deployment.md b/docs/i18n/zh-CN/docusaurus-plugin-content-docs/server/deployment.md new file mode 100644 index 0000000..dde5e65 --- /dev/null +++ b/docs/i18n/zh-CN/docusaurus-plugin-content-docs/server/deployment.md @@ -0,0 +1,182 @@ +# Server 部署 + +Claude Code Router Server 支持多种部署方式,从本地开发到生产环境。 + +## Docker 部署(推荐) + +### 使用 Docker Hub 镜像 + +```bash +docker run -d \ + --name claude-code-router \ + -p 3456:3456 \ + -v ~/.claude-code-router:/app/.claude-code-router \ + musistudio/claude-code-router:latest +``` + +### 使用 Docker Compose + +创建 `docker-compose.yml`: + +```yaml +version: '3.8' +services: + claude-code-router: + image: musistudio/claude-code-router:latest + container_name: claude-code-router + ports: + - "3456:3456" + volumes: + - ./config:/app/.claude-code-router + environment: + - LOG_LEVEL=info + - HOST=0.0.0.0 + - PORT=3456 + restart: unless-stopped +``` + +启动服务: + +```bash +docker-compose up -d +``` + +### 自定义构建 + +从源码构建 Docker 镜像: + +```bash +git clone https://github.com/musistudio/claude-code-router.git +cd claude-code-router +docker build -t claude-code-router:latest . +``` + +## 配置文件挂载 + +将配置文件挂载到容器中: + +```bash +docker run -d \ + --name claude-code-router \ + -p 3456:3456 \ + -v $(pwd)/config.json:/app/.claude-code-router/config.json \ + musistudio/claude-code-router:latest +``` + +配置文件示例: + +```json5 +{ + // 服务器配置 + "HOST": "0.0.0.0", + "PORT": 3456, + "APIKEY": "your-api-key-here", + + // 日志配置 + "LOG": true, + "LOG_LEVEL": "info", + + // LLM 提供商配置 + "Providers": [ + { + "name": "openai", + "baseUrl": "https://api.openai.com/v1", + "apiKey": "$OPENAI_API_KEY", + "models": ["gpt-4", "gpt-3.5-turbo"] + } + ], + + // 路由配置 + "Router": { + "default": "openai,gpt-4" + } +} +``` + +## 环境变量 + +支持通过环境变量覆盖配置: + +| 变量名 | 说明 | 默认值 | +|--------|------|--------| +| `HOST` | 监听地址 | `127.0.0.1` | +| `PORT` | 监听端口 | `3456` | +| `APIKEY` | API 密钥 | - | +| `LOG_LEVEL` | 日志级别 | `debug` | +| `LOG` | 是否启用日志 | `true` | + +## 生产环境建议 + +### 1. 使用反向代理 + +使用 Nginx 作为反向代理: + +```nginx +server { + listen 80; + server_name your-domain.com; + + location / { + proxy_pass http://localhost:3456; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_cache_bypass $http_upgrade; + } +} +``` + +### 2. 配置 HTTPS + +使用 Let's Encrypt 获取免费证书: + +```bash +sudo certbot --nginx -d your-domain.com +``` + +### 3. 日志管理 + +配置日志轮转和持久化: + +```yaml +version: '3.8' +services: + claude-code-router: + image: musistudio/claude-code-router:latest + volumes: + - ./logs:/app/.claude-code-router/logs + environment: + - LOG_LEVEL=warn +``` + +### 4. 健康检查 + +配置 Docker 健康检查: + +```yaml +healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3456/api/config"] + interval: 30s + timeout: 10s + retries: 3 +``` + +## 访问 Web UI + +部署完成后,访问 Web UI: + +``` +http://localhost:3456/ui/ +``` + +通过 Web UI 可以: +- 查看和管理配置 +- 监控日志 +- 查看服务状态 + +## 二次开发 + +如果需要基于 CCR Server 进行二次开发,请查看 [API 参考](/docs/category/api)。 diff --git a/docs/i18n/zh-CN/docusaurus-plugin-content-docs/server/intro.md b/docs/i18n/zh-CN/docusaurus-plugin-content-docs/server/intro.md new file mode 100644 index 0000000..ddb3ff3 --- /dev/null +++ b/docs/i18n/zh-CN/docusaurus-plugin-content-docs/server/intro.md @@ -0,0 +1,77 @@ +# Server 简介 + +Claude Code Router Server 是一个核心服务组件,负责将 Claude Code 的 API 请求路由到不同的 LLM 提供商。它提供了完整的 HTTP API,支持: + +- **API 请求路由**:将 Anthropic 格式的请求转换为各种提供商的 API 格式 +- **认证与授权**:支持 API Key 认证 +- **配置管理**:动态配置提供商、路由规则和转换器 +- **Web UI**:内置管理界面 +- **日志系统**:完整的请求日志记录 + +## 架构概述 + +``` +┌─────────────┐ ┌──────────────────┐ ┌──────────────┐ +│ Claude Code │────▶│ CCR Server │────▶│ LLM Provider │ +│ Client │ │ (Router + │ │ (OpenAI/ │ +└─────────────┘ │ Transformer) │ │ Gemini/etc)│ + └──────────────────┘ └──────────────┘ + │ + ├─ Web UI + ├─ Config API + └─ Logs API +``` + +## 核心功能 + +### 1. 请求路由 +- 基于 Token 数量的智能路由 +- 项目级路由配置 +- 自定义路由函数 +- 场景化路由(background、think、longContext 等) + +### 2. 请求转换 +- 支持多种 LLM 提供商的 API 格式转换 +- 内置转换器:Anthropic、DeepSeek、Gemini、OpenRouter、Groq 等 +- 可扩展的转换器系统 + +### 3. Agent 系统 +- 插件式的 Agent 架构 +- 内置图片处理 Agent +- 自定义 Agent 支持 + +### 4. 配置管理 +- JSON5 格式配置文件 +- 环境变量插值 +- 配置热更新(需重启服务) + +## 使用场景 + +### 场景一:个人本地服务 +在本地运行服务,供个人 Claude Code 使用: + +```bash +ccr start +``` + +### 场景二:团队共享服务 +使用 Docker 部署,为团队成员提供共享服务: + +```bash +docker run -d -p 3456:3456 musistudio/claude-code-router +``` + +### 场景三:二次开发 +基于暴露的 API 构建自定义应用: + +```bash +GET /api/config +POST /v1/messages +GET /api/logs +``` + +## 下一步 + +- [Docker 部署指南](/docs/server/deployment) - 学习如何部署服务 +- [API 参考](/docs/category/api) - 查看完整的 API 文档 +- [配置说明](/docs/category/server-config) - 了解服务器配置选项 diff --git a/docs/sidebars.ts b/docs/sidebars.ts index 2f771ae..80f9426 100644 --- a/docs/sidebars.ts +++ b/docs/sidebars.ts @@ -8,7 +8,7 @@ const sidebars: SidebarsConfig = { link: { type: 'generated-index', title: 'Claude Code Router Server', - description: '部署和管理 Claude Code Router 服务', + description: 'Deploy and manage Claude Code Router server', slug: 'category/server', }, items: [ @@ -20,7 +20,7 @@ const sidebars: SidebarsConfig = { link: { type: 'generated-index', title: 'API Reference', - description: '服务器 API 接口文档', + description: 'Server API documentation', slug: 'category/api', }, items: [ @@ -36,7 +36,7 @@ const sidebars: SidebarsConfig = { link: { type: 'generated-index', title: 'Server Configuration', - description: '服务器配置说明', + description: 'Server configuration guide', slug: 'category/server-config', }, items: [ @@ -52,7 +52,7 @@ const sidebars: SidebarsConfig = { link: { type: 'generated-index', title: 'Advanced Topics', - description: '高级功能和自定义', + description: 'Advanced features and customization', slug: 'category/server-advanced', }, items: [ @@ -69,7 +69,7 @@ const sidebars: SidebarsConfig = { link: { type: 'generated-index', title: 'Claude Code Router CLI', - description: '命令行工具使用指南', + description: 'Command-line tool usage guide', slug: 'category/cli', }, items: [ @@ -82,7 +82,7 @@ const sidebars: SidebarsConfig = { link: { type: 'generated-index', title: 'CLI Commands', - description: '完整的命令参考', + description: 'Complete command reference', slug: 'category/cli-commands', }, items: [ @@ -98,7 +98,7 @@ const sidebars: SidebarsConfig = { link: { type: 'generated-index', title: 'CLI Configuration', - description: 'CLI 配置说明', + description: 'CLI configuration guide', slug: 'category/cli-config', }, items: [ diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..855420b --- /dev/null +++ b/examples/README.md @@ -0,0 +1,158 @@ +# Preset 示例说明 + +本目录包含 CCR 预设配置的示例文件。 + +## 示例文件 + +### 1. `simple-preset-example.json` - 简单示例 +适合初学者,展示了基本的动态配置功能: +- 密码输入(API Key) +- 单选下拉框(选择模型) +- 确认框(是否使用代理) +- 条件显示(只有选择使用代理时才显示代理地址输入) + +**使用场景**:快速配置单个 Provider + +### 2. `preset-manifest-example.json` - 完整示例 +展示了所有高级功能: +- 多种输入类型(password, select, confirm, number, multiselect) +- 动态选项(从 Providers 配置中提取) +- 复杂条件逻辑(when 条件) +- 模板变量替换({{variable}}) +- 配置映射(configMappings) + +**使用场景**:生产环境的完整配置 + +### 3. `dynamic-preset-example.json` - 多Provider示例 +展示了如何在多个 Provider 之间切换: +- Provider 选择器 +- 根据选择的 Provider 动态显示对应的模型选项 +- 代理配置 +- 高级功能开关 + +## 如何使用这些示例 + +### 方法1:直接复制到预设目录 + +```bash +# 创建预设目录 +mkdir -p ~/.claude-code-router/presets/my-preset + +# 复制示例文件 +cp simple-preset-example.json ~/.claude-code-router/presets/my-preset/manifest.json + +# 应用预设 +ccr my-preset +``` + +### 方法2:修改后使用 + +1. 复制示例文件到本地 +2. 根据需要修改配置 +3. 使用 CLI 安装: + +```bash +ccr preset install ./simple-preset-example.json --name my-preset +``` + +## Schema 字段类型说明 + +| 类型 | 说明 | 适用场景 | +|------|------|----------| +| `password` | 密码输入 | API Key、密钥等敏感信息 | +| `input` | 单行文本 | Base URL、端点地址 | +| `number` | 数字输入 | 超时时间、Token数量 | +| `select` | 单选 | Provider选择、模型选择 | +| `multiselect` | 多选 | 功能开关、标签选择 | +| `confirm` | 确认框 | 是否启用某功能 | +| `editor` | 多行文本 | 自定义配置、脚本 | + +## 条件运算符 + +| 运算符 | 说明 | 示例 | +|--------|------|------| +| `eq` | 等于 | 当 provider == "openai" 时显示 | +| `ne` | 不等于 | 当 mode != "simple" 时显示 | +| `exists` | 字段存在 | 当 apiKey 有值时显示 | +| `gt/lt` | 大于/小于 | 当 timeout > 30 时显示 | + +## 动态选项类型 + +### static - 静态选项 +```json +"options": { + "type": "static", + "options": [ + {"label": "选项1", "value": "value1"}, + {"label": "选项2", "value": "value2"} + ] +} +``` + +### providers - 从 Providers 配置提取 +```json +"options": { + "type": "providers" +} +``` +自动从 `Providers` 数组中提取 name 作为选项。 + +### models - 从指定 Provider 的 models 提取 +```json +"options": { + "type": "models", + "providerField": "{{selectedProvider}}" +} +``` +根据用户选择的 Provider,动态显示该 Provider 的 models。 + +## 模板变量 + +使用 `{{变量名}}` 语法在 template 中引用用户输入: + +```json +"template": { + "Providers": [ + { + "name": "{{providerName}}", + "api_key": "{{apiKey}}" + } + ] +} +``` + +## 配置映射 + +对于复杂的配置需求,使用 `configMappings` 精确控制值的位置: + +```json +"configMappings": [ + { + "target": "Providers[0].api_key", + "value": "{{apiKey}}" + }, + { + "target": "PROXY_URL", + "value": "{{proxyUrl}}", + "when": { + "field": "useProxy", + "operator": "eq", + "value": true + } + } +] +``` + +## 最佳实践 + +1. **提供默认值**:为非必填项设置合理的 `defaultValue` +2. **清晰的标签**:使用用户友好的 `label` 和 `prompt` +3. **条件显示**:使用 `when` 避免显示无关选项 +4. **输入验证**:使用 `validator` 或 `min/max` 确保输入有效 +5. **分组配置**:相关字段使用相同的前缀(如 `proxy*`) +6. **版本管理**:在 metadata 中记录版本和变更 + +## 更多帮助 + +- 查看完整文档:[Presets 配置指南](../docs/docs/server/advanced/presets.md) +- 查看类型定义:[types.ts](../packages/shared/src/preset/types.ts) diff --git a/examples/dynamic-preset-example.json b/examples/dynamic-preset-example.json new file mode 100644 index 0000000..5dbfd3d --- /dev/null +++ b/examples/dynamic-preset-example.json @@ -0,0 +1,246 @@ +{ + "metadata": { + "name": "multi-provider-preset", + "version": "1.0.0", + "description": "示例预设:支持多provider选择和动态配置", + "author": "CCR Team", + "keywords": ["example", "multi-provider", "dynamic"], + "ccrVersion": "2.0.0" + }, + "schema": [ + { + "id": "providerChoice", + "type": "select", + "label": "选择Provider", + "prompt": "请选择要使用的LLM提供商", + "options": { + "type": "static", + "options": [ + { + "label": "OpenAI", + "value": "openai", + "description": "使用OpenAI的GPT模型" + }, + { + "label": "DeepSeek", + "value": "deepseek", + "description": "使用DeepSeek的高性价比模型" + }, + { + "label": "Gemini", + "value": "gemini", + "description": "使用Google的Gemini模型" + } + ] + }, + "required": true, + "defaultValue": "openai" + }, + { + "id": "apiKey", + "type": "password", + "label": "API Key", + "prompt": "请输入您的API Key", + "placeholder": "sk-...", + "required": true, + "when": { + "field": "providerChoice", + "operator": "exists" + } + }, + { + "id": "baseUrl", + "type": "input", + "label": "Base URL(可选)", + "prompt": "自定义API Base URL,留空使用默认值", + "required": false, + "when": { + "field": "providerChoice", + "operator": "exists" + } + }, + { + "id": "modelChoice", + "type": "select", + "label": "选择模型", + "prompt": "请选择要使用的模型", + "options": { + "type": "static", + "options": [ + { + "label": "GPT-4o", + "value": "gpt-4o" + }, + { + "label": "GPT-4o-mini", + "value": "gpt-4o-mini" + }, + { + "label": "GPT-3.5-turbo", + "value": "gpt-3.5-turbo" + } + ] + }, + "required": true, + "when": { + "field": "providerChoice", + "operator": "eq", + "value": "openai" + }, + "defaultValue": "gpt-4o" + }, + { + "id": "deepseekModelChoice", + "type": "select", + "label": "选择模型", + "prompt": "请选择要使用的DeepSeek模型", + "options": { + "type": "static", + "options": [ + { + "label": "DeepSeek-V3", + "value": "deepseek-v3" + }, + { + "label": "DeepSeek-Chat", + "value": "deepseek-chat" + } + ] + }, + "required": true, + "when": { + "field": "providerChoice", + "operator": "eq", + "value": "deepseek" + }, + "defaultValue": "deepseek-v3" + }, + { + "id": "useProxy", + "type": "confirm", + "label": "使用代理", + "prompt": "是否通过代理访问API?", + "defaultValue": false + }, + { + "id": "proxyUrl", + "type": "input", + "label": "代理URL", + "prompt": "请输入代理地址(如:http://127.0.0.1:7890)", + "placeholder": "http://127.0.0.1:7890", + "required": true, + "when": { + "field": "useProxy", + "operator": "eq", + "value": true + } + }, + { + "id": "maxTokens", + "type": "number", + "label": "最大Token数", + "prompt": "设置单次请求的最大token数量", + "min": 100, + "max": 128000, + "defaultValue": 4096, + "required": false + }, + { + "id": "advancedSettings", + "type": "confirm", + "label": "高级设置", + "prompt": "是否配置高级选项?", + "defaultValue": false + }, + { + "id": "temperature", + "type": "number", + "label": "Temperature", + "prompt": "控制生成随机性(0-2)", + "min": 0, + "max": 2, + "defaultValue": 0.7, + "required": false, + "when": { + "field": "advancedSettings", + "operator": "eq", + "value": true + } + }, + { + "id": "features", + "type": "multiselect", + "label": "启用功能", + "prompt": "选择要启用的功能", + "options": { + "type": "static", + "options": [ + { + "label": "流式输出", + "value": "stream" + }, + { + "label": "工具调用", + "value": "tools" + }, + { + "label": "长上下文支持", + "value": "longContext" + } + ] + }, + "defaultValue": ["stream"], + "required": false + } + ], + "template": { + "Providers": [ + { + "name": "{{providerChoice}}", + "api_base_url": "{{baseUrl}}", + "api_key": "{{apiKey}}", + "models": ["{{modelChoice}}", "{{deepseekModelChoice}}"] + } + ], + "Router": { + "default": "{{providerChoice}}/{{modelChoice}}{{deepseekModelChoice}}" + }, + "PROXY_URL": "{{proxyUrl}}", + "API_TIMEOUT_MS": 60000 + }, + "configMappings": [ + { + "target": "Providers[0].name", + "value": "{{providerChoice}}" + }, + { + "target": "Providers[0].api_key", + "value": "{{apiKey}}" + }, + { + "target": "Providers[0].api_base_url", + "value": "{{baseUrl}}", + "when": { + "field": "baseUrl", + "operator": "exists" + } + }, + { + "target": "Router.default", + "value": "{{providerChoice}}/{{modelChoice}}{{deepseekModelChoice}}" + }, + { + "target": "PROXY_URL", + "value": "{{proxyUrl}}", + "when": { + "field": "useProxy", + "operator": "eq", + "value": true + } + }, + { + "target": "API_TIMEOUT_MS", + "value": 60000 + } + ] +} diff --git a/examples/preset-manifest-example.json b/examples/preset-manifest-example.json new file mode 100644 index 0000000..910421f --- /dev/null +++ b/examples/preset-manifest-example.json @@ -0,0 +1,268 @@ +{ + "name": "multi-provider-example", + "version": "1.0.0", + "description": "多Provider配置示例 - 支持OpenAI和DeepSeek切换", + "author": "CCR Team", + "keywords": ["openai", "deepseek", "multi-provider"], + "ccrVersion": "2.0.0", + "Providers": [ + { + "name": "openai", + "api_base_url": "https://api.openai.com/v1", + "models": ["gpt-4o", "gpt-4o-mini", "gpt-3.5-turbo"] + }, + { + "name": "deepseek", + "api_base_url": "https://api.deepseek.com", + "models": ["deepseek-v3", "deepseek-chat"] + } + ], + "schema": [ + { + "id": "primaryProvider", + "type": "select", + "label": "主要Provider", + "prompt": "选择您主要使用的LLM提供商", + "options": { + "type": "providers" + }, + "required": true, + "defaultValue": "openai" + }, + { + "id": "apiKey", + "type": "password", + "label": "API Key", + "prompt": "请输入您的API Key(将从环境变量或安全存储中读取)", + "placeholder": "sk-...", + "required": true, + "when": { + "field": "primaryProvider", + "operator": "exists" + } + }, + { + "id": "customBaseUrl", + "type": "input", + "label": "自定义Base URL", + "prompt": "如果使用代理或自定义端点,请输入Base URL(留空使用默认值)", + "placeholder": "https://api.openai.com/v1", + "required": false, + "when": { + "field": "primaryProvider", + "operator": "exists" + } + }, + { + "id": "defaultModel", + "type": "select", + "label": "默认模型", + "prompt": "选择默认使用的模型", + "options": { + "type": "models", + "providerField": "{{primaryProvider}}" + }, + "required": true, + "defaultValue": "gpt-4o", + "when": { + "field": "primaryProvider", + "operator": "eq", + "value": "openai" + } + }, + { + "id": "backgroundModel", + "type": "select", + "label": "后台任务模型", + "prompt": "用于后台任务的轻量级模型", + "options": { + "type": "models", + "providerField": "{{primaryProvider}}" + }, + "required": false, + "when": { + "field": "primaryProvider", + "operator": "exists" + } + }, + { + "id": "enableProxy", + "type": "confirm", + "label": "启用代理", + "prompt": "是否通过代理访问API?", + "defaultValue": false + }, + { + "id": "proxyUrl", + "type": "input", + "label": "代理地址", + "prompt": "输入代理服务器地址", + "placeholder": "http://127.0.0.1:7890", + "required": true, + "when": { + "field": "enableProxy", + "operator": "eq", + "value": true + } + }, + { + "id": "maxTokens", + "type": "number", + "label": "最大Token数", + "prompt": "设置单次请求的最大token数量", + "min": 100, + "max": 128000, + "defaultValue": 4096, + "required": false + }, + { + "id": "timeout", + "type": "number", + "label": "请求超时(秒)", + "prompt": "API请求超时时间", + "min": 10, + "max": 300, + "defaultValue": 60, + "required": false + }, + { + "id": "enableFeatures", + "type": "multiselect", + "label": "启用功能", + "prompt": "选择要启用的功能", + "options": { + "type": "static", + "options": [ + { + "label": "流式输出", + "value": "stream", + "description": "实时显示AI响应" + }, + { + "label": "工具调用", + "value": "tools", + "description": "启用Function Calling功能" + }, + { + "label": "长上下文", + "value": "longContext", + "description": "支持长文本处理" + }, + { + "label": "思维链", + "value": "think", + "description": "在思考模式中使用" + } + ] + }, + "defaultValue": ["stream", "tools"], + "required": false + }, + { + "id": "advancedMode", + "type": "confirm", + "label": "高级模式", + "prompt": "启用高级配置选项?", + "defaultValue": false + }, + { + "id": "temperature", + "type": "number", + "label": "Temperature", + "prompt": "控制生成随机性(0-2,值越高越随机)", + "min": 0, + "max": 2, + "defaultValue": 0.7, + "required": false, + "when": { + "field": "advancedMode", + "operator": "eq", + "value": true + } + }, + { + "id": "logLevel", + "type": "select", + "label": "日志级别", + "prompt": "设置详细的日志级别", + "options": { + "type": "static", + "options": [ + { + "label": "错误", + "value": "error" + }, + { + "label": "警告", + "value": "warn" + }, + { + "label": "信息", + "value": "info" + }, + { + "label": "调试", + "value": "debug" + } + ] + }, + "defaultValue": "info", + "required": false, + "when": { + "field": "advancedMode", + "operator": "eq", + "value": true + } + } + ], + "template": { + "Providers": [ + { + "name": "{{primaryProvider}}", + "api_base_url": "{{customBaseUrl}}", + "api_key": "{{apiKey}}", + "models": ["{{defaultModel}}", "{{backgroundModel}}"] + } + ], + "Router": { + "default": "{{primaryProvider}}/{{defaultModel}}", + "background": "{{backgroundModel}}", + "think": "{{primaryProvider}}/{{defaultModel}}" + }, + "PROXY_URL": "{{proxyUrl}}", + "API_TIMEOUT_MS": 60000, + "LOG_LEVEL": "info" + }, + "configMappings": [ + { + "target": "Providers[0].api_base_url", + "value": "{{customBaseUrl}}", + "when": { + "field": "customBaseUrl", + "operator": "exists" + } + }, + { + "target": "PROXY_URL", + "value": "{{proxyUrl}}", + "when": { + "field": "enableProxy", + "operator": "eq", + "value": true + } + }, + { + "target": "API_TIMEOUT_MS", + "value": 60000 + }, + { + "target": "LOG_LEVEL", + "value": "{{logLevel}}", + "when": { + "field": "advancedMode", + "operator": "eq", + "value": true + } + } + ] +} diff --git a/examples/simple-preset-example.json b/examples/simple-preset-example.json new file mode 100644 index 0000000..69c9ed3 --- /dev/null +++ b/examples/simple-preset-example.json @@ -0,0 +1,87 @@ +{ + "name": "simple-openai-preset", + "version": "1.0.0", + "description": "简单的OpenAI配置预设", + "author": "Your Name", + "keywords": ["openai", "simple"], + "schema": [ + { + "id": "apiKey", + "type": "password", + "label": "OpenAI API Key", + "prompt": "请输入您的OpenAI API Key", + "placeholder": "sk-...", + "required": true + }, + { + "id": "model", + "type": "select", + "label": "选择模型", + "prompt": "选择要使用的GPT模型", + "options": { + "type": "static", + "options": [ + { + "label": "GPT-4o (推荐)", + "value": "gpt-4o" + }, + { + "label": "GPT-4o-mini (快速)", + "value": "gpt-4o-mini" + }, + { + "label": "GPT-3.5 Turbo (经济)", + "value": "gpt-3.5-turbo" + } + ] + }, + "required": true, + "defaultValue": "gpt-4o" + }, + { + "id": "useProxy", + "type": "confirm", + "label": "使用代理", + "prompt": "是否需要通过代理访问OpenAI API?", + "defaultValue": false + }, + { + "id": "proxyUrl", + "type": "input", + "label": "代理地址", + "prompt": "请输入代理地址", + "placeholder": "http://127.0.0.1:7890", + "required": true, + "when": { + "field": "useProxy", + "operator": "eq", + "value": true + } + } + ], + "template": { + "Providers": [ + { + "name": "openai", + "api_base_url": "https://api.openai.com/v1", + "api_key": "{{apiKey}}", + "models": ["{{model}}"] + } + ], + "Router": { + "default": "openai/{{model}}" + }, + "PROXY_URL": "{{proxyUrl}}" + }, + "configMappings": [ + { + "target": "PROXY_URL", + "value": "{{proxyUrl}}", + "when": { + "field": "useProxy", + "operator": "eq", + "value": true + } + } + ] +} diff --git a/packages/cli/src/utils/preset/install.ts b/packages/cli/src/utils/preset/install.ts index 35934f0..9031f4c 100644 --- a/packages/cli/src/utils/preset/install.ts +++ b/packages/cli/src/utils/preset/install.ts @@ -18,8 +18,14 @@ import { findPresetFile, isPresetInstalled, ManifestFile, - PresetFile + PresetFile, + RequiredInput, + UserInputValues, + applyConfigMappings, + replaceTemplateVariables, + setValueByPath, } from '@CCR/shared'; +import { collectUserInputs } from '../prompt/schema-input'; // 重新导出 loadPreset export { loadPresetShared as loadPreset }; @@ -34,67 +40,38 @@ const BOLDCYAN = "\x1B[1m\x1B[36m"; const DIM = "\x1B[2m"; /** - * 收集缺失的敏感信息 + * 应用用户输入到配置(新版schema) */ -async function collectSensitiveInputs( - preset: PresetFile -): Promise> { - const inputs: Record = {}; +function applyUserInputs( + preset: PresetFile, + values: UserInputValues +): PresetConfigSection { + let config = { ...preset.config }; - if (!preset.requiredInputs || preset.requiredInputs.length === 0) { - return inputs; + // 1. 先应用 template(如果存在) + if (preset.template) { + config = replaceTemplateVariables(preset.template, values) as any; } - console.log(`\n${BOLDYELLOW}This preset requires additional information:${RESET}\n`); - - for (const inputField of preset.requiredInputs) { - let value: string; - - // 尝试从环境变量获取 - const envVarName = inputField.placeholder; - if (envVarName && process.env[envVarName]) { - const useEnv = await confirm({ - message: `Found ${envVarName} in environment. Use it?`, - default: true, - }); - - if (useEnv) { - value = process.env[envVarName]!; - inputs[inputField.field] = value; - console.log(`${GREEN}✓${RESET} Using ${envVarName} from environment\n`); - continue; - } - } - - // 提示用户输入 - value = await password({ - message: inputField.prompt || `Enter ${inputField.field}:`, - mask: '*', - }); - - if (!value || value.trim() === '') { - console.error(`${YELLOW}Error:${RESET} ${inputField.field} is required`); - process.exit(1); - } - - // 验证输入 - if (inputField.validator) { - const regex = typeof inputField.validator === 'string' - ? new RegExp(inputField.validator) - : inputField.validator; - - if (!regex.test(value)) { - console.error(`${YELLOW}Error:${RESET} Invalid format for ${inputField.field}`); - console.error(` Expected: ${inputField.validator}`); - process.exit(1); - } - } - - inputs[inputField.field] = value; - console.log(''); + // 2. 再应用 configMappings(如果存在) + if (preset.configMappings && preset.configMappings.length > 0) { + config = applyConfigMappings(preset.configMappings, values, config); } - return inputs; + // 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; } /** @@ -136,7 +113,7 @@ export async function applyPresetCli( // 检查是否已经配置了敏感信息(例如api_key) const hasSecrets = existingManifest.Providers?.some((p: any) => p.api_key && p.api_key !== ''); if (hasSecrets) { - console.log(`\n${GREEN}✓${RESET} Preset already configured with secrets`); + console.log(`\n${GREEN}✓${RESET} Preset already configured`); console.log(`${DIM}You can use this preset with: ccr ${presetName}${RESET}\n`); return; } @@ -144,31 +121,34 @@ export async function applyPresetCli( // manifest不存在,继续配置流程 } - // 收集敏感信息 - const sensitiveInputs = await collectSensitiveInputs(preset); + // 收集用户输入 + let userInputs: UserInputValues = {}; + + // 使用 schema 系统 + if (preset.schema && preset.schema.length > 0) { + userInputs = await collectUserInputs(preset.schema, preset.config); + } + + // 应用用户输入到配置 + const finalConfig = applyUserInputs(preset, userInputs); // 读取现有的manifest并更新 const manifest: ManifestFile = { ...(preset.metadata || {}), - ...preset.config, + ...finalConfig, }; - // 将secrets信息应用到manifest中 - for (const [fieldPath, value] of Object.entries(sensitiveInputs)) { - const keys = fieldPath.split(/[.\[\]]+/).filter(k => k !== ''); - let current = manifest as any; - for (let i = 0; i < keys.length - 1; i++) { - const key = keys[i]; - if (!current[key]) { - current[key] = {}; - } - current = current[key]; - } - current[keys[keys.length - 1]] = value; + // 保存 schema(如果存在) + if (preset.schema) { + manifest.schema = preset.schema; } - if (preset.requiredInputs) { - manifest.requiredInputs = preset.requiredInputs; + // 保存其他配置 + if (preset.template) { + manifest.template = preset.template; + } + if (preset.configMappings) { + manifest.configMappings = preset.configMappings; } // 保存到解压目录的manifest.json @@ -177,7 +157,7 @@ export async function applyPresetCli( // 显示摘要 console.log(`\n${BOLDGREEN}✓ Preset configured successfully!${RESET}\n`); console.log(`${BOLDCYAN}Preset directory:${RESET} ${presetDir}`); - console.log(`${BOLDCYAN}Secrets configured:${RESET} ${Object.keys(sensitiveInputs).length}`); + console.log(`${BOLDCYAN}Inputs configured:${RESET} ${Object.keys(userInputs).length}`); if (preset.metadata?.description) { console.log(`\n${BOLDCYAN}Description:${RESET} ${preset.metadata.description}`); @@ -193,7 +173,7 @@ export async function applyPresetCli( } console.log(`\n${GREEN}Use this preset:${RESET} ccr ${presetName} "your prompt"`); - console.log(`${DIM}Note: Secrets are stored in the manifest file${RESET}\n`); + console.log(`${DIM}Note: Configuration is stored in the manifest file${RESET}\n`); } catch (error: any) { console.error(`\n${YELLOW}Error applying preset:${RESET} ${error.message}`); diff --git a/packages/cli/src/utils/prompt/schema-input.ts b/packages/cli/src/utils/prompt/schema-input.ts new file mode 100644 index 0000000..7c40cac --- /dev/null +++ b/packages/cli/src/utils/prompt/schema-input.ts @@ -0,0 +1,230 @@ +/** + * 动态配置 CLI 交互处理器 + * 处理各种输入类型的用户交互 + */ + +import { + RequiredInput, + InputType, + UserInputValues, + PresetConfigSection, + shouldShowField, + resolveOptions, + validateInput, + getDefaultValue, + sortFieldsByDependencies, + getAffectedFields, +} from '@musistudio/claude-code-router-shared'; +import { input, confirm, select, password } from '@inquirer/prompts'; + +// ANSI 颜色代码 +export const COLORS = { + RESET: "\x1B[0m", + GREEN: "\x1B[32m", + YELLOW: "\x1B[33m", + BOLDYELLOW: "\x1B[1m\x1B[33m", + BOLDCYAN: "\x1B[1m\x1B[36m", + DIM: "\x1B[2m", + BOLDGREEN: "\x1B[1m\x1B[32m", +}; + +/** + * 收集用户输入(支持动态配置) + */ +export async function collectUserInputs( + schema: RequiredInput[], + presetConfig: PresetConfigSection, + existingValues?: UserInputValues +): Promise { + // 按依赖关系排序 + const sortedFields = sortFieldsByDependencies(schema); + + // 初始化值 + const values: UserInputValues = { ...existingValues }; + + // 收集所有输入 + for (const field of sortedFields) { + // 检查是否应该显示此字段 + if (!shouldShowField(field, values)) { + // 跳过,并清除该字段的值(如果之前存在) + delete values[field.id]; + continue; + } + + // 如果已有值且不是初始收集,跳过 + if (existingValues && field.id in existingValues) { + continue; + } + + // 获取输入值 + const value = await promptField(field, presetConfig, values); + + // 验证 + const validation = validateInput(field, value); + if (!validation.valid) { + console.error(`${COLORS.YELLOW}Error:${COLORS.RESET} ${validation.error}`); + // 对于必填字段,抛出错误 + if (field.required !== false) { + throw new Error(validation.error); + } + } + + values[field.id] = value; + console.log(''); + } + + return values; +} + +/** + * 重新收集受影响的字段(当某个字段值变化时) + */ +export async function recollectAffectedFields( + changedFieldId: string, + schema: RequiredInput[], + presetConfig: PresetConfigSection, + currentValues: UserInputValues +): Promise { + const affectedFields = getAffectedFields(changedFieldId, schema); + const sortedFields = sortFieldsByDependencies(schema); + + const values = { ...currentValues }; + + // 对受影响的字段重新收集输入 + for (const fieldId of affectedFields) { + const field = sortedFields.find(f => f.id === fieldId); + if (!field) { + continue; + } + + // 检查是否应该显示 + if (!shouldShowField(field, values)) { + delete values[field.id]; + continue; + } + + // 重新收集输入 + const value = await promptField(field, presetConfig, values); + values[field.id] = value; + + // 级联更新:如果这个字段的变化又影响了其他字段 + const newAffected = getAffectedFields(field.id, schema); + for (const newAffectedId of newAffected) { + if (!affectedFields.has(newAffectedId)) { + affectedFields.add(newAffectedId); + } + } + } + + return values; +} + +/** + * 提示单个字段 + */ +async function promptField( + field: RequiredInput, + presetConfig: PresetConfigSection, + currentValues: UserInputValues +): Promise { + const label = field.label || field.id; + const message = field.prompt || `${label}:`; + + switch (field.type) { + case InputType.PASSWORD: + return await password({ + message, + mask: '*', + }); + + case InputType.INPUT: + return await input({ + message, + default: field.defaultValue, + }); + + case InputType.NUMBER: + const numStr = await input({ + message, + default: String(field.defaultValue ?? 0), + }); + return Number(numStr); + + case InputType.CONFIRM: + return await confirm({ + message, + default: field.defaultValue ?? false, + }); + + case InputType.SELECT: { + const options = resolveOptions(field, presetConfig, currentValues); + if (options.length === 0) { + console.warn(`${COLORS.YELLOW}Warning:${COLORS.RESET} No options available for ${label}`); + return field.defaultValue; + } + + return await select({ + message, + choices: options.map(opt => ({ + name: opt.label, + value: opt.value, + description: opt.description, + disabled: opt.disabled, + })), + default: field.defaultValue, + }); + } + + case InputType.MULTISELECT: { + const options = resolveOptions(field, presetConfig, currentValues); + if (options.length === 0) { + console.warn(`${COLORS.YELLOW}Warning:${COLORS.RESET} No options available for ${label}`); + return field.defaultValue ?? []; + } + + // @inquirer/prompts 没有多选,使用 checkbox + const { checkbox } = await import('@inquirer/prompts'); + return await checkbox({ + message, + choices: options.map(opt => ({ + name: opt.label, + value: opt.value, + checked: Array.isArray(field.defaultValue) && field.defaultValue.includes(opt.value), + })), + }); + } + + case InputType.EDITOR: { + const { editor } = await import('@inquirer/prompts'); + return await editor({ + message, + default: field.defaultValue, + }); + } + + default: + // 默认使用 input + return await input({ + message, + default: field.defaultValue, + }); + } +} + +/** + * 收集敏感信息(兼容旧版) + */ +export async function collectSensitiveInputs( + schema: RequiredInput[], + presetConfig: PresetConfigSection, + existingValues?: UserInputValues +): Promise { + console.log(`\n${COLORS.BOLDYELLOW}This preset requires additional information:${COLORS.RESET}\n`); + + const values = await collectUserInputs(schema, presetConfig, existingValues); + + // 显示摘要 + console.log(`${COLORS.GREEN}✓${COLORS.RESET} All required information collected\n`); + + return values; +} diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 21c0121..0c969bf 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -7,3 +7,4 @@ export * from './preset/merge'; export * from './preset/install'; export * from './preset/export'; export * from './preset/readPreset'; +export * from './preset/schema'; diff --git a/packages/shared/src/preset/schema.ts b/packages/shared/src/preset/schema.ts new file mode 100644 index 0000000..7d32d01 --- /dev/null +++ b/packages/shared/src/preset/schema.ts @@ -0,0 +1,530 @@ +/** + * 动态配置 Schema 处理器 + * 负责解析和验证配置 schema,处理条件逻辑和变量替换 + */ + +import { + RequiredInput, + InputType, + Condition, + DynamicOptions, + InputOption, + ConfigMapping, + TemplateConfig, + PresetConfigSection, +} from './types'; + +// 用户输入值集合 +export interface UserInputValues { + [inputId: string]: any; +} + +/** + * 解析字段路径(支持数组和嵌套) + * 例如:Providers[0].name => ['Providers', '0', 'name'] + */ +export function parseFieldPath(path: string): string[] { + const regex = /(\w+)|\[(\d+)\]/g; + const parts: string[] = []; + let match; + + while ((match = regex.exec(path)) !== null) { + parts.push(match[1] || match[2]); + } + + return parts; +} + +/** + * 根据字段路径获取对象中的值 + */ +export function getValueByPath(obj: any, path: string): any { + const parts = parseFieldPath(path); + let current = obj; + + for (const part of parts) { + if (current == null) { + return undefined; + } + current = current[part]; + } + + return current; +} + +/** + * 根据字段路径设置对象中的值 + */ +export function setValueByPath(obj: any, path: string, value: any): void { + const parts = parseFieldPath(path); + const lastKey = parts.pop()!; + let current = obj; + + for (const part of parts) { + if (!(part in current)) { + // 判断是数组还是对象 + const nextPart = parts[parts.indexOf(part) + 1]; + if (nextPart && /^\d+$/.test(nextPart)) { + current[part] = []; + } else { + current[part] = {}; + } + } + current = current[part]; + } + + current[lastKey] = value; +} + +/** + * 评估条件表达式 + */ +export function evaluateCondition( + condition: Condition, + values: UserInputValues +): boolean { + const actualValue = values[condition.field]; + + // 处理 exists 操作符 + if (condition.operator === 'exists') { + return actualValue !== undefined && actualValue !== null; + } + + // 处理 in 操作符 + if (condition.operator === 'in') { + return Array.isArray(condition.value) && condition.value.includes(actualValue); + } + + // 处理 nin 操作符 + if (condition.operator === 'nin') { + return Array.isArray(condition.value) && !condition.value.includes(actualValue); + } + + // 处理其他操作符 + switch (condition.operator) { + case 'eq': + return actualValue === condition.value; + case 'ne': + return actualValue !== condition.value; + case 'gt': + return actualValue > condition.value; + case 'lt': + return actualValue < condition.value; + case 'gte': + return actualValue >= condition.value; + case 'lte': + return actualValue <= condition.value; + default: + // 默认使用 eq + return actualValue === condition.value; + } +} + +/** + * 评估多个条件(AND 逻辑) + */ +export function evaluateConditions( + conditions: Condition | Condition[], + values: UserInputValues +): boolean { + if (!conditions) { + return true; + } + + if (!Array.isArray(conditions)) { + return evaluateCondition(conditions, values); + } + + // 如果是数组,使用 AND 逻辑(所有条件都必须满足) + return conditions.every(condition => evaluateCondition(condition, values)); +} + +/** + * 判断字段是否应该显示 + */ +export function shouldShowField( + field: RequiredInput, + values: UserInputValues +): boolean { + if (!field.when) { + return true; + } + + return evaluateConditions(field.when, values); +} + +/** + * 获取动态选项列表 + */ +export function getDynamicOptions( + dynamicOptions: DynamicOptions, + presetConfig: PresetConfigSection, + values: UserInputValues +): InputOption[] { + switch (dynamicOptions.type) { + case 'static': + return dynamicOptions.options || []; + + case 'providers': { + // 从预设的 Providers 中提取选项 + const providers = presetConfig.Providers || []; + return providers.map((p: any) => ({ + label: p.name || p.id || String(p), + value: p.name || p.id || String(p), + description: p.api_base_url, + })); + } + + case 'models': { + // 从指定 provider 的 models 中提取 + const providerField = dynamicOptions.providerField; + if (!providerField) { + return []; + } + + // 解析 provider 引用(如 {{selectedProvider}}) + const providerId = String(providerField).replace(/^{{(.+)}}$/, '$1'); + const selectedProvider = values[providerId]; + + if (!selectedProvider || !presetConfig.Providers) { + return []; + } + + // 查找对应的 provider + const provider = presetConfig.Providers.find( + (p: any) => p.name === selectedProvider || p.id === selectedProvider + ); + + if (!provider || !provider.models) { + return []; + } + + return provider.models.map((model: string) => ({ + label: model, + value: model, + })); + } + + case 'custom': + // 预留,暂未实现 + return []; + + default: + return []; + } +} + +/** + * 解析选项(支持静态和动态选项) + */ +export function resolveOptions( + field: RequiredInput, + presetConfig: PresetConfigSection, + values: UserInputValues +): InputOption[] { + if (!field.options) { + return []; + } + + // 判断是静态选项还是动态选项 + const options = field.options as any; + + if (Array.isArray(options)) { + // 静态选项数组 + return options as InputOption[]; + } + + if (options.type) { + // 动态选项 + return getDynamicOptions(options, presetConfig, values); + } + + return []; +} + +/** + * 模板变量替换 + * 支持 {{variable}} 语法 + */ +export function replaceTemplateVariables( + template: any, + values: UserInputValues +): any { + if (template === null || template === undefined) { + return template; + } + + // 处理字符串 + if (typeof template === 'string') { + return template.replace(/\{\{(\w+)\}\}/g, (_, key) => { + return values[key] !== undefined ? String(values[key]) : ''; + }); + } + + // 处理数组 + if (Array.isArray(template)) { + return template.map(item => replaceTemplateVariables(item, values)); + } + + // 处理对象 + if (typeof template === 'object') { + const result: any = {}; + for (const [key, value] of Object.entries(template)) { + result[key] = replaceTemplateVariables(value, values); + } + return result; + } + + // 其他类型直接返回 + return template; +} + +/** + * 应用配置映射 + */ +export function applyConfigMappings( + mappings: ConfigMapping[], + values: UserInputValues, + config: PresetConfigSection +): PresetConfigSection { + const result = { ...config }; + + for (const mapping of mappings) { + // 检查条件 + if (mapping.when && !evaluateConditions(mapping.when, values)) { + continue; + } + + // 解析值 + let value: any; + if (typeof mapping.value === 'string' && mapping.value.startsWith('{{')) { + // 变量引用 + const varName = mapping.value.replace(/^{{(.+)}}$/, '$1'); + value = values[varName]; + } else { + // 固定值 + value = mapping.value; + } + + // 应用到目标路径 + setValueByPath(result, mapping.target, value); + } + + return result; +} + +/** + * 验证用户输入 + */ +export function validateInput( + field: RequiredInput, + value: any +): { valid: boolean; error?: string } { + // 检查必填 + if (field.required !== false && (value === undefined || value === null || value === '')) { + return { + valid: false, + error: `${field.label || field.id} is required`, + }; + } + + // 如果值为空且非必填,跳过验证 + if (!value && field.required === false) { + return { valid: true }; + } + + // 类型检查 + switch (field.type) { + case InputType.NUMBER: + if (isNaN(Number(value))) { + return { + valid: false, + error: `${field.label || field.id} must be a number`, + }; + } + const numValue = Number(value); + if (field.min !== undefined && numValue < field.min) { + return { + valid: false, + error: `${field.label || field.id} must be at least ${field.min}`, + }; + } + if (field.max !== undefined && numValue > field.max) { + return { + valid: false, + error: `${field.label || field.id} must be at most ${field.max}`, + }; + } + break; + + case InputType.SELECT: + case InputType.MULTISELECT: + // 检查值是否在选项中 + // 这里暂时跳过,因为需要动态获取选项 + break; + } + + // 自定义验证器 + if (field.validator) { + if (field.validator instanceof RegExp) { + if (!field.validator.test(String(value))) { + return { + valid: false, + error: `${field.label || field.id} format is invalid`, + }; + } + } else if (typeof field.validator === 'string') { + const regex = new RegExp(field.validator); + if (!regex.test(String(value))) { + return { + valid: false, + error: `${field.label || field.id} format is invalid`, + }; + } + } else if (typeof field.validator === 'function') { + const result = field.validator(value); + if (result === false) { + return { + valid: false, + error: `${field.label || field.id} is invalid`, + }; + } else if (typeof result === 'string') { + return { + valid: false, + error: result, + }; + } + } + } + + return { valid: true }; +} + +/** + * 获取字段的默认值 + */ +export function getDefaultValue(field: RequiredInput): any { + if (field.defaultValue !== undefined) { + return field.defaultValue; + } + + // 根据类型返回默认值 + switch (field.type) { + case InputType.CONFIRM: + return false; + case InputType.MULTISELECT: + return []; + case InputType.NUMBER: + return 0; + default: + return ''; + } +} + +/** + * 根据依赖关系排序字段 + * 确保被依赖的字段排在前面 + */ +export function sortFieldsByDependencies( + fields: RequiredInput[] +): RequiredInput[] { + const sorted: RequiredInput[] = []; + const visited = new Set(); + + function visit(field: RequiredInput) { + if (visited.has(field.id)) { + return; + } + + visited.add(field.id); + + // 先处理依赖的字段 + const dependencies = field.dependsOn || []; + for (const depId of dependencies) { + const depField = fields.find(f => f.id === depId); + if (depField) { + visit(depField); + } + } + + // 从 when 条件中提取依赖 + if (field.when) { + const conditions = Array.isArray(field.when) ? field.when : [field.when]; + for (const cond of conditions) { + const depField = fields.find(f => f.id === cond.field); + if (depField) { + visit(depField); + } + } + } + + sorted.push(field); + } + + for (const field of fields) { + visit(field); + } + + return sorted; +} + +/** + * 构建字段依赖图(用于优化更新顺序) + */ +export function buildDependencyGraph( + fields: RequiredInput[] +): Map> { + const graph = new Map>(); + + for (const field of fields) { + const deps = new Set(); + + // 从 dependsOn 提取依赖 + if (field.dependsOn) { + for (const dep of field.dependsOn) { + deps.add(dep); + } + } + + // 从 when 条件提取依赖 + if (field.when) { + const conditions = Array.isArray(field.when) ? field.when : [field.when]; + for (const cond of conditions) { + deps.add(cond.field); + } + } + + // 从动态选项提取依赖 + if (field.options) { + const options = field.options as any; + if (options.type === 'models' && options.providerField) { + const providerId = String(options.providerField).replace(/^{{(.+)}}$/, '$1'); + deps.add(providerId); + } + } + + graph.set(field.id, deps); + } + + return graph; +} + +/** + * 获取受影响字段(当某个字段值变化时,哪些字段需要重新计算) + */ +export function getAffectedFields( + changedFieldId: string, + fields: RequiredInput[] +): Set { + const affected = new Set(); + const graph = buildDependencyGraph(fields); + + // 找出所有依赖于 changedFieldId 的字段 + for (const [fieldId, deps] of graph.entries()) { + if (deps.has(changedFieldId)) { + affected.add(fieldId); + } + } + + return affected; +} diff --git a/packages/shared/src/preset/types.ts b/packages/shared/src/preset/types.ts index c5d4872..0f75d05 100644 --- a/packages/shared/src/preset/types.ts +++ b/packages/shared/src/preset/types.ts @@ -2,13 +2,91 @@ * 预设功能的类型定义 */ -// 敏感字段输入要求 +// 输入类型枚举 +export enum InputType { + PASSWORD = 'password', // 密码输入(隐藏) + INPUT = 'input', // 文本输入 + SELECT = 'select', // 单选 + MULTISELECT = 'multiselect', // 多选 + CONFIRM = 'confirm', // 确认框 + EDITOR = 'editor', // 多行文本编辑器 + NUMBER = 'number', // 数字输入 +} + +// 选项定义 +export interface InputOption { + label: string; // 显示文本 + value: string | number | boolean; // 实际值 + description?: string; // 选项描述 + disabled?: boolean; // 是否禁用 + icon?: string; // 图标 +} + +// 动态选项源 +export interface DynamicOptions { + type: 'static' | 'providers' | 'models' | 'custom'; + // static: 使用固定的 options 数组 + // providers: 从 Providers 配置中动态获取 + // models: 从指定 provider 的 models 中获取 + // custom: 自定义函数(暂未实现,预留) + + // 当 type 为 'static' 时使用 + options?: InputOption[]; + + // 当 type 为 'providers' 时使用 + // 自动从预设的 Providers 中提取 name 和相关配置 + + // 当 type 为 'models' 时使用 + providerField?: string; // 指向 provider 选择器的字段路径(如 "{{selectedProvider}}") + + // 当 type 为 'custom' 时使用(预留) + source?: string; // 自定义数据源 +} + +// 条件表达式 +export interface Condition { + field: string; // 依赖的字段路径 + operator?: 'eq' | 'ne' | 'in' | 'nin' | 'gt' | 'lt' | 'gte' | 'lte' | 'exists'; + value?: any; // 比较值 + // eq: 等于 + // ne: 不等于 + // in: 包含于(数组) + // nin: 不包含于(数组) + // gt: 大于 + // lt: 小于 + // gte: 大于等于 + // lte: 小于等于 + // exists: 字段存在(不检查值) +} + +// 复杂的字段输入配置 export interface RequiredInput { - field: string; // 字段路径 (如 "Providers[0].api_key") - prompt?: string; // 提示信息 - placeholder?: string; // 占位符环境变量名 - defaultValue?: string; // 默认值 - validator?: RegExp | string; // 验证规则 + id: string; // 唯一标识符(用于变量引用) + type?: InputType; // 输入类型,默认为 password + label?: string; // 显示标签 + prompt?: string; // 提示信息/描述 + placeholder?: string; // 占位符 + + // 选项配置(用于 select/multiselect) + options?: InputOption[] | DynamicOptions; + + // 条件显示 + when?: Condition | Condition[]; // 满足条件时才显示此字段(支持 AND/OR 逻辑) + + // 默认值 + defaultValue?: any; + + // 验证规则 + required?: boolean; // 是否必填,默认 true + validator?: RegExp | string | ((value: any) => boolean | string); + + // UI 配置 + min?: number; // 最小值(用于 number) + max?: number; // 最大值(用于 number) + rows?: number; // 行数(用于 editor) + + // 高级配置 + dependsOn?: string[]; // 显式声明依赖的字段(用于优化更新顺序) } // Provider 配置 @@ -50,7 +128,7 @@ export interface PresetMetadata { homepage?: string; // 主页 repository?: string; // 源码仓库 license?: string; // 许可证 - keywords?: string[]; // 关键词(原tags) + keywords?: string[]; // 关键词 ccrVersion?: string; // 兼容的 CCR 版本 source?: string; // 预设来源 URL sourceType?: 'local' | 'gist' | 'registry'; @@ -73,6 +151,25 @@ export interface PresetConfigSection { [key: string]: any; } +// 模板配置(用于根据用户输入动态生成配置) +export interface TemplateConfig { + // 使用 {{variable}} 语法的模板配置 + // 例如:{ "Providers": [{ "name": "{{providerName}}", "api_key": "{{apiKey}}" }] } + [key: string]: any; +} + +// 配置映射(将用户输入的值映射到配置的具体位置) +export interface ConfigMapping { + // 字段路径(支持数组语法,如 "Providers[0].api_key") + target: string; + + // 值来源(引用用户输入的 id,或使用固定值) + value: string | any; // 如果是 string 且以 {{ 开头,则作为变量引用 + + // 条件(可选,满足条件时才应用此映射) + when?: Condition | Condition[]; +} + // 完整的预设文件格式 export interface PresetFile { metadata?: PresetMetadata; @@ -82,12 +179,24 @@ export interface PresetFile { // 例如:{ "Providers[0].api_key": "sk-xxx", "APIKEY": "my-secret" } [fieldPath: string]: string; }; - requiredInputs?: RequiredInput[]; + + // === 动态配置系统 === + // 配置输入schema + schema?: RequiredInput[]; + + // 配置模板(使用变量替换) + template?: TemplateConfig; + + // 配置映射(将用户输入映射到配置) + configMappings?: ConfigMapping[]; } // manifest.json 格式(压缩包内的文件) export interface ManifestFile extends PresetMetadata, PresetConfigSection { - requiredInputs?: RequiredInput[]; + // === 动态配置系统 === + schema?: RequiredInput[]; + template?: TemplateConfig; + configMappings?: ConfigMapping[]; } // 在线预设索引条目 diff --git a/packages/ui/src/components/Presets.tsx b/packages/ui/src/components/Presets.tsx index 2cd9bd0..02be691 100644 --- a/packages/ui/src/components/Presets.tsx +++ b/packages/ui/src/components/Presets.tsx @@ -16,6 +16,44 @@ import { } from "@/components/ui/dialog"; import { Upload, Link, Trash2, Info, Download, CheckCircle2, AlertCircle, Loader2, ArrowLeft, Store, Search, Package } from "lucide-react"; import { Toast } from "@/components/ui/toast"; +import { DynamicConfigForm } from "./preset/DynamicConfigForm"; + +// Schema 类型 +interface InputOption { + label: string; + value: string | number | boolean; + description?: string; + disabled?: boolean; +} + +interface DynamicOptions { + type: 'static' | 'providers' | 'models' | 'custom'; + options?: InputOption[]; + providerField?: string; +} + +interface Condition { + field: string; + operator?: 'eq' | 'ne' | 'in' | 'nin' | 'gt' | 'lt' | 'gte' | 'lte' | 'exists'; + value?: any; +} + +interface RequiredInput { + id: string; + type?: 'password' | 'input' | 'select' | 'multiselect' | 'confirm' | 'editor' | 'number'; + label?: string; + prompt?: string; + placeholder?: string; + options?: InputOption[] | DynamicOptions; + when?: Condition | Condition[]; + defaultValue?: any; + required?: boolean; + validator?: RegExp | string; + min?: number; + max?: number; + rows?: number; + dependsOn?: string[]; +} interface PresetMetadata { id: string; @@ -34,9 +72,21 @@ interface PresetMetadata { installed: boolean; } +interface PresetConfigSection { + Providers?: Array<{ + name: string; + api_base_url?: string; + models?: string[]; + [key: string]: any; + }>; + [key: string]: any; +} + interface PresetDetail extends PresetMetadata { - config?: any; - requiredInputs?: Array<{ field: string; prompt?: string; placeholder?: string }>; + config?: PresetConfigSection; + schema?: RequiredInput[]; + template?: any; + configMappings?: any[]; } interface MarketPreset { @@ -145,13 +195,13 @@ export function Presets() { setSelectedPreset({ ...preset, ...detail }); setDetailDialogOpen(true); - // 初始化 secrets - if (detail.requiredInputs) { - const initialSecrets: Record = {}; - for (const input of detail.requiredInputs) { - initialSecrets[input.field] = ''; + // 初始化默认值 + if (detail.schema && detail.schema.length > 0) { + const initialValues: Record = {}; + for (const input of detail.schema) { + initialValues[input.id] = input.defaultValue ?? ''; } - setSecrets(initialSecrets); + setSecrets(initialValues); } } catch (error) { console.error('Failed to load preset details:', error); @@ -188,21 +238,27 @@ export function Presets() { }; // 应用预设(配置敏感信息) - const handleApplyPreset = async () => { + const handleApplyPreset = async (values?: Record) => { try { setIsApplying(true); + // 使用传入的values或现有的secrets + const inputValues = values || secrets; + // 验证所有必填项都已填写 - if (selectedPreset?.requiredInputs) { - for (const input of selectedPreset.requiredInputs) { - if (!secrets[input.field] || secrets[input.field].trim() === '') { - setToast({ message: t('presets.please_fill_field', { field: input.field }), type: 'warning' }); + if (selectedPreset?.schema && selectedPreset.schema.length > 0) { + // 验证在 DynamicConfigForm 中已完成 + // 这里只做简单检查 + for (const input of selectedPreset.schema) { + if (input.required !== false && !inputValues[input.id]) { + setToast({ message: t('presets.please_fill_field', { field: input.label || input.id }), type: 'warning' }); + setIsApplying(false); return; } } } - await api.applyPreset(selectedPreset!.name, secrets); + await api.applyPreset(selectedPreset!.name, inputValues); setToast({ message: t('presets.preset_applied'), type: 'success' }); setDetailDialogOpen(false); setSecrets({}); @@ -423,23 +479,18 @@ export function Presets() { )} - {selectedPreset?.requiredInputs && selectedPreset.requiredInputs.length > 0 && ( -
-

{t('presets.required_information')}

- {selectedPreset.requiredInputs.map((input) => ( -
- - setSecrets({ ...secrets, [input.field]: e.target.value })} - /> -
- ))} + {/* 配置表单 */} + {selectedPreset?.schema && selectedPreset.schema.length > 0 && ( +
+

{t('presets.required_information')}

+ handleApplyPreset(values)} + onCancel={() => setDetailDialogOpen(false)} + isSubmitting={isApplying} + initialValues={secrets} + />
)}
@@ -447,21 +498,6 @@ export function Presets() { - {selectedPreset?.requiredInputs && selectedPreset.requiredInputs.length > 0 && ( - - )} diff --git a/packages/ui/src/components/preset/DynamicConfigForm.tsx b/packages/ui/src/components/preset/DynamicConfigForm.tsx new file mode 100644 index 0000000..c34068b --- /dev/null +++ b/packages/ui/src/components/preset/DynamicConfigForm.tsx @@ -0,0 +1,453 @@ +import { useState, useEffect } from 'react'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Button } from '@/components/ui/button'; +import { Checkbox } from '@/components/ui/checkbox'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { Textarea } from '@/components/ui/textarea'; +import { CheckCircle2, Loader2 } from 'lucide-react'; + +// 类型定义 +interface InputOption { + label: string; + value: string | number | boolean; + description?: string; + disabled?: boolean; +} + +interface DynamicOptions { + type: 'static' | 'providers' | 'models' | 'custom'; + options?: InputOption[]; + providerField?: string; +} + +interface Condition { + field: string; + operator?: 'eq' | 'ne' | 'in' | 'nin' | 'gt' | 'lt' | 'gte' | 'lte' | 'exists'; + value?: any; +} + +interface RequiredInput { + id: string; + type?: 'password' | 'input' | 'select' | 'multiselect' | 'confirm' | 'editor' | 'number'; + label?: string; + prompt?: string; + placeholder?: string; + options?: InputOption[] | DynamicOptions; + when?: Condition | Condition[]; + defaultValue?: any; + required?: boolean; + validator?: RegExp | string | ((value: any) => boolean | string); + min?: number; + max?: number; + rows?: number; + dependsOn?: string[]; +} + +interface PresetConfigSection { + Providers?: Array<{ + name: string; + api_base_url?: string; + models?: string[]; + [key: string]: any; + }>; + [key: string]: any; +} + +interface DynamicConfigFormProps { + schema: RequiredInput[]; + presetConfig: PresetConfigSection; + onSubmit: (values: Record) => void; + onCancel: () => void; + isSubmitting?: boolean; + initialValues?: Record; +} + +export function DynamicConfigForm({ + schema, + presetConfig, + onSubmit, + onCancel, + isSubmitting = false, + initialValues = {}, +}: DynamicConfigFormProps) { + const [values, setValues] = useState>(initialValues); + const [errors, setErrors] = useState>({}); + const [visibleFields, setVisibleFields] = useState>(new Set()); + + // 计算可见字段 + useEffect(() => { + const updateVisibility = () => { + const visible = new Set(); + + for (const field of schema) { + if (shouldShowField(field, values)) { + visible.add(field.id); + } + } + + setVisibleFields(visible); + }; + + updateVisibility(); + }, [values, schema]); + + // 评估条件 + const evaluateCondition = (condition: Condition): boolean => { + const actualValue = values[condition.field]; + + if (condition.operator === 'exists') { + return actualValue !== undefined && actualValue !== null; + } + + if (condition.operator === 'in') { + return Array.isArray(condition.value) && condition.value.includes(actualValue); + } + + if (condition.operator === 'nin') { + return Array.isArray(condition.value) && !condition.value.includes(actualValue); + } + + switch (condition.operator) { + case 'eq': + return actualValue === condition.value; + case 'ne': + return actualValue !== condition.value; + case 'gt': + return actualValue > condition.value; + case 'lt': + return actualValue < condition.value; + case 'gte': + return actualValue >= condition.value; + case 'lte': + return actualValue <= condition.value; + default: + return actualValue === condition.value; + } + }; + + // 判断字段是否应该显示 + const shouldShowField = (field: RequiredInput): boolean => { + if (!field.when) { + return true; + } + + const conditions = Array.isArray(field.when) ? field.when : [field.when]; + return conditions.every(condition => evaluateCondition(condition)); + }; + + // 获取选项列表 + const getOptions = (field: RequiredInput): InputOption[] => { + if (!field.options) { + return []; + } + + const options = field.options as any; + + if (Array.isArray(options)) { + return options as InputOption[]; + } + + if (options.type === 'static') { + return options.options || []; + } + + if (options.type === 'providers') { + const providers = presetConfig.Providers || []; + return providers.map((p) => ({ + label: p.name || p.id || String(p), + value: p.name || p.id || String(p), + description: p.api_base_url, + })); + } + + if (options.type === 'models') { + const providerField = options.providerField; + if (!providerField) { + return []; + } + + const providerId = String(providerField).replace(/^{{(.+)}}$/, '$1'); + const selectedProvider = values[providerId]; + + if (!selectedProvider || !presetConfig.Providers) { + return []; + } + + const provider = presetConfig.Providers.find( + (p) => p.name === selectedProvider || p.id === selectedProvider + ); + + if (!provider || !provider.models) { + return []; + } + + return provider.models.map((model: string) => ({ + label: model, + value: model, + })); + } + + return []; + }; + + // 更新字段值 + const updateValue = (fieldId: string, value: any) => { + setValues((prev) => ({ + ...prev, + [fieldId]: value, + })); + // 清除该字段的错误 + setErrors((prev) => { + const newErrors = { ...prev }; + delete newErrors[fieldId]; + return newErrors; + }); + }; + + // 验证单个字段 + const validateField = (field: RequiredInput): string | null => { + const value = values[field.id]; + + // 检查必填 + if (field.required !== false && (value === undefined || value === null || value === '')) { + return `${field.label || field.id} is required`; + } + + if (!value && field.required === false) { + return null; + } + + // 类型检查 + if (field.type === 'number' && isNaN(Number(value))) { + return `${field.label || field.id} must be a number`; + } + + if (field.type === 'number') { + const numValue = Number(value); + if (field.min !== undefined && numValue < field.min) { + return `${field.label || field.id} must be at least ${field.min}`; + } + if (field.max !== undefined && numValue > field.max) { + return `${field.label || field.id} must be at most ${field.max}`; + } + } + + // 自定义验证器 + if (field.validator) { + if (field.validator instanceof RegExp) { + if (!field.validator.test(String(value))) { + return `${field.label || field.id} format is invalid`; + } + } else if (typeof field.validator === 'string') { + const regex = new RegExp(field.validator); + if (!regex.test(String(value))) { + return `${field.label || field.id} format is invalid`; + } + } + } + + return null; + }; + + // 提交表单 + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + // 验证所有可见字段 + const newErrors: Record = {}; + + for (const field of schema) { + if (!visibleFields.has(field.id)) { + continue; + } + + const error = validateField(field); + if (error) { + newErrors[field.id] = error; + } + } + + if (Object.keys(newErrors).length > 0) { + setErrors(newErrors); + return; + } + + onSubmit(values); + }; + + return ( +
+ {schema.map((field) => { + if (!visibleFields.has(field.id)) { + return null; + } + + const label = field.label || field.id; + const prompt = field.prompt; + const error = errors[field.id]; + + return ( +
+ + + {prompt && ( +

{prompt}

+ )} + + {/* Password / Input */} + {(field.type === 'password' || field.type === 'input' || !field.type) && ( + updateValue(field.id, e.target.value)} + disabled={isSubmitting} + /> + )} + + {/* Number */} + {field.type === 'number' && ( + updateValue(field.id, Number(e.target.value))} + min={field.min} + max={field.max} + disabled={isSubmitting} + /> + )} + + {/* Select */} + {field.type === 'select' && ( + + )} + + {/* Multiselect */} + {field.type === 'multiselect' && ( +
+ {getOptions(field).map((option) => ( +
+ { + const current = Array.isArray(values[field.id]) ? values[field.id] : []; + if (checked) { + updateValue(field.id, [...current, option.value]); + } else { + updateValue(field.id, current.filter((v: any) => v !== option.value)); + } + }} + disabled={isSubmitting || option.disabled} + /> + +
+ ))} +
+ )} + + {/* Confirm */} + {field.type === 'confirm' && ( +
+ updateValue(field.id, checked)} + disabled={isSubmitting} + /> + +
+ )} + + {/* Editor */} + {field.type === 'editor' && ( +