mirror of
https://github.com/musistudio/claude-code-router.git
synced 2026-01-29 22:02:05 +00:00
change doc
This commit is contained in:
@@ -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
|
||||
{
|
||||
|
||||
@@ -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/<project-id>/claude-code-router.json
|
||||
```
|
||||
|
||||
其中 `<project-id>` 是 Claude Code 项目的唯一标识符。
|
||||
Where `<project-id>` 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/<id>/claude-code-router.json`)
|
||||
3. **全局配置** (`~/.claude-code-router/config.json`)
|
||||
4. **内置路由规则**
|
||||
1. **Custom routing function** (`CUSTOM_ROUTER_PATH`)
|
||||
2. **Project-level configuration** (`~/.claude/projects/<id>/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/<project-id>/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/<project-id>/claude-code-router.json
|
||||
}
|
||||
```
|
||||
|
||||
### Web 项目配置
|
||||
### Web Project Configuration
|
||||
|
||||
```json5
|
||||
{
|
||||
@@ -196,7 +200,7 @@ rm ~/.claude/projects/<project-id>/claude-code-router.json
|
||||
}
|
||||
```
|
||||
|
||||
### AI 项目配置
|
||||
### AI Project Configuration
|
||||
|
||||
```json5
|
||||
{
|
||||
@@ -207,7 +211,7 @@ rm ~/.claude/projects/<project-id>/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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 <preset-name>
|
||||
```
|
||||
|
||||
The server will automatically restart to load the new configuration.
|
||||
|
||||
### Delete a Preset
|
||||
|
||||
```bash
|
||||
ccr preset delete <preset-name>
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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:]"'
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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. **文档化**:为自定义预设添加描述和版本信息
|
||||
|
||||
## 下一步
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,213 @@
|
||||
# 项目级配置
|
||||
|
||||
除了全局配置,`ccr` 还支持为特定项目设置不同的路由规则。
|
||||
|
||||
## 项目配置文件
|
||||
|
||||
项目配置文件位于:
|
||||
|
||||
```
|
||||
~/.claude/projects/<project-id>/claude-code-router.json
|
||||
```
|
||||
|
||||
其中 `<project-id>` 是 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/<id>/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/<project-id>/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(继承全局配置)
|
||||
83
docs/i18n/zh-CN/docusaurus-plugin-content-docs/cli/intro.md
Normal file
83
docs/i18n/zh-CN/docusaurus-plugin-content-docs/cli/intro.md
Normal file
@@ -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) - 配置文件详解
|
||||
@@ -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'"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}"
|
||||
}
|
||||
```
|
||||
@@ -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:]"'
|
||||
```
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -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/*` 端点保持向后兼容。
|
||||
@@ -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)。
|
||||
@@ -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) - 了解服务器配置选项
|
||||
@@ -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: [
|
||||
|
||||
158
examples/README.md
Normal file
158
examples/README.md
Normal file
@@ -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)
|
||||
246
examples/dynamic-preset-example.json
Normal file
246
examples/dynamic-preset-example.json
Normal file
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
268
examples/preset-manifest-example.json
Normal file
268
examples/preset-manifest-example.json
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
87
examples/simple-preset-example.json
Normal file
87
examples/simple-preset-example.json
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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<Record<string, string>> {
|
||||
const inputs: Record<string, string> = {};
|
||||
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`);
|
||||
// 2. 再应用 configMappings(如果存在)
|
||||
if (preset.configMappings && preset.configMappings.length > 0) {
|
||||
config = applyConfigMappings(preset.configMappings, values, config);
|
||||
}
|
||||
|
||||
for (const inputField of preset.requiredInputs) {
|
||||
let value: string;
|
||||
// 3. 兼容旧版:直接将 values 应用到 config
|
||||
// 检查是否有任何值没有通过 mappings 应用
|
||||
for (const [key, value] of Object.entries(values)) {
|
||||
// 如果这个值已经在 template 或 mappings 中处理过,跳过
|
||||
// 这里简化处理:直接应用所有值
|
||||
// 在实际使用中,template 和 mappings 应该覆盖所有需要设置的字段
|
||||
|
||||
// 尝试从环境变量获取
|
||||
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;
|
||||
// 尝试智能判断:如果 key 包含 '.' 或 '[',说明是路径
|
||||
if (key.includes('.') || key.includes('[')) {
|
||||
setValueByPath(config, key, value);
|
||||
}
|
||||
}
|
||||
|
||||
// 提示用户输入
|
||||
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('');
|
||||
}
|
||||
|
||||
return inputs;
|
||||
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}`);
|
||||
|
||||
230
packages/cli/src/utils/prompt/schema-input.ts
Normal file
230
packages/cli/src/utils/prompt/schema-input.ts
Normal file
@@ -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<UserInputValues> {
|
||||
// 按依赖关系排序
|
||||
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<UserInputValues> {
|
||||
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<any> {
|
||||
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<UserInputValues> {
|
||||
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;
|
||||
}
|
||||
@@ -7,3 +7,4 @@ export * from './preset/merge';
|
||||
export * from './preset/install';
|
||||
export * from './preset/export';
|
||||
export * from './preset/readPreset';
|
||||
export * from './preset/schema';
|
||||
|
||||
530
packages/shared/src/preset/schema.ts
Normal file
530
packages/shared/src/preset/schema.ts
Normal file
@@ -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<string>();
|
||||
|
||||
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<string, Set<string>> {
|
||||
const graph = new Map<string, Set<string>>();
|
||||
|
||||
for (const field of fields) {
|
||||
const deps = new Set<string>();
|
||||
|
||||
// 从 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<string> {
|
||||
const affected = new Set<string>();
|
||||
const graph = buildDependencyGraph(fields);
|
||||
|
||||
// 找出所有依赖于 changedFieldId 的字段
|
||||
for (const [fieldId, deps] of graph.entries()) {
|
||||
if (deps.has(changedFieldId)) {
|
||||
affected.add(fieldId);
|
||||
}
|
||||
}
|
||||
|
||||
return affected;
|
||||
}
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
// 在线预设索引条目
|
||||
|
||||
@@ -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<string, string> = {};
|
||||
for (const input of detail.requiredInputs) {
|
||||
initialSecrets[input.field] = '';
|
||||
// 初始化默认值
|
||||
if (detail.schema && detail.schema.length > 0) {
|
||||
const initialValues: Record<string, any> = {};
|
||||
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<string, any>) => {
|
||||
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,45 +479,25 @@ export function Presets() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedPreset?.requiredInputs && selectedPreset.requiredInputs.length > 0 && (
|
||||
<div className="mt-6 space-y-4">
|
||||
<h4 className="font-medium text-sm">{t('presets.required_information')}</h4>
|
||||
{selectedPreset.requiredInputs.map((input) => (
|
||||
<div key={input.field} className="space-y-2">
|
||||
<Label htmlFor={`secret-${input.field}`}>
|
||||
{input.prompt || input.field}
|
||||
</Label>
|
||||
<Input
|
||||
id={`secret-${input.field}`}
|
||||
type="password"
|
||||
placeholder={input.placeholder || t('presets.please_fill_field', { field: input.field })}
|
||||
value={secrets[input.field] || ''}
|
||||
onChange={(e) => setSecrets({ ...secrets, [input.field]: e.target.value })}
|
||||
{/* 配置表单 */}
|
||||
{selectedPreset?.schema && selectedPreset.schema.length > 0 && (
|
||||
<div className="mt-6">
|
||||
<h4 className="font-medium text-sm mb-4">{t('presets.required_information')}</h4>
|
||||
<DynamicConfigForm
|
||||
schema={selectedPreset.schema}
|
||||
presetConfig={selectedPreset.config || {}}
|
||||
onSubmit={(values) => handleApplyPreset(values)}
|
||||
onCancel={() => setDetailDialogOpen(false)}
|
||||
isSubmitting={isApplying}
|
||||
initialValues={secrets}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setDetailDialogOpen(false)}>
|
||||
{t('presets.close')}
|
||||
</Button>
|
||||
{selectedPreset?.requiredInputs && selectedPreset.requiredInputs.length > 0 && (
|
||||
<Button onClick={handleApplyPreset} disabled={isApplying}>
|
||||
{isApplying ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
{t('presets.applying')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CheckCircle2 className="mr-2 h-4 w-4" />
|
||||
{t('presets.apply')}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
453
packages/ui/src/components/preset/DynamicConfigForm.tsx
Normal file
453
packages/ui/src/components/preset/DynamicConfigForm.tsx
Normal file
@@ -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<string, any>) => void;
|
||||
onCancel: () => void;
|
||||
isSubmitting?: boolean;
|
||||
initialValues?: Record<string, any>;
|
||||
}
|
||||
|
||||
export function DynamicConfigForm({
|
||||
schema,
|
||||
presetConfig,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
isSubmitting = false,
|
||||
initialValues = {},
|
||||
}: DynamicConfigFormProps) {
|
||||
const [values, setValues] = useState<Record<string, any>>(initialValues);
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
const [visibleFields, setVisibleFields] = useState<Set<string>>(new Set());
|
||||
|
||||
// 计算可见字段
|
||||
useEffect(() => {
|
||||
const updateVisibility = () => {
|
||||
const visible = new Set<string>();
|
||||
|
||||
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<string, string> = {};
|
||||
|
||||
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 (
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{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 (
|
||||
<div key={field.id} className="space-y-2">
|
||||
<Label htmlFor={`field-${field.id}`}>
|
||||
{label}
|
||||
{field.required !== false && <span className="text-red-500 ml-1">*</span>}
|
||||
</Label>
|
||||
|
||||
{prompt && (
|
||||
<p className="text-sm text-gray-600">{prompt}</p>
|
||||
)}
|
||||
|
||||
{/* Password / Input */}
|
||||
{(field.type === 'password' || field.type === 'input' || !field.type) && (
|
||||
<Input
|
||||
id={`field-${field.id}`}
|
||||
type={field.type === 'password' ? 'password' : 'text'}
|
||||
placeholder={field.placeholder}
|
||||
value={values[field.id] || ''}
|
||||
onChange={(e) => updateValue(field.id, e.target.value)}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Number */}
|
||||
{field.type === 'number' && (
|
||||
<Input
|
||||
id={`field-${field.id}`}
|
||||
type="number"
|
||||
placeholder={field.placeholder}
|
||||
value={values[field.id] || ''}
|
||||
onChange={(e) => updateValue(field.id, Number(e.target.value))}
|
||||
min={field.min}
|
||||
max={field.max}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Select */}
|
||||
{field.type === 'select' && (
|
||||
<Select
|
||||
value={values[field.id] || ''}
|
||||
onValueChange={(value) => updateValue(field.id, value)}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<SelectTrigger id={`field-${field.id}`}>
|
||||
<SelectValue placeholder={field.placeholder || `Select ${label}`} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{getOptions(field).map((option) => (
|
||||
<SelectItem
|
||||
key={String(option.value)}
|
||||
value={String(option.value)}
|
||||
disabled={option.disabled}
|
||||
>
|
||||
<div>
|
||||
<div>{option.label}</div>
|
||||
{option.description && (
|
||||
<div className="text-xs text-gray-500">{option.description}</div>
|
||||
)}
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
|
||||
{/* Multiselect */}
|
||||
{field.type === 'multiselect' && (
|
||||
<div className="space-y-2">
|
||||
{getOptions(field).map((option) => (
|
||||
<div key={String(option.value)} className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id={`field-${field.id}-${option.value}`}
|
||||
checked={Array.isArray(values[field.id]) && values[field.id].includes(option.value)}
|
||||
onCheckedChange={(checked) => {
|
||||
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}
|
||||
/>
|
||||
<Label
|
||||
htmlFor={`field-${field.id}-${option.value}`}
|
||||
className="text-sm font-normal cursor-pointer"
|
||||
>
|
||||
{option.label}
|
||||
{option.description && (
|
||||
<span className="text-gray-500 ml-2">{option.description}</span>
|
||||
)}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Confirm */}
|
||||
{field.type === 'confirm' && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id={`field-${field.id}`}
|
||||
checked={values[field.id] || false}
|
||||
onCheckedChange={(checked) => updateValue(field.id, checked)}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
<Label htmlFor={`field-${field.id}`} className="text-sm font-normal cursor-pointer">
|
||||
{field.prompt || label}
|
||||
</Label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Editor */}
|
||||
{field.type === 'editor' && (
|
||||
<Textarea
|
||||
id={`field-${field.id}`}
|
||||
placeholder={field.placeholder}
|
||||
value={values[field.id] || ''}
|
||||
onChange={(e) => updateValue(field.id, e.target.value)}
|
||||
rows={field.rows || 5}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<p className="text-sm text-red-500">{error}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<div className="flex justify-end gap-2 pt-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={onCancel}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Applying...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CheckCircle2 className="mr-2 h-4 w-4" />
|
||||
Apply
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user