CCR
This commit is contained in:
@@ -118,7 +118,7 @@ bun run tauri build # 生产构建
|
|||||||
```bash
|
```bash
|
||||||
bun run tauri dev # 启动开发服务器
|
bun run tauri dev # 启动开发服务器
|
||||||
bunx tsc --noEmit # 类型检查
|
bunx tsc --noEmit # 类型检查
|
||||||
cd src-tauri && cargo test # Rust 测试
|
cd src-tauri && cargo test.md # Rust 测试
|
||||||
bun run check # 完整检查
|
bun run check # 完整检查
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use std::collections::HashMap;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
use dirs::home_dir;
|
use dirs::home_dir;
|
||||||
@@ -16,9 +17,27 @@ pub struct ClaudeConfig {
|
|||||||
pub model: Option<String>,
|
pub model: Option<String>,
|
||||||
#[serde(rename = "apiKeyHelper", skip_serializing_if = "Option::is_none")]
|
#[serde(rename = "apiKeyHelper", skip_serializing_if = "Option::is_none")]
|
||||||
pub api_key_helper: Option<String>,
|
pub api_key_helper: Option<String>,
|
||||||
|
#[serde(rename = "statusLine", skip_serializing_if = "Option::is_none")]
|
||||||
|
pub status_line: Option<StatusLineConfig>,
|
||||||
|
// 使用 flatten 来支持任何其他未知字段
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub extra_fields: std::collections::HashMap<String, serde_json::Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub command: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub padding: Option<i32>,
|
||||||
|
// 支持其他可能的 statusLine 字段
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub extra_fields: std::collections::HashMap<String, serde_json::Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ClaudeEnv {
|
pub struct ClaudeEnv {
|
||||||
#[serde(rename = "ANTHROPIC_AUTH_TOKEN", skip_serializing_if = "Option::is_none")]
|
#[serde(rename = "ANTHROPIC_AUTH_TOKEN", skip_serializing_if = "Option::is_none")]
|
||||||
pub anthropic_auth_token: Option<String>,
|
pub anthropic_auth_token: Option<String>,
|
||||||
@@ -26,6 +45,20 @@ pub struct ClaudeEnv {
|
|||||||
pub anthropic_base_url: Option<String>,
|
pub anthropic_base_url: Option<String>,
|
||||||
#[serde(rename = "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC", skip_serializing_if = "Option::is_none")]
|
#[serde(rename = "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC", skip_serializing_if = "Option::is_none")]
|
||||||
pub disable_nonessential_traffic: Option<String>,
|
pub disable_nonessential_traffic: Option<String>,
|
||||||
|
// 使用 flatten 来支持任何其他环境变量
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub extra_fields: std::collections::HashMap<String, serde_json::Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
@@ -59,6 +92,8 @@ pub fn read_claude_config() -> Result<ClaudeConfig, String> {
|
|||||||
permissions: Some(ClaudePermissions::default()),
|
permissions: Some(ClaudePermissions::default()),
|
||||||
model: None,
|
model: None,
|
||||||
api_key_helper: None,
|
api_key_helper: None,
|
||||||
|
status_line: None,
|
||||||
|
extra_fields: HashMap::new(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,7 +181,7 @@ pub fn restore_claude_config() -> Result<(), String> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 根据中转站配置更新 Claude 配置
|
/// 根据中转站配置更新 Claude 配置(仅更新 API 相关字段)
|
||||||
pub fn apply_relay_station_to_config(station: &RelayStation) -> Result<(), String> {
|
pub fn apply_relay_station_to_config(station: &RelayStation) -> Result<(), String> {
|
||||||
// 先备份当前配置
|
// 先备份当前配置
|
||||||
backup_claude_config()?;
|
backup_claude_config()?;
|
||||||
@@ -154,17 +189,17 @@ pub fn apply_relay_station_to_config(station: &RelayStation) -> Result<(), Strin
|
|||||||
// 读取当前配置
|
// 读取当前配置
|
||||||
let mut config = read_claude_config()?;
|
let mut config = read_claude_config()?;
|
||||||
|
|
||||||
// 更新 API URL
|
// 仅更新这三个关键字段,保留其他所有配置不变:
|
||||||
|
// 1. ANTHROPIC_BASE_URL
|
||||||
config.env.anthropic_base_url = Some(station.api_url.clone());
|
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());
|
config.env.anthropic_auth_token = Some(station.system_token.clone());
|
||||||
|
|
||||||
// 将中转站的 token 也设置到 apiKeyHelper
|
// 3. apiKeyHelper - 设置为 echo 格式
|
||||||
// 格式:echo 'token'
|
|
||||||
config.api_key_helper = Some(format!("echo '{}'", station.system_token));
|
config.api_key_helper = Some(format!("echo '{}'", station.system_token));
|
||||||
|
|
||||||
// 如果是特定适配器,可能需要特殊处理
|
// 如果是特定适配器,可能需要特殊处理 URL 格式
|
||||||
match station.adapter.as_str() {
|
match station.adapter.as_str() {
|
||||||
"packycode" => {
|
"packycode" => {
|
||||||
// PackyCode 使用原始配置,不做特殊处理
|
// PackyCode 使用原始配置,不做特殊处理
|
||||||
@@ -187,7 +222,7 @@ pub fn apply_relay_station_to_config(station: &RelayStation) -> Result<(), Strin
|
|||||||
// 写入更新后的配置
|
// 写入更新后的配置
|
||||||
write_claude_config(&config)?;
|
write_claude_config(&config)?;
|
||||||
|
|
||||||
log::info!("已将中转站 {} 的配置应用到 Claude 配置文件", station.name);
|
log::info!("已将中转站 {} 的 API 配置(apiKeyHelper, ANTHROPIC_BASE_URL, ANTHROPIC_AUTH_TOKEN)应用到 Claude 配置文件", station.name);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
337
src-tauri/src/commands/ccr.rs
Normal file
337
src-tauri/src/commands/ccr.rs
Normal file
@@ -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<u16>,
|
||||||
|
pub endpoint: Option<String>,
|
||||||
|
pub has_ccr_binary: bool,
|
||||||
|
pub ccr_version: Option<String>,
|
||||||
|
pub process_id: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct CcrServiceInfo {
|
||||||
|
pub status: CcrServiceStatus,
|
||||||
|
pub message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 检查 CCR 是否已安装
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn check_ccr_installation() -> Result<bool, String> {
|
||||||
|
// 直接尝试执行 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<String, String> {
|
||||||
|
// 尝试多个版本命令参数
|
||||||
|
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<CcrServiceStatus, String> {
|
||||||
|
// 首先检查 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::<u16>() {
|
||||||
|
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::<u32>() {
|
||||||
|
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<CcrServiceInfo, String> {
|
||||||
|
// 先检查是否已安装
|
||||||
|
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<CcrServiceInfo, String> {
|
||||||
|
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<CcrServiceInfo, String> {
|
||||||
|
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<String, String> {
|
||||||
|
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<String, String> {
|
||||||
|
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())
|
||||||
|
}
|
@@ -14,3 +14,4 @@ pub mod packycode_nodes;
|
|||||||
pub mod filesystem;
|
pub mod filesystem;
|
||||||
pub mod git;
|
pub mod git;
|
||||||
pub mod terminal;
|
pub mod terminal;
|
||||||
|
pub mod ccr;
|
||||||
|
@@ -79,6 +79,10 @@ use commands::terminal::{
|
|||||||
create_terminal_session, send_terminal_input, close_terminal_session,
|
create_terminal_session, send_terminal_input, close_terminal_session,
|
||||||
list_terminal_sessions, resize_terminal, cleanup_terminal_sessions, TerminalState,
|
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 process::ProcessRegistryState;
|
||||||
use file_watcher::FileWatcherState;
|
use file_watcher::FileWatcherState;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
@@ -417,6 +421,16 @@ fn main() {
|
|||||||
list_terminal_sessions,
|
list_terminal_sessions,
|
||||||
resize_terminal,
|
resize_terminal,
|
||||||
cleanup_terminal_sessions,
|
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!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
@@ -29,6 +29,7 @@ import { useAppLifecycle, useTrackEvent } from "@/hooks";
|
|||||||
import { useTranslation } from "@/hooks/useTranslation";
|
import { useTranslation } from "@/hooks/useTranslation";
|
||||||
import { WelcomePage } from "@/components/WelcomePage";
|
import { WelcomePage } from "@/components/WelcomePage";
|
||||||
import RelayStationManager from "@/components/RelayStationManager";
|
import RelayStationManager from "@/components/RelayStationManager";
|
||||||
|
import { CcrRouterManager } from "@/components/CcrRouterManager";
|
||||||
import i18n from "@/lib/i18n";
|
import i18n from "@/lib/i18n";
|
||||||
|
|
||||||
type View =
|
type View =
|
||||||
@@ -44,6 +45,7 @@ type View =
|
|||||||
| "agent-run-view"
|
| "agent-run-view"
|
||||||
| "mcp"
|
| "mcp"
|
||||||
| "relay-stations"
|
| "relay-stations"
|
||||||
|
| "ccr-router"
|
||||||
| "usage-dashboard"
|
| "usage-dashboard"
|
||||||
| "project-settings"
|
| "project-settings"
|
||||||
| "tabs"; // New view for tab-based interface
|
| "tabs"; // New view for tab-based interface
|
||||||
@@ -282,6 +284,11 @@ function AppContent() {
|
|||||||
<RelayStationManager onBack={() => handleViewChange("welcome")} />
|
<RelayStationManager onBack={() => handleViewChange("welcome")} />
|
||||||
);
|
);
|
||||||
|
|
||||||
|
case "ccr-router":
|
||||||
|
return (
|
||||||
|
<CcrRouterManager onBack={() => handleViewChange("welcome")} />
|
||||||
|
);
|
||||||
|
|
||||||
case "cc-agents":
|
case "cc-agents":
|
||||||
return (
|
return (
|
||||||
<CCAgents
|
<CCAgents
|
||||||
|
463
src/components/CcrRouterManager.tsx
Normal file
463
src/components/CcrRouterManager.tsx
Normal file
@@ -0,0 +1,463 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { ArrowLeft, Play, Square, RotateCcw, ExternalLink, Download, AlertCircle, CheckCircle, Loader2 } from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Toast, ToastContainer } from "@/components/ui/toast";
|
||||||
|
import { ccrApi, type CcrServiceStatus } from "@/lib/api";
|
||||||
|
import { open } from '@tauri-apps/plugin-shell';
|
||||||
|
|
||||||
|
interface CcrRouterManagerProps {
|
||||||
|
onBack: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CcrRouterManager({ onBack }: CcrRouterManagerProps) {
|
||||||
|
const [serviceStatus, setServiceStatus] = useState<CcrServiceStatus | null>(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<string>("");
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className={`w-3 h-3 rounded-full ${statusColor}`}></div>
|
||||||
|
<span className="font-medium">{statusText}</span>
|
||||||
|
{serviceStatus.is_running && serviceStatus.port && (
|
||||||
|
<Badge variant="secondary">端口 {serviceStatus.port}</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderInstallationStatus = () => {
|
||||||
|
if (!serviceStatus) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{serviceStatus.has_ccr_binary ? (
|
||||||
|
<>
|
||||||
|
<CheckCircle className="w-4 h-4 text-green-500" />
|
||||||
|
<span className="text-green-600">已安装</span>
|
||||||
|
{serviceStatus.ccr_version && (
|
||||||
|
<Badge variant="outline">{serviceStatus.ccr_version}</Badge>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<AlertCircle className="w-4 h-4 text-red-500" />
|
||||||
|
<span className="text-red-600">未安装</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="flex-1 flex items-center justify-center">
|
||||||
|
<Loader2 className="h-6 w-6 animate-spin" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex-1 overflow-y-auto">
|
||||||
|
<div className="container mx-auto p-6 max-w-4xl">
|
||||||
|
{/* Header */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: -20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
className="mb-6"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3 mb-4">
|
||||||
|
<Button variant="ghost" size="sm" onClick={onBack}>
|
||||||
|
<ArrowLeft className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold tracking-tight">CCR 路由管理</h1>
|
||||||
|
<p className="mt-1 text-sm text-muted-foreground">
|
||||||
|
管理 Claude Code Router 服务和配置
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Service Status Card */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.5, delay: 0.1 }}
|
||||||
|
className="mb-6"
|
||||||
|
>
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center justify-between">
|
||||||
|
<span>服务状态</span>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={loadServiceStatus}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
刷新
|
||||||
|
</Button>
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
CCR 路由服务当前状态和控制选项
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm font-medium">安装状态:</span>
|
||||||
|
{renderInstallationStatus()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm font-medium">服务状态:</span>
|
||||||
|
{renderServiceStatus()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{serviceStatus?.endpoint && (
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm font-medium">服务地址:</span>
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleOpenInBrowser}
|
||||||
|
className="p-0 h-auto"
|
||||||
|
>
|
||||||
|
{serviceStatus.endpoint}/ui/
|
||||||
|
<ExternalLink className="w-3 h-3 ml-1" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{serviceStatus?.process_id && (
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm font-medium">进程 ID:</span>
|
||||||
|
<Badge variant="outline">{serviceStatus.process_id}</Badge>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{configPath && (
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm font-medium">配置文件:</span>
|
||||||
|
<span className="text-xs text-muted-foreground font-mono">
|
||||||
|
{configPath}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Control Panel */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.5, delay: 0.2 }}
|
||||||
|
className="mb-6"
|
||||||
|
>
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>服务控制</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
启动、停止或重启 CCR 路由服务
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{serviceStatus?.has_ccr_binary ? (
|
||||||
|
<div className="flex gap-3 flex-wrap">
|
||||||
|
{!serviceStatus.is_running ? (
|
||||||
|
<Button
|
||||||
|
onClick={handleStartService}
|
||||||
|
disabled={actionLoading}
|
||||||
|
className="gap-2"
|
||||||
|
>
|
||||||
|
{actionLoading ? (
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Play className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
启动服务
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
onClick={handleStopService}
|
||||||
|
disabled={actionLoading}
|
||||||
|
variant="destructive"
|
||||||
|
className="gap-2"
|
||||||
|
>
|
||||||
|
{actionLoading ? (
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Square className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
停止服务
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={handleRestartService}
|
||||||
|
disabled={actionLoading}
|
||||||
|
variant="outline"
|
||||||
|
className="gap-2"
|
||||||
|
>
|
||||||
|
{actionLoading ? (
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<RotateCcw className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
重启服务
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={handleOpenUI}
|
||||||
|
disabled={actionLoading}
|
||||||
|
className="gap-2"
|
||||||
|
>
|
||||||
|
{actionLoading ? (
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<ExternalLink className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
{serviceStatus.is_running ? "打开管理界面" : "启动并打开管理界面"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-8">
|
||||||
|
<AlertCircle className="w-12 h-12 text-muted-foreground mx-auto mb-4" />
|
||||||
|
<h3 className="text-lg font-medium mb-2">CCR 未安装</h3>
|
||||||
|
<p className="text-muted-foreground mb-4">
|
||||||
|
需要先安装 Claude Code Router 才能使用此功能
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
onClick={() => open("https://www.npmjs.com/package/@musistudio/claude-code-router")}
|
||||||
|
className="gap-2"
|
||||||
|
>
|
||||||
|
<Download className="w-4 h-4" />
|
||||||
|
安装 CCR
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Information Card */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.5, delay: 0.3 }}
|
||||||
|
>
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>关于 CCR 路由</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-3 text-sm text-muted-foreground">
|
||||||
|
<p>
|
||||||
|
Claude Code Router (CCR) 是一个强大的路由工具,允许您将 Claude Code 请求转发到不同的 LLM 提供商。
|
||||||
|
</p>
|
||||||
|
<ul className="list-disc list-inside space-y-1">
|
||||||
|
<li>支持多个 LLM 提供商(OpenRouter、DeepSeek、Gemini 等)</li>
|
||||||
|
<li>智能路由规则,根据令牌数量和请求类型自动选择</li>
|
||||||
|
<li>Web UI 管理界面,方便配置和监控</li>
|
||||||
|
<li>无需 Anthropic 账户即可使用 Claude Code</li>
|
||||||
|
</ul>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Toast Container */}
|
||||||
|
<ToastContainer>
|
||||||
|
{toast && (
|
||||||
|
<Toast
|
||||||
|
message={toast.message}
|
||||||
|
type={toast.type}
|
||||||
|
onDismiss={() => setToast(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ToastContainer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@@ -1,11 +1,11 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
import {
|
import {
|
||||||
Network,
|
Network,
|
||||||
Globe,
|
Globe,
|
||||||
Terminal,
|
Terminal,
|
||||||
Trash2,
|
Trash2,
|
||||||
Play,
|
Play,
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
Loader2,
|
Loader2,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
@@ -57,7 +57,7 @@ export const MCPServerList: React.FC<MCPServerListProps> = ({
|
|||||||
const [expandedServers, setExpandedServers] = useState<Set<string>>(new Set());
|
const [expandedServers, setExpandedServers] = useState<Set<string>>(new Set());
|
||||||
const [copiedServer, setCopiedServer] = useState<string | null>(null);
|
const [copiedServer, setCopiedServer] = useState<string | null>(null);
|
||||||
const [connectedServers] = useState<string[]>([]);
|
const [connectedServers] = useState<string[]>([]);
|
||||||
|
|
||||||
// Analytics tracking
|
// Analytics tracking
|
||||||
const trackEvent = useTrackEvent();
|
const trackEvent = useTrackEvent();
|
||||||
|
|
||||||
@@ -103,18 +103,18 @@ export const MCPServerList: React.FC<MCPServerListProps> = ({
|
|||||||
const handleRemoveServer = async (name: string) => {
|
const handleRemoveServer = async (name: string) => {
|
||||||
try {
|
try {
|
||||||
setRemovingServer(name);
|
setRemovingServer(name);
|
||||||
|
|
||||||
// Check if server was connected
|
// Check if server was connected
|
||||||
const wasConnected = connectedServers.includes(name);
|
const wasConnected = connectedServers.includes(name);
|
||||||
|
|
||||||
await api.mcpRemove(name);
|
await api.mcpRemove(name);
|
||||||
|
|
||||||
// Track server removal
|
// Track server removal
|
||||||
trackEvent.mcpServerRemoved({
|
trackEvent.mcpServerRemoved({
|
||||||
server_name: name,
|
server_name: name,
|
||||||
was_connected: wasConnected
|
was_connected: wasConnected
|
||||||
});
|
});
|
||||||
|
|
||||||
onServerRemoved(name);
|
onServerRemoved(name);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to remove server:", error);
|
console.error("Failed to remove server:", error);
|
||||||
@@ -131,15 +131,15 @@ export const MCPServerList: React.FC<MCPServerListProps> = ({
|
|||||||
setTestingServer(name);
|
setTestingServer(name);
|
||||||
const result = await api.mcpTestConnection(name);
|
const result = await api.mcpTestConnection(name);
|
||||||
const server = servers.find(s => s.name === name);
|
const server = servers.find(s => s.name === name);
|
||||||
|
|
||||||
// Track connection result - result is a string message
|
// Track connection result - result is a string message
|
||||||
trackEvent.mcpServerConnected(name, true, server?.transport || 'unknown');
|
trackEvent.mcpServerConnected(name, true, server?.transport || 'unknown');
|
||||||
|
|
||||||
// TODO: Show result in a toast or modal
|
// TODO: Show result in a toast or modal
|
||||||
console.log("Test result:", result);
|
console.log("Test result:", result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to test connection:", error);
|
console.error("Failed to test.md connection:", error);
|
||||||
|
|
||||||
trackEvent.mcpConnectionError({
|
trackEvent.mcpConnectionError({
|
||||||
server_name: name,
|
server_name: name,
|
||||||
error_type: 'test_failed',
|
error_type: 'test_failed',
|
||||||
@@ -202,7 +202,7 @@ export const MCPServerList: React.FC<MCPServerListProps> = ({
|
|||||||
const renderServerItem = (server: MCPServer) => {
|
const renderServerItem = (server: MCPServer) => {
|
||||||
const isExpanded = expandedServers.has(server.name);
|
const isExpanded = expandedServers.has(server.name);
|
||||||
const isCopied = copiedServer === server.name;
|
const isCopied = copiedServer === server.name;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={server.name}
|
key={server.name}
|
||||||
@@ -226,7 +226,7 @@ export const MCPServerList: React.FC<MCPServerListProps> = ({
|
|||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{server.command && !isExpanded && (
|
{server.command && !isExpanded && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<p className="text-xs text-muted-foreground font-mono truncate pl-9 flex-1" title={server.command}>
|
<p className="text-xs text-muted-foreground font-mono truncate pl-9 flex-1" title={server.command}>
|
||||||
@@ -243,7 +243,7 @@ export const MCPServerList: React.FC<MCPServerListProps> = ({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{server.transport === "sse" && server.url && !isExpanded && (
|
{server.transport === "sse" && server.url && !isExpanded && (
|
||||||
<div className="overflow-hidden">
|
<div className="overflow-hidden">
|
||||||
<p className="text-xs text-muted-foreground font-mono truncate pl-9" title={server.url}>
|
<p className="text-xs text-muted-foreground font-mono truncate pl-9" title={server.url}>
|
||||||
@@ -251,14 +251,14 @@ export const MCPServerList: React.FC<MCPServerListProps> = ({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{Object.keys(server.env).length > 0 && !isExpanded && (
|
{Object.keys(server.env).length > 0 && !isExpanded && (
|
||||||
<div className="flex items-center gap-1 text-xs text-muted-foreground pl-9">
|
<div className="flex items-center gap-1 text-xs text-muted-foreground pl-9">
|
||||||
<span>{t('mcp.environmentVariablesCount', { count: Object.keys(server.env).length })}</span>
|
<span>{t('mcp.environmentVariablesCount', { count: Object.keys(server.env).length })}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0">
|
<div className="flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -288,7 +288,7 @@ export const MCPServerList: React.FC<MCPServerListProps> = ({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Expanded Details */}
|
{/* Expanded Details */}
|
||||||
{isExpanded && (
|
{isExpanded && (
|
||||||
<motion.div
|
<motion.div
|
||||||
@@ -328,7 +328,7 @@ export const MCPServerList: React.FC<MCPServerListProps> = ({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{server.args && server.args.length > 0 && (
|
{server.args && server.args.length > 0 && (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<p className="text-xs font-medium text-muted-foreground">{t('mcp.arguments')}</p>
|
<p className="text-xs font-medium text-muted-foreground">{t('mcp.arguments')}</p>
|
||||||
@@ -342,7 +342,7 @@ export const MCPServerList: React.FC<MCPServerListProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{server.transport === "sse" && server.url && (
|
{server.transport === "sse" && server.url && (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<p className="text-xs font-medium text-muted-foreground">{t('mcp.url')}</p>
|
<p className="text-xs font-medium text-muted-foreground">{t('mcp.url')}</p>
|
||||||
@@ -351,7 +351,7 @@ export const MCPServerList: React.FC<MCPServerListProps> = ({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{Object.keys(server.env).length > 0 && (
|
{Object.keys(server.env).length > 0 && (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<p className="text-xs font-medium text-muted-foreground">{t('mcp.environmentVariables')}</p>
|
<p className="text-xs font-medium text-muted-foreground">{t('mcp.environmentVariables')}</p>
|
||||||
@@ -433,4 +433,4 @@ export const MCPServerList: React.FC<MCPServerListProps> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { motion } from "framer-motion";
|
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 { useTranslation } from "@/hooks/useTranslation";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { ClaudiaLogoMinimal } from "@/components/ClaudiaLogo";
|
import { ClaudiaLogoMinimal } from "@/components/ClaudiaLogo";
|
||||||
@@ -61,6 +61,15 @@ export function WelcomePage({ onNavigate, onNewSession }: WelcomePageProps) {
|
|||||||
bgColor: "bg-orange-500/10",
|
bgColor: "bg-orange-500/10",
|
||||||
view: "mcp"
|
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",
|
id: "claude-md",
|
||||||
icon: FileText,
|
icon: FileText,
|
||||||
@@ -147,7 +156,7 @@ export function WelcomePage({ onNavigate, onNewSession }: WelcomePageProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bottom Feature Cards */}
|
{/* Bottom Feature Cards */}
|
||||||
<div className="grid grid-cols-4 gap-6 mb-10">
|
<div className="grid grid-cols-5 gap-6 mb-10">
|
||||||
{bottomFeatures.map((feature, index) => (
|
{bottomFeatures.map((feature, index) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={feature.id}
|
key={feature.id}
|
||||||
|
206
src/lib/api.ts
206
src/lib/api.ts
@@ -2,7 +2,7 @@ import { invoke } from "@tauri-apps/api/core";
|
|||||||
import type { HooksConfiguration } from '@/types/hooks';
|
import type { HooksConfiguration } from '@/types/hooks';
|
||||||
|
|
||||||
/** Process type for tracking in ProcessRegistry */
|
/** Process type for tracking in ProcessRegistry */
|
||||||
export type ProcessType =
|
export type ProcessType =
|
||||||
| { AgentRun: { agent_id: number; agent_name: string } }
|
| { AgentRun: { agent_id: number; agent_name: string } }
|
||||||
| { ClaudeSession: { session_id: string } };
|
| { ClaudeSession: { session_id: string } };
|
||||||
|
|
||||||
@@ -451,7 +451,7 @@ export interface ImportServerResult {
|
|||||||
// ================================
|
// ================================
|
||||||
|
|
||||||
/** 中转站适配器类型 */
|
/** 中转站适配器类型 */
|
||||||
export type RelayStationAdapter =
|
export type RelayStationAdapter =
|
||||||
| 'packycode' // PackyCode 平台(默认)
|
| 'packycode' // PackyCode 平台(默认)
|
||||||
| 'newapi' // NewAPI 兼容平台
|
| 'newapi' // NewAPI 兼容平台
|
||||||
| 'oneapi' // OneAPI 兼容平台
|
| 'oneapi' // OneAPI 兼容平台
|
||||||
@@ -459,7 +459,7 @@ export type RelayStationAdapter =
|
|||||||
| 'custom'; // 自定义简单配置
|
| 'custom'; // 自定义简单配置
|
||||||
|
|
||||||
/** 认证方式 */
|
/** 认证方式 */
|
||||||
export type AuthMethod =
|
export type AuthMethod =
|
||||||
| 'bearer_token' // Bearer Token 认证(推荐)
|
| 'bearer_token' // Bearer Token 认证(推荐)
|
||||||
| 'api_key' // API Key 认证
|
| 'api_key' // API Key 认证
|
||||||
| 'custom'; // 自定义认证方式
|
| 'custom'; // 自定义认证方式
|
||||||
@@ -560,7 +560,7 @@ export interface TokenPaginationResponse {
|
|||||||
// ============= PackyCode Nodes =============
|
// ============= PackyCode Nodes =============
|
||||||
|
|
||||||
/** PackyCode 节点类型 */
|
/** PackyCode 节点类型 */
|
||||||
export type NodeType =
|
export type NodeType =
|
||||||
| 'direct' // 直连节点
|
| 'direct' // 直连节点
|
||||||
| 'backup' // 备用节点
|
| 'backup' // 备用节点
|
||||||
| 'emergency'; // 紧急节点
|
| 'emergency'; // 紧急节点
|
||||||
@@ -678,13 +678,13 @@ export const api = {
|
|||||||
try {
|
try {
|
||||||
const result = await invoke<{ data: ClaudeSettings }>("get_claude_settings");
|
const result = await invoke<{ data: ClaudeSettings }>("get_claude_settings");
|
||||||
console.log("Raw result from get_claude_settings:", result);
|
console.log("Raw result from get_claude_settings:", result);
|
||||||
|
|
||||||
// The Rust backend returns ClaudeSettings { data: ... }
|
// The Rust backend returns ClaudeSettings { data: ... }
|
||||||
// We need to extract the data field
|
// We need to extract the data field
|
||||||
if (result && typeof result === 'object' && 'data' in result) {
|
if (result && typeof result === 'object' && 'data' in result) {
|
||||||
return result.data;
|
return result.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the result is already the settings object, return it
|
// If the result is already the settings object, return it
|
||||||
return result as ClaudeSettings;
|
return result as ClaudeSettings;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -805,7 +805,7 @@ export const api = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Agent API methods
|
// Agent API methods
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lists all CC agents
|
* Lists all CC agents
|
||||||
* @returns Promise resolving to an array of agents
|
* @returns Promise resolving to an array of agents
|
||||||
@@ -830,17 +830,17 @@ export const api = {
|
|||||||
* @returns Promise resolving to the created agent
|
* @returns Promise resolving to the created agent
|
||||||
*/
|
*/
|
||||||
async createAgent(
|
async createAgent(
|
||||||
name: string,
|
name: string,
|
||||||
icon: string,
|
icon: string,
|
||||||
system_prompt: string,
|
system_prompt: string,
|
||||||
default_task?: string,
|
default_task?: string,
|
||||||
model?: string,
|
model?: string,
|
||||||
hooks?: string
|
hooks?: string
|
||||||
): Promise<Agent> {
|
): Promise<Agent> {
|
||||||
try {
|
try {
|
||||||
return await invoke<Agent>('create_agent', {
|
return await invoke<Agent>('create_agent', {
|
||||||
name,
|
name,
|
||||||
icon,
|
icon,
|
||||||
systemPrompt: system_prompt,
|
systemPrompt: system_prompt,
|
||||||
defaultTask: default_task,
|
defaultTask: default_task,
|
||||||
model,
|
model,
|
||||||
@@ -864,19 +864,19 @@ export const api = {
|
|||||||
* @returns Promise resolving to the updated agent
|
* @returns Promise resolving to the updated agent
|
||||||
*/
|
*/
|
||||||
async updateAgent(
|
async updateAgent(
|
||||||
id: number,
|
id: number,
|
||||||
name: string,
|
name: string,
|
||||||
icon: string,
|
icon: string,
|
||||||
system_prompt: string,
|
system_prompt: string,
|
||||||
default_task?: string,
|
default_task?: string,
|
||||||
model?: string,
|
model?: string,
|
||||||
hooks?: string
|
hooks?: string
|
||||||
): Promise<Agent> {
|
): Promise<Agent> {
|
||||||
try {
|
try {
|
||||||
return await invoke<Agent>('update_agent', {
|
return await invoke<Agent>('update_agent', {
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
icon,
|
icon,
|
||||||
systemPrompt: system_prompt,
|
systemPrompt: system_prompt,
|
||||||
defaultTask: default_task,
|
defaultTask: default_task,
|
||||||
model,
|
model,
|
||||||
@@ -1545,9 +1545,9 @@ export const api = {
|
|||||||
* Tracks a batch of messages for a session for checkpointing
|
* Tracks a batch of messages for a session for checkpointing
|
||||||
*/
|
*/
|
||||||
trackSessionMessages: (
|
trackSessionMessages: (
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
projectPath: string,
|
projectPath: string,
|
||||||
messages: string[]
|
messages: string[]
|
||||||
): Promise<void> =>
|
): Promise<void> =>
|
||||||
invoke("track_session_messages", { sessionId, projectId, projectPath, messages }),
|
invoke("track_session_messages", { sessionId, projectId, projectPath, messages }),
|
||||||
@@ -1662,7 +1662,7 @@ export const api = {
|
|||||||
try {
|
try {
|
||||||
return await invoke<string>("mcp_test_connection", { name });
|
return await invoke<string>("mcp_test_connection", { name });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to test MCP connection:", error);
|
console.error("Failed to test.md MCP connection:", error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2289,13 +2289,13 @@ export const api = {
|
|||||||
/**
|
/**
|
||||||
* Tests relay station connection
|
* Tests relay station connection
|
||||||
* @param stationId - The relay station ID
|
* @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<ConnectionTestResult> {
|
async relayStationTestConnection(stationId: string): Promise<ConnectionTestResult> {
|
||||||
try {
|
try {
|
||||||
return await invoke<ConnectionTestResult>("relay_station_test_connection", { stationId });
|
return await invoke<ConnectionTestResult>("relay_station_test_connection", { stationId });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to test connection:", error);
|
console.error("Failed to test.md connection:", error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2309,9 +2309,9 @@ export const api = {
|
|||||||
* @returns Promise resolving to usage logs
|
* @returns Promise resolving to usage logs
|
||||||
*/
|
*/
|
||||||
async relayStationGetUsageLogs(
|
async relayStationGetUsageLogs(
|
||||||
stationId: string,
|
stationId: string,
|
||||||
userId: string,
|
userId: string,
|
||||||
page?: number,
|
page?: number,
|
||||||
size?: number
|
size?: number
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
try {
|
try {
|
||||||
@@ -2330,8 +2330,8 @@ export const api = {
|
|||||||
* @returns Promise resolving to token pagination response
|
* @returns Promise resolving to token pagination response
|
||||||
*/
|
*/
|
||||||
async relayStationListTokens(
|
async relayStationListTokens(
|
||||||
stationId: string,
|
stationId: string,
|
||||||
page?: number,
|
page?: number,
|
||||||
size?: number
|
size?: number
|
||||||
): Promise<TokenPaginationResponse> {
|
): Promise<TokenPaginationResponse> {
|
||||||
try {
|
try {
|
||||||
@@ -2350,8 +2350,8 @@ export const api = {
|
|||||||
* @returns Promise resolving to created token info
|
* @returns Promise resolving to created token info
|
||||||
*/
|
*/
|
||||||
async relayStationCreateToken(
|
async relayStationCreateToken(
|
||||||
stationId: string,
|
stationId: string,
|
||||||
name: string,
|
name: string,
|
||||||
quota?: number
|
quota?: number
|
||||||
): Promise<TokenInfo> {
|
): Promise<TokenInfo> {
|
||||||
try {
|
try {
|
||||||
@@ -2371,9 +2371,9 @@ export const api = {
|
|||||||
* @returns Promise resolving to updated token info
|
* @returns Promise resolving to updated token info
|
||||||
*/
|
*/
|
||||||
async relayStationUpdateToken(
|
async relayStationUpdateToken(
|
||||||
stationId: string,
|
stationId: string,
|
||||||
tokenId: string,
|
tokenId: string,
|
||||||
name?: string,
|
name?: string,
|
||||||
quota?: number
|
quota?: number
|
||||||
): Promise<TokenInfo> {
|
): Promise<TokenInfo> {
|
||||||
try {
|
try {
|
||||||
@@ -2400,16 +2400,16 @@ export const api = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// ============= PackyCode Nodes =============
|
// ============= PackyCode Nodes =============
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests all PackyCode nodes and returns speed test results
|
* Tests all PackyCode nodes and returns speed test.md results
|
||||||
* @returns Promise resolving to array of node speed test results
|
* @returns Promise resolving to array of node speed test.md results
|
||||||
*/
|
*/
|
||||||
async testAllPackycodeNodes(): Promise<NodeSpeedTestResult[]> {
|
async testAllPackycodeNodes(): Promise<NodeSpeedTestResult[]> {
|
||||||
try {
|
try {
|
||||||
return await invoke<NodeSpeedTestResult[]>("test_all_packycode_nodes");
|
return await invoke<NodeSpeedTestResult[]>("test_all_packycode_nodes");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to test PackyCode nodes:", error);
|
console.error("Failed to test.md PackyCode nodes:", error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2455,7 +2455,7 @@ export const api = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// ============= File System Watching =============
|
// ============= File System Watching =============
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts watching a directory for file system changes
|
* Starts watching a directory for file system changes
|
||||||
* @param directoryPath - The directory path to watch
|
* @param directoryPath - The directory path to watch
|
||||||
@@ -2486,7 +2486,7 @@ export const api = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// ============= Claude Project Directory Watching =============
|
// ============= Claude Project Directory Watching =============
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts watching Claude project directory for the given project path
|
* Starts watching Claude project directory for the given project path
|
||||||
* @param projectPath - The project path to find the corresponding Claude directory
|
* @param projectPath - The project path to find the corresponding Claude directory
|
||||||
@@ -2516,7 +2516,7 @@ export const api = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// ============= Terminal API =============
|
// ============= Terminal API =============
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new terminal session using Zellij
|
* Creates a new terminal session using Zellij
|
||||||
* @param workingDirectory - The working directory for the terminal session
|
* @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<boolean> {
|
||||||
|
try {
|
||||||
|
return await invoke<boolean>("check_ccr_installation");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to check CCR installation:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get CCR version
|
||||||
|
*/
|
||||||
|
async getVersion(): Promise<string> {
|
||||||
|
try {
|
||||||
|
return await invoke<string>("get_ccr_version");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to get CCR version:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get CCR service status
|
||||||
|
*/
|
||||||
|
async getServiceStatus(): Promise<CcrServiceStatus> {
|
||||||
|
try {
|
||||||
|
return await invoke<CcrServiceStatus>("get_ccr_service_status");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to get CCR service status:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start CCR service
|
||||||
|
*/
|
||||||
|
async startService(): Promise<CcrServiceInfo> {
|
||||||
|
try {
|
||||||
|
return await invoke<CcrServiceInfo>("start_ccr_service");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to start CCR service:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop CCR service
|
||||||
|
*/
|
||||||
|
async stopService(): Promise<CcrServiceInfo> {
|
||||||
|
try {
|
||||||
|
return await invoke<CcrServiceInfo>("stop_ccr_service");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to stop CCR service:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restart CCR service
|
||||||
|
*/
|
||||||
|
async restartService(): Promise<CcrServiceInfo> {
|
||||||
|
try {
|
||||||
|
return await invoke<CcrServiceInfo>("restart_ccr_service");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to restart CCR service:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open CCR UI
|
||||||
|
*/
|
||||||
|
async openUI(): Promise<string> {
|
||||||
|
try {
|
||||||
|
return await invoke<string>("open_ccr_ui");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to open CCR UI:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get CCR config file path
|
||||||
|
*/
|
||||||
|
async getConfigPath(): Promise<string> {
|
||||||
|
try {
|
||||||
|
return await invoke<string>("get_ccr_config_path");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to get CCR config path:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
@@ -125,7 +125,9 @@
|
|||||||
"claudeMdDesc": "Edit Claude configuration files",
|
"claudeMdDesc": "Edit Claude configuration files",
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
"settingsDesc": "App settings and configuration",
|
"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": {
|
"projects": {
|
||||||
"title": "Projects",
|
"title": "Projects",
|
||||||
|
@@ -122,7 +122,9 @@
|
|||||||
"claudeMdDesc": "编辑 Claude 配置文件",
|
"claudeMdDesc": "编辑 Claude 配置文件",
|
||||||
"settings": "设置",
|
"settings": "设置",
|
||||||
"settingsDesc": "应用设置和配置",
|
"settingsDesc": "应用设置和配置",
|
||||||
"quickStartSession": "快速开始新会话"
|
"quickStartSession": "快速开始新会话",
|
||||||
|
"ccrRouter": "CCR 路由",
|
||||||
|
"ccrRouterDesc": "Claude Code Router 配置管理"
|
||||||
},
|
},
|
||||||
"projects": {
|
"projects": {
|
||||||
"title": "项目",
|
"title": "项目",
|
||||||
|
Reference in New Issue
Block a user