From 71adf8416ade8b786fae0b1aa12f45da67e1f758 Mon Sep 17 00:00:00 2001
From: YoVinchen
Date: Fri, 5 Sep 2025 22:16:06 +0800
Subject: [PATCH] CCR
---
README.md | 2 +-
src-tauri/src/claude_config.rs | 51 ++-
src-tauri/src/commands/ccr.rs | 337 ++++++++++++++++++++
src-tauri/src/commands/mod.rs | 1 +
src-tauri/src/main.rs | 14 +
src/App.tsx | 7 +
src/components/CcrRouterManager.tsx | 463 ++++++++++++++++++++++++++++
src/components/MCPServerList.tsx | 50 +--
src/components/WelcomePage.tsx | 13 +-
src/lib/api.ts | 206 ++++++++++---
src/locales/en/common.json | 4 +-
src/locales/zh/common.json | 4 +-
12 files changed, 1068 insertions(+), 84 deletions(-)
create mode 100644 src-tauri/src/commands/ccr.rs
create mode 100644 src/components/CcrRouterManager.tsx
diff --git a/README.md b/README.md
index d094381..e038c4b 100644
--- a/README.md
+++ b/README.md
@@ -118,7 +118,7 @@ bun run tauri build # 生产构建
```bash
bun run tauri dev # 启动开发服务器
bunx tsc --noEmit # 类型检查
-cd src-tauri && cargo test # Rust 测试
+cd src-tauri && cargo test.md # Rust 测试
bun run check # 完整检查
```
diff --git a/src-tauri/src/claude_config.rs b/src-tauri/src/claude_config.rs
index 24e4202..7cd47fe 100644
--- a/src-tauri/src/claude_config.rs
+++ b/src-tauri/src/claude_config.rs
@@ -1,5 +1,6 @@
use std::fs;
use std::path::PathBuf;
+use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use dirs::home_dir;
@@ -16,9 +17,27 @@ pub struct ClaudeConfig {
pub model: Option,
#[serde(rename = "apiKeyHelper", skip_serializing_if = "Option::is_none")]
pub api_key_helper: Option,
+ #[serde(rename = "statusLine", skip_serializing_if = "Option::is_none")]
+ pub status_line: Option,
+ // 使用 flatten 来支持任何其他未知字段
+ #[serde(flatten)]
+ pub extra_fields: std::collections::HashMap,
}
-#[derive(Debug, Clone, Serialize, Deserialize, Default)]
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct StatusLineConfig {
+ #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
+ pub config_type: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub command: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub padding: Option,
+ // 支持其他可能的 statusLine 字段
+ #[serde(flatten)]
+ pub extra_fields: std::collections::HashMap,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClaudeEnv {
#[serde(rename = "ANTHROPIC_AUTH_TOKEN", skip_serializing_if = "Option::is_none")]
pub anthropic_auth_token: Option,
@@ -26,6 +45,20 @@ pub struct ClaudeEnv {
pub anthropic_base_url: Option,
#[serde(rename = "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC", skip_serializing_if = "Option::is_none")]
pub disable_nonessential_traffic: Option,
+ // 使用 flatten 来支持任何其他环境变量
+ #[serde(flatten)]
+ pub extra_fields: std::collections::HashMap,
+}
+
+impl Default for ClaudeEnv {
+ fn default() -> Self {
+ Self {
+ anthropic_auth_token: None,
+ anthropic_base_url: None,
+ disable_nonessential_traffic: None,
+ extra_fields: HashMap::new(),
+ }
+ }
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
@@ -59,6 +92,8 @@ pub fn read_claude_config() -> Result {
permissions: Some(ClaudePermissions::default()),
model: None,
api_key_helper: None,
+ status_line: None,
+ extra_fields: HashMap::new(),
});
}
@@ -146,7 +181,7 @@ pub fn restore_claude_config() -> Result<(), String> {
Ok(())
}
-/// 根据中转站配置更新 Claude 配置
+/// 根据中转站配置更新 Claude 配置(仅更新 API 相关字段)
pub fn apply_relay_station_to_config(station: &RelayStation) -> Result<(), String> {
// 先备份当前配置
backup_claude_config()?;
@@ -154,17 +189,17 @@ pub fn apply_relay_station_to_config(station: &RelayStation) -> Result<(), Strin
// 读取当前配置
let mut config = read_claude_config()?;
- // 更新 API URL
+ // 仅更新这三个关键字段,保留其他所有配置不变:
+ // 1. ANTHROPIC_BASE_URL
config.env.anthropic_base_url = Some(station.api_url.clone());
- // 更新 API Token
+ // 2. ANTHROPIC_AUTH_TOKEN
config.env.anthropic_auth_token = Some(station.system_token.clone());
- // 将中转站的 token 也设置到 apiKeyHelper
- // 格式:echo 'token'
+ // 3. apiKeyHelper - 设置为 echo 格式
config.api_key_helper = Some(format!("echo '{}'", station.system_token));
- // 如果是特定适配器,可能需要特殊处理
+ // 如果是特定适配器,可能需要特殊处理 URL 格式
match station.adapter.as_str() {
"packycode" => {
// PackyCode 使用原始配置,不做特殊处理
@@ -187,7 +222,7 @@ pub fn apply_relay_station_to_config(station: &RelayStation) -> Result<(), Strin
// 写入更新后的配置
write_claude_config(&config)?;
- log::info!("已将中转站 {} 的配置应用到 Claude 配置文件", station.name);
+ log::info!("已将中转站 {} 的 API 配置(apiKeyHelper, ANTHROPIC_BASE_URL, ANTHROPIC_AUTH_TOKEN)应用到 Claude 配置文件", station.name);
Ok(())
}
diff --git a/src-tauri/src/commands/ccr.rs b/src-tauri/src/commands/ccr.rs
new file mode 100644
index 0000000..ef0bbdf
--- /dev/null
+++ b/src-tauri/src/commands/ccr.rs
@@ -0,0 +1,337 @@
+use serde::{Deserialize, Serialize};
+use std::process::{Command, Stdio};
+use log::{debug, error, info};
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub struct CcrServiceStatus {
+ pub is_running: bool,
+ pub port: Option,
+ pub endpoint: Option,
+ pub has_ccr_binary: bool,
+ pub ccr_version: Option,
+ pub process_id: Option,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct CcrServiceInfo {
+ pub status: CcrServiceStatus,
+ pub message: String,
+}
+
+/// 检查 CCR 是否已安装
+#[tauri::command]
+pub async fn check_ccr_installation() -> Result {
+ // 直接尝试执行 ccr --version 命令来检测是否安装
+ // 这比使用 which 命令更可靠,特别是在打包后的应用中
+ let output = Command::new("ccr")
+ .arg("--version")
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped())
+ .output();
+
+ match output {
+ Ok(result) => Ok(result.status.success()),
+ Err(e) => {
+ // 如果命令执行失败,可能是因为 ccr 未安装或不在 PATH 中
+ debug!("CCR installation check failed: {}", e);
+ Ok(false)
+ }
+ }
+}
+
+/// 获取 CCR 版本信息
+#[tauri::command]
+pub async fn get_ccr_version() -> Result {
+ // 尝试多个版本命令参数
+ let version_args = vec!["--version", "-v", "version"];
+
+ for arg in version_args {
+ let output = Command::new("ccr")
+ .arg(arg)
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped())
+ .output();
+
+ if let Ok(result) = output {
+ if result.status.success() {
+ let version = String::from_utf8_lossy(&result.stdout);
+ let trimmed = version.trim().to_string();
+ if !trimmed.is_empty() {
+ return Ok(trimmed);
+ }
+ }
+ }
+ }
+
+ Err("Unable to get CCR version".to_string())
+}
+
+/// 检查 CCR 服务状态
+#[tauri::command]
+pub async fn get_ccr_service_status() -> Result {
+ // 首先检查 ccr 二进制是否存在
+ let has_ccr_binary = check_ccr_installation().await.unwrap_or(false);
+
+ if !has_ccr_binary {
+ info!("CCR binary not found in PATH");
+ return Ok(CcrServiceStatus {
+ is_running: false,
+ port: None,
+ endpoint: None,
+ has_ccr_binary: false,
+ ccr_version: None,
+ process_id: None,
+ });
+ }
+
+ // 获取版本信息
+ let ccr_version = get_ccr_version().await.ok();
+ debug!("CCR version: {:?}", ccr_version);
+
+ // 检查服务状态
+ let output = Command::new("ccr")
+ .arg("status")
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped())
+ .output();
+
+ let output = match output {
+ Ok(o) => o,
+ Err(e) => {
+ error!("Failed to execute ccr status: {}", e);
+ return Ok(CcrServiceStatus {
+ is_running: false,
+ port: None,
+ endpoint: None,
+ has_ccr_binary: true,
+ ccr_version,
+ process_id: None,
+ });
+ }
+ };
+
+ let status_output = String::from_utf8_lossy(&output.stdout);
+ let stderr_output = String::from_utf8_lossy(&output.stderr);
+
+ debug!("CCR status stdout: {}", status_output);
+ debug!("CCR status stderr: {}", stderr_output);
+
+ // 更宽松的运行状态检测
+ let is_running = output.status.success() &&
+ (status_output.contains("Running") ||
+ status_output.contains("running") ||
+ status_output.contains("✅") ||
+ status_output.contains("Port:"));
+
+ // 尝试从输出中提取端口、端点和进程ID信息
+ let mut port = None;
+ let mut endpoint = None;
+ let mut process_id = None;
+
+ if is_running {
+ // 提取端口信息 - 支持多种格式
+ for line in status_output.lines() {
+ if line.contains("Port:") || line.contains("port:") {
+ // 尝试提取端口号
+ if let Some(port_str) = line.split(':').last() {
+ // 清理字符串,只保留数字
+ let cleaned: String = port_str.chars()
+ .filter(|c| c.is_numeric())
+ .collect();
+ if let Ok(port_num) = cleaned.parse::() {
+ port = Some(port_num);
+ break;
+ }
+ }
+ }
+ }
+
+ // 提取API端点信息 - 支持多种格式
+ for line in status_output.lines() {
+ if line.contains("API Endpoint:") || line.contains("Endpoint:") || line.contains("http://") || line.contains("https://") {
+ // 尝试提取URL
+ if let Some(start) = line.find("http") {
+ let url_part = &line[start..];
+ // 找到URL的结束位置(空格或行尾)
+ let end = url_part.find(char::is_whitespace).unwrap_or(url_part.len());
+ let url = &url_part[..end];
+ if url.contains(":") && (url.contains("localhost") || url.contains("127.0.0.1")) {
+ endpoint = Some(url.to_string());
+ break;
+ }
+ }
+ }
+ }
+
+ // 提取进程ID信息 - 支持多种格式
+ for line in status_output.lines() {
+ if line.contains("Process ID:") || line.contains("PID:") || line.contains("pid:") {
+ // 尝试提取PID
+ if let Some(pid_str) = line.split(':').last() {
+ // 清理字符串,只保留数字
+ let cleaned: String = pid_str.chars()
+ .filter(|c| c.is_numeric())
+ .collect();
+ if let Ok(pid_num) = cleaned.parse::() {
+ process_id = Some(pid_num);
+ break;
+ }
+ }
+ }
+ }
+
+ // 如果没有找到具体信息,使用默认值
+ if port.is_none() {
+ port = Some(3456);
+ debug!("Using default port: 3456");
+ }
+ if endpoint.is_none() {
+ let port_num = port.unwrap_or(3456);
+ endpoint = Some(format!("http://localhost:{}", port_num));
+ debug!("Using default endpoint: {:?}", endpoint);
+ }
+ }
+
+ Ok(CcrServiceStatus {
+ is_running,
+ port,
+ endpoint,
+ has_ccr_binary,
+ ccr_version,
+ process_id,
+ })
+}
+
+/// 启动 CCR 服务
+#[tauri::command]
+pub async fn start_ccr_service() -> Result {
+ // 先检查是否已安装
+ if !check_ccr_installation().await.unwrap_or(false) {
+ return Err("CCR is not installed. Please install claude-code-router first.".to_string());
+ }
+
+ // 检查当前状态
+ let current_status = get_ccr_service_status().await?;
+ if current_status.is_running {
+ return Ok(CcrServiceInfo {
+ status: current_status,
+ message: "CCR service is already running".to_string(),
+ });
+ }
+
+ // 启动服务
+ let _output = Command::new("ccr")
+ .arg("start")
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped())
+ .spawn()
+ .map_err(|e| format!("Failed to start ccr service: {}", e))?;
+
+ // 等待一下让服务启动
+ tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
+
+ // 再次检查状态
+ let new_status = get_ccr_service_status().await?;
+
+ if new_status.is_running {
+ Ok(CcrServiceInfo {
+ status: new_status,
+ message: "CCR service started successfully".to_string(),
+ })
+ } else {
+ Err("Failed to start CCR service".to_string())
+ }
+}
+
+/// 停止 CCR 服务
+#[tauri::command]
+pub async fn stop_ccr_service() -> Result {
+ if !check_ccr_installation().await.unwrap_or(false) {
+ return Err("CCR is not installed".to_string());
+ }
+
+ let output = Command::new("ccr")
+ .arg("stop")
+ .output()
+ .map_err(|e| format!("Failed to stop ccr service: {}", e))?;
+
+ if !output.status.success() {
+ let error = String::from_utf8_lossy(&output.stderr);
+ return Err(format!("Failed to stop CCR service: {}", error));
+ }
+
+ // 检查新状态
+ let new_status = get_ccr_service_status().await?;
+
+ Ok(CcrServiceInfo {
+ status: new_status,
+ message: "CCR service stopped successfully".to_string(),
+ })
+}
+
+/// 重启 CCR 服务
+#[tauri::command]
+pub async fn restart_ccr_service() -> Result {
+ if !check_ccr_installation().await.unwrap_or(false) {
+ return Err("CCR is not installed".to_string());
+ }
+
+ let output = Command::new("ccr")
+ .arg("restart")
+ .output()
+ .map_err(|e| format!("Failed to restart ccr service: {}", e))?;
+
+ if !output.status.success() {
+ let error = String::from_utf8_lossy(&output.stderr);
+ return Err(format!("Failed to restart CCR service: {}", error));
+ }
+
+ // 等待服务重启
+ tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;
+
+ // 检查新状态
+ let new_status = get_ccr_service_status().await?;
+
+ Ok(CcrServiceInfo {
+ status: new_status,
+ message: "CCR service restarted successfully".to_string(),
+ })
+}
+
+/// 打开 CCR UI
+#[tauri::command]
+pub async fn open_ccr_ui() -> Result {
+ if !check_ccr_installation().await.unwrap_or(false) {
+ return Err("CCR is not installed".to_string());
+ }
+
+ // 检查服务状态
+ let status = get_ccr_service_status().await?;
+ if !status.is_running {
+ // 如果服务未运行,尝试启动
+ let _start_result = start_ccr_service().await?;
+ // 再等待一下
+ tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
+ }
+
+ // 执行 ccr ui 命令
+ let _output = Command::new("ccr")
+ .arg("ui")
+ .spawn()
+ .map_err(|e| format!("Failed to open ccr ui: {}", e))?;
+
+ Ok("CCR UI opening...".to_string())
+}
+
+/// 获取 CCR 配置路径
+#[tauri::command]
+pub async fn get_ccr_config_path() -> Result {
+ let home_dir = dirs::home_dir()
+ .ok_or("Could not find home directory")?;
+
+ let config_path = home_dir
+ .join(".claude-code-router")
+ .join("config.json");
+
+ Ok(config_path.to_string_lossy().to_string())
+}
\ No newline at end of file
diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs
index 03cc1fb..db0f061 100644
--- a/src-tauri/src/commands/mod.rs
+++ b/src-tauri/src/commands/mod.rs
@@ -14,3 +14,4 @@ pub mod packycode_nodes;
pub mod filesystem;
pub mod git;
pub mod terminal;
+pub mod ccr;
diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs
index aebca31..a0d24e6 100644
--- a/src-tauri/src/main.rs
+++ b/src-tauri/src/main.rs
@@ -79,6 +79,10 @@ use commands::terminal::{
create_terminal_session, send_terminal_input, close_terminal_session,
list_terminal_sessions, resize_terminal, cleanup_terminal_sessions, TerminalState,
};
+use commands::ccr::{
+ check_ccr_installation, get_ccr_version, get_ccr_service_status, start_ccr_service,
+ stop_ccr_service, restart_ccr_service, open_ccr_ui, get_ccr_config_path,
+};
use process::ProcessRegistryState;
use file_watcher::FileWatcherState;
use std::sync::Mutex;
@@ -417,6 +421,16 @@ fn main() {
list_terminal_sessions,
resize_terminal,
cleanup_terminal_sessions,
+
+ // CCR (Claude Code Router)
+ check_ccr_installation,
+ get_ccr_version,
+ get_ccr_service_status,
+ start_ccr_service,
+ stop_ccr_service,
+ restart_ccr_service,
+ open_ccr_ui,
+ get_ccr_config_path,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
diff --git a/src/App.tsx b/src/App.tsx
index c9a13fd..0796a15 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -29,6 +29,7 @@ import { useAppLifecycle, useTrackEvent } from "@/hooks";
import { useTranslation } from "@/hooks/useTranslation";
import { WelcomePage } from "@/components/WelcomePage";
import RelayStationManager from "@/components/RelayStationManager";
+import { CcrRouterManager } from "@/components/CcrRouterManager";
import i18n from "@/lib/i18n";
type View =
@@ -44,6 +45,7 @@ type View =
| "agent-run-view"
| "mcp"
| "relay-stations"
+ | "ccr-router"
| "usage-dashboard"
| "project-settings"
| "tabs"; // New view for tab-based interface
@@ -282,6 +284,11 @@ function AppContent() {
handleViewChange("welcome")} />
);
+ case "ccr-router":
+ return (
+ handleViewChange("welcome")} />
+ );
+
case "cc-agents":
return (
void;
+}
+
+export function CcrRouterManager({ onBack }: CcrRouterManagerProps) {
+ const [serviceStatus, setServiceStatus] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [actionLoading, setActionLoading] = useState(false);
+ const [toast, setToast] = useState<{ message: string; type: "success" | "error" | "info" } | null>(null);
+ const [configPath, setConfigPath] = useState("");
+
+ useEffect(() => {
+ loadServiceStatus();
+ loadConfigPath();
+ }, []);
+
+ const loadServiceStatus = async () => {
+ try {
+ setLoading(true);
+ const status = await ccrApi.getServiceStatus();
+ setServiceStatus(status);
+ } catch (error) {
+ console.error("Failed to load CCR service status:", error);
+ setToast({
+ message: `加载CCR服务状态失败: ${error}`,
+ type: "error"
+ });
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const loadConfigPath = async () => {
+ try {
+ const path = await ccrApi.getConfigPath();
+ setConfigPath(path);
+ } catch (error) {
+ console.error("Failed to get config path:", error);
+ }
+ };
+
+ const handleStartService = async () => {
+ try {
+ setActionLoading(true);
+ const result = await ccrApi.startService();
+ setServiceStatus(result.status);
+ setToast({
+ message: result.message,
+ type: "success"
+ });
+ } catch (error) {
+ console.error("Failed to start CCR service:", error);
+ setToast({
+ message: `启动CCR服务失败: ${error}`,
+ type: "error"
+ });
+ } finally {
+ setActionLoading(false);
+ }
+ };
+
+ const handleStopService = async () => {
+ try {
+ setActionLoading(true);
+ const result = await ccrApi.stopService();
+ setServiceStatus(result.status);
+ setToast({
+ message: result.message,
+ type: "success"
+ });
+ } catch (error) {
+ console.error("Failed to stop CCR service:", error);
+ setToast({
+ message: `停止CCR服务失败: ${error}`,
+ type: "error"
+ });
+ } finally {
+ setActionLoading(false);
+ }
+ };
+
+ const handleRestartService = async () => {
+ try {
+ setActionLoading(true);
+ const result = await ccrApi.restartService();
+ setServiceStatus(result.status);
+ setToast({
+ message: result.message,
+ type: "success"
+ });
+ } catch (error) {
+ console.error("Failed to restart CCR service:", error);
+ setToast({
+ message: `重启CCR服务失败: ${error}`,
+ type: "error"
+ });
+ } finally {
+ setActionLoading(false);
+ }
+ };
+
+ const handleOpenUI = async () => {
+ try {
+ setActionLoading(true);
+
+ // 如果服务未运行,先尝试启动
+ if (!serviceStatus?.is_running) {
+ setToast({
+ message: "检测到服务未运行,正在启动...",
+ type: "info"
+ });
+ const startResult = await ccrApi.startService();
+ setServiceStatus(startResult.status);
+
+ if (!startResult.status.is_running) {
+ throw new Error("服务启动失败");
+ }
+
+ // 等待服务完全启动
+ await new Promise(resolve => setTimeout(resolve, 3000));
+ }
+
+ await ccrApi.openUI();
+ setToast({
+ message: "正在打开CCR UI...",
+ type: "info"
+ });
+
+ // 刷新服务状态
+ setTimeout(() => {
+ loadServiceStatus();
+ }, 2000);
+ } catch (error) {
+ console.error("Failed to open CCR UI:", error);
+ setToast({
+ message: `打开CCR UI失败: ${error}`,
+ type: "error"
+ });
+ } finally {
+ setActionLoading(false);
+ }
+ };
+
+ const handleOpenInBrowser = async () => {
+ try {
+ // 如果服务未运行,先尝试启动
+ if (!serviceStatus?.is_running) {
+ setActionLoading(true);
+ setToast({
+ message: "检测到服务未运行,正在启动...",
+ type: "info"
+ });
+
+ const startResult = await ccrApi.startService();
+ setServiceStatus(startResult.status);
+
+ if (!startResult.status.is_running) {
+ throw new Error("服务启动失败");
+ }
+
+ // 等待服务完全启动
+ await new Promise(resolve => setTimeout(resolve, 2000));
+ setActionLoading(false);
+ }
+
+ if (serviceStatus?.endpoint) {
+ open(`${serviceStatus.endpoint}/ui/`);
+ setToast({
+ message: "正在打开CCR管理界面...",
+ type: "info"
+ });
+ }
+ } catch (error) {
+ console.error("Failed to open CCR UI in browser:", error);
+ setToast({
+ message: `打开管理界面失败: ${error}`,
+ type: "error"
+ });
+ setActionLoading(false);
+ }
+ };
+
+ const renderServiceStatus = () => {
+ if (!serviceStatus) return null;
+
+ const statusColor = serviceStatus.is_running ? "bg-green-500" : "bg-red-500";
+ const statusText = serviceStatus.is_running ? "运行中" : "已停止";
+
+ return (
+
+
+
{statusText}
+ {serviceStatus.is_running && serviceStatus.port && (
+
端口 {serviceStatus.port}
+ )}
+
+ );
+ };
+
+ const renderInstallationStatus = () => {
+ if (!serviceStatus) return null;
+
+ return (
+
+ {serviceStatus.has_ccr_binary ? (
+ <>
+
+
已安装
+ {serviceStatus.ccr_version && (
+
{serviceStatus.ccr_version}
+ )}
+ >
+ ) : (
+ <>
+
+
未安装
+ >
+ )}
+
+ );
+ };
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+ {/* Header */}
+
+
+
+
+
CCR 路由管理
+
+ 管理 Claude Code Router 服务和配置
+
+
+
+
+
+ {/* Service Status Card */}
+
+
+
+
+ 服务状态
+
+
+
+ CCR 路由服务当前状态和控制选项
+
+
+
+
+ 安装状态:
+ {renderInstallationStatus()}
+
+
+
+ 服务状态:
+ {renderServiceStatus()}
+
+
+ {serviceStatus?.endpoint && (
+
+ 服务地址:
+
+
+ )}
+
+ {serviceStatus?.process_id && (
+
+ 进程 ID:
+ {serviceStatus.process_id}
+
+ )}
+
+ {configPath && (
+
+ 配置文件:
+
+ {configPath}
+
+
+ )}
+
+
+
+
+ {/* Control Panel */}
+
+
+
+ 服务控制
+
+ 启动、停止或重启 CCR 路由服务
+
+
+
+ {serviceStatus?.has_ccr_binary ? (
+
+ {!serviceStatus.is_running ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+ ) : (
+
+
+
CCR 未安装
+
+ 需要先安装 Claude Code Router 才能使用此功能
+
+
+
+ )}
+
+
+
+
+ {/* Information Card */}
+
+
+
+ 关于 CCR 路由
+
+
+
+ Claude Code Router (CCR) 是一个强大的路由工具,允许您将 Claude Code 请求转发到不同的 LLM 提供商。
+
+
+ - 支持多个 LLM 提供商(OpenRouter、DeepSeek、Gemini 等)
+ - 智能路由规则,根据令牌数量和请求类型自动选择
+ - Web UI 管理界面,方便配置和监控
+ - 无需 Anthropic 账户即可使用 Claude Code
+
+
+
+
+
+
+ {/* Toast Container */}
+
+ {toast && (
+ setToast(null)}
+ />
+ )}
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/MCPServerList.tsx b/src/components/MCPServerList.tsx
index 57eac5a..fa53393 100644
--- a/src/components/MCPServerList.tsx
+++ b/src/components/MCPServerList.tsx
@@ -1,11 +1,11 @@
import React, { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
-import {
- Network,
- Globe,
- Terminal,
- Trash2,
- Play,
+import {
+ Network,
+ Globe,
+ Terminal,
+ Trash2,
+ Play,
CheckCircle,
Loader2,
RefreshCw,
@@ -57,7 +57,7 @@ export const MCPServerList: React.FC = ({
const [expandedServers, setExpandedServers] = useState>(new Set());
const [copiedServer, setCopiedServer] = useState(null);
const [connectedServers] = useState([]);
-
+
// Analytics tracking
const trackEvent = useTrackEvent();
@@ -103,18 +103,18 @@ export const MCPServerList: React.FC = ({
const handleRemoveServer = async (name: string) => {
try {
setRemovingServer(name);
-
+
// Check if server was connected
const wasConnected = connectedServers.includes(name);
-
+
await api.mcpRemove(name);
-
+
// Track server removal
trackEvent.mcpServerRemoved({
server_name: name,
was_connected: wasConnected
});
-
+
onServerRemoved(name);
} catch (error) {
console.error("Failed to remove server:", error);
@@ -131,15 +131,15 @@ export const MCPServerList: React.FC = ({
setTestingServer(name);
const result = await api.mcpTestConnection(name);
const server = servers.find(s => s.name === name);
-
+
// Track connection result - result is a string message
trackEvent.mcpServerConnected(name, true, server?.transport || 'unknown');
-
+
// TODO: Show result in a toast or modal
console.log("Test result:", result);
} catch (error) {
- console.error("Failed to test connection:", error);
-
+ console.error("Failed to test.md connection:", error);
+
trackEvent.mcpConnectionError({
server_name: name,
error_type: 'test_failed',
@@ -202,7 +202,7 @@ export const MCPServerList: React.FC = ({
const renderServerItem = (server: MCPServer) => {
const isExpanded = expandedServers.has(server.name);
const isCopied = copiedServer === server.name;
-
+
return (
= ({
)}
-
+
{server.command && !isExpanded && (
@@ -243,7 +243,7 @@ export const MCPServerList: React.FC = ({
)}
-
+
{server.transport === "sse" && server.url && !isExpanded && (
@@ -251,14 +251,14 @@ export const MCPServerList: React.FC = ({
)}
-
+
{Object.keys(server.env).length > 0 && !isExpanded && (
{t('mcp.environmentVariablesCount', { count: Object.keys(server.env).length })}
)}
-
+
-
+
{/* Expanded Details */}
{isExpanded && (
= ({
)}
-
+
{server.args && server.args.length > 0 && (
{t('mcp.arguments')}
@@ -342,7 +342,7 @@ export const MCPServerList: React.FC
= ({
)}
-
+
{server.transport === "sse" && server.url && (
{t('mcp.url')}
@@ -351,7 +351,7 @@ export const MCPServerList: React.FC
= ({
)}
-
+
{Object.keys(server.env).length > 0 && (
{t('mcp.environmentVariables')}
@@ -433,4 +433,4 @@ export const MCPServerList: React.FC
= ({
)}
);
-};
\ No newline at end of file
+};
diff --git a/src/components/WelcomePage.tsx b/src/components/WelcomePage.tsx
index 0fdbf0b..c8578c1 100644
--- a/src/components/WelcomePage.tsx
+++ b/src/components/WelcomePage.tsx
@@ -1,5 +1,5 @@
import { motion } from "framer-motion";
-import { Bot, FolderCode, BarChart, ServerCog, FileText, Settings, Network } from "lucide-react";
+import { Bot, FolderCode, BarChart, ServerCog, FileText, Settings, Network, Router } from "lucide-react";
import { useTranslation } from "@/hooks/useTranslation";
import { Button } from "@/components/ui/button";
import { ClaudiaLogoMinimal } from "@/components/ClaudiaLogo";
@@ -61,6 +61,15 @@ export function WelcomePage({ onNavigate, onNewSession }: WelcomePageProps) {
bgColor: "bg-orange-500/10",
view: "mcp"
},
+ {
+ id: "ccr-router",
+ icon: Router,
+ title: t("welcome.ccrRouter"),
+ subtitle: t("welcome.ccrRouterDesc"),
+ color: "text-orange-500",
+ bgColor: "bg-orange-500/10",
+ view: "ccr-router"
+ },
{
id: "claude-md",
icon: FileText,
@@ -147,7 +156,7 @@ export function WelcomePage({ onNavigate, onNewSession }: WelcomePageProps) {
{/* Bottom Feature Cards */}
-
+
{bottomFeatures.map((feature, index) => (
("get_claude_settings");
console.log("Raw result from get_claude_settings:", result);
-
+
// The Rust backend returns ClaudeSettings { data: ... }
// We need to extract the data field
if (result && typeof result === 'object' && 'data' in result) {
return result.data;
}
-
+
// If the result is already the settings object, return it
return result as ClaudeSettings;
} catch (error) {
@@ -805,7 +805,7 @@ export const api = {
},
// Agent API methods
-
+
/**
* Lists all CC agents
* @returns Promise resolving to an array of agents
@@ -830,17 +830,17 @@ export const api = {
* @returns Promise resolving to the created agent
*/
async createAgent(
- name: string,
- icon: string,
- system_prompt: string,
- default_task?: string,
+ name: string,
+ icon: string,
+ system_prompt: string,
+ default_task?: string,
model?: string,
hooks?: string
): Promise {
try {
- return await invoke('create_agent', {
- name,
- icon,
+ return await invoke('create_agent', {
+ name,
+ icon,
systemPrompt: system_prompt,
defaultTask: default_task,
model,
@@ -864,19 +864,19 @@ export const api = {
* @returns Promise resolving to the updated agent
*/
async updateAgent(
- id: number,
- name: string,
- icon: string,
- system_prompt: string,
- default_task?: string,
+ id: number,
+ name: string,
+ icon: string,
+ system_prompt: string,
+ default_task?: string,
model?: string,
hooks?: string
): Promise {
try {
- return await invoke('update_agent', {
- id,
- name,
- icon,
+ return await invoke('update_agent', {
+ id,
+ name,
+ icon,
systemPrompt: system_prompt,
defaultTask: default_task,
model,
@@ -1545,9 +1545,9 @@ export const api = {
* Tracks a batch of messages for a session for checkpointing
*/
trackSessionMessages: (
- sessionId: string,
- projectId: string,
- projectPath: string,
+ sessionId: string,
+ projectId: string,
+ projectPath: string,
messages: string[]
): Promise =>
invoke("track_session_messages", { sessionId, projectId, projectPath, messages }),
@@ -1662,7 +1662,7 @@ export const api = {
try {
return await invoke("mcp_test_connection", { name });
} catch (error) {
- console.error("Failed to test MCP connection:", error);
+ console.error("Failed to test.md MCP connection:", error);
throw error;
}
},
@@ -2289,13 +2289,13 @@ export const api = {
/**
* Tests relay station connection
* @param stationId - The relay station ID
- * @returns Promise resolving to connection test result
+ * @returns Promise resolving to connection test.md result
*/
async relayStationTestConnection(stationId: string): Promise {
try {
return await invoke("relay_station_test_connection", { stationId });
} catch (error) {
- console.error("Failed to test connection:", error);
+ console.error("Failed to test.md connection:", error);
throw error;
}
},
@@ -2309,9 +2309,9 @@ export const api = {
* @returns Promise resolving to usage logs
*/
async relayStationGetUsageLogs(
- stationId: string,
- userId: string,
- page?: number,
+ stationId: string,
+ userId: string,
+ page?: number,
size?: number
): Promise {
try {
@@ -2330,8 +2330,8 @@ export const api = {
* @returns Promise resolving to token pagination response
*/
async relayStationListTokens(
- stationId: string,
- page?: number,
+ stationId: string,
+ page?: number,
size?: number
): Promise {
try {
@@ -2350,8 +2350,8 @@ export const api = {
* @returns Promise resolving to created token info
*/
async relayStationCreateToken(
- stationId: string,
- name: string,
+ stationId: string,
+ name: string,
quota?: number
): Promise {
try {
@@ -2371,9 +2371,9 @@ export const api = {
* @returns Promise resolving to updated token info
*/
async relayStationUpdateToken(
- stationId: string,
- tokenId: string,
- name?: string,
+ stationId: string,
+ tokenId: string,
+ name?: string,
quota?: number
): Promise {
try {
@@ -2400,16 +2400,16 @@ export const api = {
},
// ============= PackyCode Nodes =============
-
+
/**
- * Tests all PackyCode nodes and returns speed test results
- * @returns Promise resolving to array of node speed test results
+ * Tests all PackyCode nodes and returns speed test.md results
+ * @returns Promise resolving to array of node speed test.md results
*/
async testAllPackycodeNodes(): Promise {
try {
return await invoke("test_all_packycode_nodes");
} catch (error) {
- console.error("Failed to test PackyCode nodes:", error);
+ console.error("Failed to test.md PackyCode nodes:", error);
throw error;
}
},
@@ -2455,7 +2455,7 @@ export const api = {
},
// ============= File System Watching =============
-
+
/**
* Starts watching a directory for file system changes
* @param directoryPath - The directory path to watch
@@ -2486,7 +2486,7 @@ export const api = {
},
// ============= Claude Project Directory Watching =============
-
+
/**
* Starts watching Claude project directory for the given project path
* @param projectPath - The project path to find the corresponding Claude directory
@@ -2516,7 +2516,7 @@ export const api = {
},
// ============= Terminal API =============
-
+
/**
* Creates a new terminal session using Zellij
* @param workingDirectory - The working directory for the terminal session
@@ -2621,3 +2621,117 @@ export const api = {
}
}
};
+
+// CCR (Claude Code Router) Related Interfaces
+export interface CcrServiceStatus {
+ is_running: boolean;
+ port?: number;
+ endpoint?: string;
+ has_ccr_binary: boolean;
+ ccr_version?: string;
+ process_id?: number;
+}
+
+export interface CcrServiceInfo {
+ status: CcrServiceStatus;
+ message: string;
+}
+
+// CCR API methods
+export const ccrApi = {
+ /**
+ * Check if CCR is installed
+ */
+ async checkInstallation(): Promise {
+ try {
+ return await invoke("check_ccr_installation");
+ } catch (error) {
+ console.error("Failed to check CCR installation:", error);
+ throw error;
+ }
+ },
+
+ /**
+ * Get CCR version
+ */
+ async getVersion(): Promise {
+ try {
+ return await invoke("get_ccr_version");
+ } catch (error) {
+ console.error("Failed to get CCR version:", error);
+ throw error;
+ }
+ },
+
+ /**
+ * Get CCR service status
+ */
+ async getServiceStatus(): Promise {
+ try {
+ return await invoke("get_ccr_service_status");
+ } catch (error) {
+ console.error("Failed to get CCR service status:", error);
+ throw error;
+ }
+ },
+
+ /**
+ * Start CCR service
+ */
+ async startService(): Promise {
+ try {
+ return await invoke("start_ccr_service");
+ } catch (error) {
+ console.error("Failed to start CCR service:", error);
+ throw error;
+ }
+ },
+
+ /**
+ * Stop CCR service
+ */
+ async stopService(): Promise {
+ try {
+ return await invoke("stop_ccr_service");
+ } catch (error) {
+ console.error("Failed to stop CCR service:", error);
+ throw error;
+ }
+ },
+
+ /**
+ * Restart CCR service
+ */
+ async restartService(): Promise {
+ try {
+ return await invoke("restart_ccr_service");
+ } catch (error) {
+ console.error("Failed to restart CCR service:", error);
+ throw error;
+ }
+ },
+
+ /**
+ * Open CCR UI
+ */
+ async openUI(): Promise {
+ try {
+ return await invoke("open_ccr_ui");
+ } catch (error) {
+ console.error("Failed to open CCR UI:", error);
+ throw error;
+ }
+ },
+
+ /**
+ * Get CCR config file path
+ */
+ async getConfigPath(): Promise {
+ try {
+ return await invoke("get_ccr_config_path");
+ } catch (error) {
+ console.error("Failed to get CCR config path:", error);
+ throw error;
+ }
+ }
+};
diff --git a/src/locales/en/common.json b/src/locales/en/common.json
index 690cb0b..cd7eb3d 100644
--- a/src/locales/en/common.json
+++ b/src/locales/en/common.json
@@ -125,7 +125,9 @@
"claudeMdDesc": "Edit Claude configuration files",
"settings": "Settings",
"settingsDesc": "App settings and configuration",
- "quickStartSession": "Quick Start New Session"
+ "quickStartSession": "Quick Start New Session",
+ "ccrRouter": "CCR Router",
+ "ccrRouterDesc": "Claude Code Router configuration management"
},
"projects": {
"title": "Projects",
diff --git a/src/locales/zh/common.json b/src/locales/zh/common.json
index cbffe5a..129e686 100644
--- a/src/locales/zh/common.json
+++ b/src/locales/zh/common.json
@@ -122,7 +122,9 @@
"claudeMdDesc": "编辑 Claude 配置文件",
"settings": "设置",
"settingsDesc": "应用设置和配置",
- "quickStartSession": "快速开始新会话"
+ "quickStartSession": "快速开始新会话",
+ "ccrRouter": "CCR 路由",
+ "ccrRouterDesc": "Claude Code Router 配置管理"
},
"projects": {
"title": "项目",