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": "项目",