feat: 添加 CCR (Claude Code Router) 账户类型支持

实现通过供应商前缀语法进行 CCR 后端路由的完整支持。
用户现在可以在 Claude Code 中使用 `/model ccr,model_name` 将请求路由到 CCR 后端。
暂时没有实现`/v1/messages/count_tokens`,因为这需要在CCR后端支持。
CCR类型的账户也暂时没有考虑模型的支持情况

## 核心实现

### 供应商前缀路由

- 添加 modelHelper 工具用于解析模型名称中的 `ccr,` 供应商前缀
- 检测到前缀时自动路由到 CCR 账户池
- 转发到 CCR 后端前移除供应商前缀

### 账户管理

- 创建 ccrAccountService 实现 CCR 账户的完整 CRUD 操作
- 支持账户属性:名称、API URL、API Key、代理、优先级、配额
- 实现账户状态:active、rate_limited、unauthorized、overloaded
- 支持模型映射和支持模型配置

### 请求转发

- 实现 ccrRelayService 处理 CCR 后端通信
- 支持流式和非流式请求
- 从 SSE 流中解析和捕获使用数据
- 支持 Bearer 和 x-api-key 两种认证格式

### 统一调度

- 将 CCR 账户集成到 unifiedClaudeScheduler
- 添加 \_selectCcrAccount 方法用于 CCR 特定账户选择
- 支持 CCR 账户的会话粘性
- 防止跨类型会话映射(CCR 会话仅用于 CCR 请求)

### 错误处理

- 实现全面的错误状态管理
- 处理 401(未授权)、429(速率限制)、529(过载)错误
- 成功请求后自动从错误状态恢复
- 支持可配置的速率限制持续时间

### Web 管理界面

- 添加 CcrAccountForm 组件用于创建/编辑 CCR 账户
- 将 CCR 账户集成到 AccountsView 中,提供完整管理功能
- 支持账户切换、重置和使用统计
- 在界面中显示账户状态和错误信息

### API 端点

- POST /admin/ccr-accounts - 创建 CCR 账户
- GET /admin/ccr-accounts - 列出所有 CCR 账户
- PUT /admin/ccr-accounts/:id - 更新 CCR 账户
- DELETE /admin/ccr-accounts/:id - 删除 CCR 账户
- PUT /admin/ccr-accounts/:id/toggle - 切换账户启用状态
- PUT /admin/ccr-accounts/:id/toggle-schedulable - 切换可调度状态
- POST /admin/ccr-accounts/:id/reset-usage - 重置每日使用量
- POST /admin/ccr-accounts/:id/reset-status - 重置错误状态

## 技术细节

- CCR 账户使用 'ccr' 作为 accountType 标识符
- 带有 `ccr,` 前缀的请求绕过普通账户池
- 转发到 CCR 后端前清理模型名称内的`ccr,`
- 从流式和非流式响应中捕获使用数据
- 支持缓存令牌跟踪(创建和读取)
This commit is contained in:
sususu98
2025-09-10 14:21:15 +08:00
parent 1c3b74f45b
commit 7f9869ae20
11 changed files with 3117 additions and 52 deletions

78
src/utils/modelHelper.js Normal file
View File

@@ -0,0 +1,78 @@
/**
* Model Helper Utility
*
* Provides utilities for parsing vendor-prefixed model names.
* Supports parsing model strings like "ccr,model_name" to extract vendor type and base model.
*/
/**
* Parse vendor-prefixed model string
* @param {string} modelStr - Model string, potentially with vendor prefix (e.g., "ccr,gemini-2.5-pro")
* @returns {{vendor: string|null, baseModel: string}} - Parsed vendor and base model
*/
function parseVendorPrefixedModel(modelStr) {
if (!modelStr || typeof modelStr !== 'string') {
return { vendor: null, baseModel: modelStr || '' }
}
// Trim whitespace and convert to lowercase for comparison
const trimmed = modelStr.trim()
const lowerTrimmed = trimmed.toLowerCase()
// Check for ccr prefix (case insensitive)
if (lowerTrimmed.startsWith('ccr,')) {
const parts = trimmed.split(',')
if (parts.length >= 2) {
// Extract base model (everything after the first comma, rejoined in case model name contains commas)
const baseModel = parts.slice(1).join(',').trim()
return {
vendor: 'ccr',
baseModel
}
}
}
// No recognized vendor prefix found
return {
vendor: null,
baseModel: trimmed
}
}
/**
* Check if a model string has a vendor prefix
* @param {string} modelStr - Model string to check
* @returns {boolean} - True if the model has a vendor prefix
*/
function hasVendorPrefix(modelStr) {
const { vendor } = parseVendorPrefixedModel(modelStr)
return vendor !== null
}
/**
* Get the effective model name for scheduling and processing
* This removes vendor prefixes to get the actual model name used for API calls
* @param {string} modelStr - Original model string
* @returns {string} - Effective model name without vendor prefix
*/
function getEffectiveModel(modelStr) {
const { baseModel } = parseVendorPrefixedModel(modelStr)
return baseModel
}
/**
* Get the vendor type from a model string
* @param {string} modelStr - Model string to parse
* @returns {string|null} - Vendor type ('ccr') or null if no prefix
*/
function getVendorType(modelStr) {
const { vendor } = parseVendorPrefixedModel(modelStr)
return vendor
}
module.exports = {
parseVendorPrefixedModel,
hasVendorPrefix,
getEffectiveModel,
getVendorType
}