mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-06-16 13:34:04 +08:00
Merge tianrking/main: feat: add provider-specific terminal button
Merged PR #452 which adds: - Terminal button for Claude providers to launch with provider-specific config - Cross-platform support (macOS/Linux/Windows) - Auto-cleanup of temporary config files
This commit is contained in:
@@ -1,9 +1,13 @@
|
|||||||
#![allow(non_snake_case)]
|
#![allow(non_snake_case)]
|
||||||
|
|
||||||
|
use crate::app_config::AppType;
|
||||||
use crate::init_status::{InitErrorPayload, SkillsMigrationPayload};
|
use crate::init_status::{InitErrorPayload, SkillsMigrationPayload};
|
||||||
|
use crate::services::ProviderService;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
use std::str::FromStr;
|
||||||
use tauri::AppHandle;
|
use tauri::AppHandle;
|
||||||
|
use tauri::State;
|
||||||
use tauri_plugin_opener::OpenerExt;
|
use tauri_plugin_opener::OpenerExt;
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
@@ -300,3 +304,285 @@ fn scan_cli_version(tool: &str) -> (Option<String>, Option<String>) {
|
|||||||
|
|
||||||
(None, Some("未安装或无法执行".to_string()))
|
(None, Some("未安装或无法执行".to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 打开指定提供商的终端
|
||||||
|
///
|
||||||
|
/// 根据提供商配置的环境变量启动一个带有该提供商特定设置的终端
|
||||||
|
/// 无需检查是否为当前激活的提供商,任何提供商都可以打开终端
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn open_provider_terminal(
|
||||||
|
state: State<'_, crate::store::AppState>,
|
||||||
|
app: String,
|
||||||
|
#[allow(non_snake_case)] providerId: String,
|
||||||
|
) -> Result<bool, String> {
|
||||||
|
let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
// 获取提供商配置
|
||||||
|
let providers = ProviderService::list(state.inner(), app_type.clone())
|
||||||
|
.map_err(|e| format!("获取提供商列表失败: {e}"))?;
|
||||||
|
|
||||||
|
let provider = providers
|
||||||
|
.get(&providerId)
|
||||||
|
.ok_or_else(|| format!("提供商 {providerId} 不存在"))?;
|
||||||
|
|
||||||
|
// 从提供商配置中提取环境变量
|
||||||
|
let config = &provider.settings_config;
|
||||||
|
let env_vars = extract_env_vars_from_config(config, &app_type);
|
||||||
|
|
||||||
|
// 根据平台启动终端,传入提供商ID用于生成唯一的配置文件名
|
||||||
|
launch_terminal_with_env(env_vars, &providerId).map_err(|e| format!("启动终端失败: {e}"))?;
|
||||||
|
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 从提供商配置中提取环境变量
|
||||||
|
fn extract_env_vars_from_config(
|
||||||
|
config: &serde_json::Value,
|
||||||
|
app_type: &AppType,
|
||||||
|
) -> Vec<(String, String)> {
|
||||||
|
let mut env_vars = Vec::new();
|
||||||
|
|
||||||
|
let Some(obj) = config.as_object() else {
|
||||||
|
return env_vars;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理 env 字段(Claude/Gemini 通用)
|
||||||
|
if let Some(env) = obj.get("env").and_then(|v| v.as_object()) {
|
||||||
|
for (key, value) in env {
|
||||||
|
if let Some(str_val) = value.as_str() {
|
||||||
|
env_vars.push((key.clone(), str_val.to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理 base_url: 根据应用类型添加对应的环境变量
|
||||||
|
let base_url_key = match app_type {
|
||||||
|
AppType::Claude => Some("ANTHROPIC_BASE_URL"),
|
||||||
|
AppType::Gemini => Some("GOOGLE_GEMINI_BASE_URL"),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(key) = base_url_key {
|
||||||
|
if let Some(url_str) = env.get(key).and_then(|v| v.as_str()) {
|
||||||
|
env_vars.push((key.to_string(), url_str.to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Codex 使用 auth 字段转换为 OPENAI_API_KEY
|
||||||
|
if *app_type == AppType::Codex {
|
||||||
|
if let Some(auth) = obj.get("auth").and_then(|v| v.as_str()) {
|
||||||
|
env_vars.push(("OPENAI_API_KEY".to_string(), auth.to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gemini 使用 api_key 字段转换为 GEMINI_API_KEY
|
||||||
|
if *app_type == AppType::Gemini {
|
||||||
|
if let Some(api_key) = obj.get("api_key").and_then(|v| v.as_str()) {
|
||||||
|
env_vars.push(("GEMINI_API_KEY".to_string(), api_key.to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
env_vars
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 创建临时配置文件并启动 claude 终端
|
||||||
|
/// 使用 --settings 参数传入提供商特定的 API 配置
|
||||||
|
fn launch_terminal_with_env(
|
||||||
|
env_vars: Vec<(String, String)>,
|
||||||
|
provider_id: &str,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let temp_dir = std::env::temp_dir();
|
||||||
|
let config_file = temp_dir.join(format!(
|
||||||
|
"claude_{}_{}.json",
|
||||||
|
provider_id,
|
||||||
|
std::process::id()
|
||||||
|
));
|
||||||
|
|
||||||
|
// 创建并写入配置文件
|
||||||
|
write_claude_config(&config_file, &env_vars)?;
|
||||||
|
|
||||||
|
// 转义配置文件路径用于 shell
|
||||||
|
let config_path_escaped = escape_shell_path(&config_file);
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
launch_macos_terminal(&config_file, &config_path_escaped)?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
launch_linux_terminal(&config_file, &config_path_escaped)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
launch_windows_terminal(&temp_dir, &config_file)?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
|
||||||
|
Err("不支持的操作系统".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 写入 claude 配置文件
|
||||||
|
fn write_claude_config(
|
||||||
|
config_file: &std::path::Path,
|
||||||
|
env_vars: &[(String, String)],
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let mut config_obj = serde_json::Map::new();
|
||||||
|
let mut env_obj = serde_json::Map::new();
|
||||||
|
|
||||||
|
for (key, value) in env_vars {
|
||||||
|
env_obj.insert(key.clone(), serde_json::Value::String(value.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
config_obj.insert("env".to_string(), serde_json::Value::Object(env_obj));
|
||||||
|
|
||||||
|
let config_json =
|
||||||
|
serde_json::to_string_pretty(&config_obj).map_err(|e| format!("序列化配置失败: {e}"))?;
|
||||||
|
|
||||||
|
std::fs::write(config_file, config_json).map_err(|e| format!("写入配置文件失败: {e}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 转义 shell 路径
|
||||||
|
fn escape_shell_path(path: &std::path::Path) -> String {
|
||||||
|
path.to_string_lossy()
|
||||||
|
.replace('\\', "\\\\")
|
||||||
|
.replace('"', "\\\"")
|
||||||
|
.replace('$', "\\$")
|
||||||
|
.replace(' ', "\\ ")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 生成 bash 包装脚本,用于清理临时文件
|
||||||
|
fn generate_wrapper_script(config_path: &str, escaped_path: &str) -> String {
|
||||||
|
format!(
|
||||||
|
"bash -c 'trap \"rm -f \\\"{}\\\"\" EXIT; echo \"Using provider-specific claude config:\"; echo \"{}\"; claude --settings \"{}\"; exec bash --norc --noprofile'",
|
||||||
|
config_path, escaped_path, escaped_path
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// macOS: 使用 Terminal.app 启动
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
fn launch_macos_terminal(
|
||||||
|
config_file: &std::path::Path,
|
||||||
|
config_path_escaped: &str,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
let config_path_for_script = config_file.to_string_lossy().replace('\"', "\\\"");
|
||||||
|
|
||||||
|
let shell_script = generate_wrapper_script(&config_path_for_script, config_path_escaped);
|
||||||
|
|
||||||
|
let script = format!(
|
||||||
|
r#"tell application "Terminal"
|
||||||
|
activate
|
||||||
|
do script "{}"
|
||||||
|
end tell"#,
|
||||||
|
shell_script.replace('\"', "\\\"")
|
||||||
|
);
|
||||||
|
|
||||||
|
Command::new("osascript")
|
||||||
|
.arg("-e")
|
||||||
|
.arg(&script)
|
||||||
|
.spawn()
|
||||||
|
.map_err(|e| format!("启动 macOS 终端失败: {e}"))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Linux: 尝试使用常见终端启动
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fn launch_linux_terminal(
|
||||||
|
config_file: &std::path::Path,
|
||||||
|
config_path_escaped: &str,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
let terminals = [
|
||||||
|
"gnome-terminal",
|
||||||
|
"konsole",
|
||||||
|
"xfce4-terminal",
|
||||||
|
"mate-terminal",
|
||||||
|
"lxterminal",
|
||||||
|
"alacritty",
|
||||||
|
"kitty",
|
||||||
|
];
|
||||||
|
|
||||||
|
let config_path_for_bash = config_file.to_string_lossy();
|
||||||
|
let shell_cmd = generate_wrapper_script(&config_path_for_bash, config_path_escaped);
|
||||||
|
|
||||||
|
let mut last_error = String::from("未找到可用的终端");
|
||||||
|
|
||||||
|
for terminal in terminals {
|
||||||
|
// 检查终端是否存在
|
||||||
|
if std::path::Path::new(&format!("/usr/bin/{}", terminal)).exists()
|
||||||
|
|| std::path::Path::new(&format!("/bin/{}", terminal)).exists()
|
||||||
|
{
|
||||||
|
let result = match terminal {
|
||||||
|
"gnome-terminal" | "mate-terminal" => Command::new(terminal)
|
||||||
|
.arg("--")
|
||||||
|
.arg("bash")
|
||||||
|
.arg("-c")
|
||||||
|
.arg(&shell_cmd)
|
||||||
|
.spawn(),
|
||||||
|
_ => Command::new(terminal)
|
||||||
|
.arg("-e")
|
||||||
|
.arg("bash")
|
||||||
|
.arg("-c")
|
||||||
|
.arg(&shell_cmd)
|
||||||
|
.spawn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(_) => return Ok(()),
|
||||||
|
Err(e) => {
|
||||||
|
last_error = format!("启动 {} 失败: {}", terminal, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理配置文件
|
||||||
|
let _ = std::fs::remove_file(config_file);
|
||||||
|
Err(last_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Windows: 创建临时批处理文件启动
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
fn launch_windows_terminal(
|
||||||
|
temp_dir: &std::path::Path,
|
||||||
|
config_file: &std::path::Path,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
let bat_file = temp_dir.join(format!("cc_switch_claude_{}.bat", std::process::id()));
|
||||||
|
let config_path_for_batch = config_file.to_string_lossy().replace('&', "^&");
|
||||||
|
|
||||||
|
let content = format!(
|
||||||
|
"@echo off
|
||||||
|
echo Using provider-specific claude config:
|
||||||
|
echo {}
|
||||||
|
claude --settings \"{}\"
|
||||||
|
del \"{}\" >nul 2>&1
|
||||||
|
del \"%~f0\" >nul 2>&1
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo.
|
||||||
|
echo Press any key to close...
|
||||||
|
pause >nul
|
||||||
|
)",
|
||||||
|
config_path_for_batch, config_path_for_batch, config_path_for_batch
|
||||||
|
);
|
||||||
|
|
||||||
|
std::fs::write(&bat_file, content).map_err(|e| format!("写入批处理文件失败: {e}"))?;
|
||||||
|
|
||||||
|
Command::new("cmd")
|
||||||
|
.args(["/C", "start", "cmd", "/C", &bat_file.to_string_lossy()])
|
||||||
|
.creation_flags(CREATE_NO_WINDOW)
|
||||||
|
.spawn()
|
||||||
|
.map_err(|e| format!("启动 Windows 终端失败: {e}"))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ mod usage_script;
|
|||||||
|
|
||||||
pub use app_config::{AppType, McpApps, McpServer, MultiAppConfig};
|
pub use app_config::{AppType, McpApps, McpServer, MultiAppConfig};
|
||||||
pub use codex_config::{get_codex_auth_path, get_codex_config_path, write_codex_live_atomic};
|
pub use codex_config::{get_codex_auth_path, get_codex_config_path, write_codex_live_atomic};
|
||||||
|
pub use commands::open_provider_terminal;
|
||||||
pub use commands::*;
|
pub use commands::*;
|
||||||
pub use config::{get_claude_mcp_path, get_claude_settings_path, read_json_file};
|
pub use config::{get_claude_mcp_path, get_claude_settings_path, read_json_file};
|
||||||
pub use database::Database;
|
pub use database::Database;
|
||||||
@@ -832,6 +833,8 @@ pub fn run() {
|
|||||||
commands::get_stream_check_config,
|
commands::get_stream_check_config,
|
||||||
commands::save_stream_check_config,
|
commands::save_stream_check_config,
|
||||||
commands::get_tool_versions,
|
commands::get_tool_versions,
|
||||||
|
// Provider terminal
|
||||||
|
commands::open_provider_terminal,
|
||||||
// Universal Provider management
|
// Universal Provider management
|
||||||
commands::get_universal_providers,
|
commands::get_universal_providers,
|
||||||
commands::get_universal_provider,
|
commands::get_universal_provider,
|
||||||
|
|||||||
+21
@@ -382,6 +382,26 @@ function App() {
|
|||||||
await addProvider(duplicatedProvider);
|
await addProvider(duplicatedProvider);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 打开提供商终端
|
||||||
|
const handleOpenTerminal = async (provider: Provider) => {
|
||||||
|
try {
|
||||||
|
await providersApi.openTerminal(provider.id, activeApp);
|
||||||
|
toast.success(
|
||||||
|
t("provider.terminalOpened", {
|
||||||
|
defaultValue: "终端已打开",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[App] Failed to open terminal", error);
|
||||||
|
const errorMessage = extractErrorMessage(error);
|
||||||
|
toast.error(
|
||||||
|
t("provider.terminalOpenFailed", {
|
||||||
|
defaultValue: "打开终端失败",
|
||||||
|
}) + (errorMessage ? `: ${errorMessage}` : ""),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 导入配置成功后刷新
|
// 导入配置成功后刷新
|
||||||
const handleImportSuccess = async () => {
|
const handleImportSuccess = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -482,6 +502,7 @@ function App() {
|
|||||||
onDuplicate={handleDuplicateProvider}
|
onDuplicate={handleDuplicateProvider}
|
||||||
onConfigureUsage={setUsageProvider}
|
onConfigureUsage={setUsageProvider}
|
||||||
onOpenWebsite={handleOpenWebsite}
|
onOpenWebsite={handleOpenWebsite}
|
||||||
|
onOpenTerminal={activeApp === "claude" ? handleOpenTerminal : undefined}
|
||||||
onCreate={() => setIsAddOpen(true)}
|
onCreate={() => setIsAddOpen(true)}
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
Loader2,
|
Loader2,
|
||||||
Play,
|
Play,
|
||||||
Plus,
|
Plus,
|
||||||
|
Terminal,
|
||||||
TestTube2,
|
TestTube2,
|
||||||
Trash2,
|
Trash2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
@@ -23,6 +24,7 @@ interface ProviderActionsProps {
|
|||||||
onTest?: () => void;
|
onTest?: () => void;
|
||||||
onConfigureUsage: () => void;
|
onConfigureUsage: () => void;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
|
onOpenTerminal?: () => void;
|
||||||
// 故障转移相关
|
// 故障转移相关
|
||||||
isAutoFailoverEnabled?: boolean;
|
isAutoFailoverEnabled?: boolean;
|
||||||
isInFailoverQueue?: boolean;
|
isInFailoverQueue?: boolean;
|
||||||
@@ -39,6 +41,7 @@ export function ProviderActions({
|
|||||||
onTest,
|
onTest,
|
||||||
onConfigureUsage,
|
onConfigureUsage,
|
||||||
onDelete,
|
onDelete,
|
||||||
|
onOpenTerminal,
|
||||||
// 故障转移相关
|
// 故障转移相关
|
||||||
isAutoFailoverEnabled = false,
|
isAutoFailoverEnabled = false,
|
||||||
isInFailoverQueue = false,
|
isInFailoverQueue = false,
|
||||||
@@ -171,6 +174,21 @@ export function ProviderActions({
|
|||||||
<BarChart3 className="h-4 w-4" />
|
<BarChart3 className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
{onOpenTerminal && (
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={onOpenTerminal}
|
||||||
|
title={t("provider.openTerminal", "打开终端")}
|
||||||
|
className={cn(
|
||||||
|
iconButtonClass,
|
||||||
|
"hover:text-emerald-600 dark:hover:text-emerald-400",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Terminal className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ interface ProviderCardProps {
|
|||||||
onOpenWebsite: (url: string) => void;
|
onOpenWebsite: (url: string) => void;
|
||||||
onDuplicate: (provider: Provider) => void;
|
onDuplicate: (provider: Provider) => void;
|
||||||
onTest?: (provider: Provider) => void;
|
onTest?: (provider: Provider) => void;
|
||||||
|
onOpenTerminal?: (provider: Provider) => void;
|
||||||
isTesting?: boolean;
|
isTesting?: boolean;
|
||||||
isProxyRunning: boolean;
|
isProxyRunning: boolean;
|
||||||
isProxyTakeover?: boolean; // 代理接管模式(Live配置已被接管,切换为热切换)
|
isProxyTakeover?: boolean; // 代理接管模式(Live配置已被接管,切换为热切换)
|
||||||
@@ -91,6 +92,7 @@ export function ProviderCard({
|
|||||||
onOpenWebsite,
|
onOpenWebsite,
|
||||||
onDuplicate,
|
onDuplicate,
|
||||||
onTest,
|
onTest,
|
||||||
|
onOpenTerminal,
|
||||||
isTesting,
|
isTesting,
|
||||||
isProxyRunning,
|
isProxyRunning,
|
||||||
isProxyTakeover = false,
|
isProxyTakeover = false,
|
||||||
@@ -339,6 +341,7 @@ export function ProviderCard({
|
|||||||
onTest={onTest ? () => onTest(provider) : undefined}
|
onTest={onTest ? () => onTest(provider) : undefined}
|
||||||
onConfigureUsage={() => onConfigureUsage(provider)}
|
onConfigureUsage={() => onConfigureUsage(provider)}
|
||||||
onDelete={() => onDelete(provider)}
|
onDelete={() => onDelete(provider)}
|
||||||
|
onOpenTerminal={onOpenTerminal ? () => onOpenTerminal(provider) : undefined}
|
||||||
// 故障转移相关
|
// 故障转移相关
|
||||||
isAutoFailoverEnabled={isAutoFailoverEnabled}
|
isAutoFailoverEnabled={isAutoFailoverEnabled}
|
||||||
isInFailoverQueue={isInFailoverQueue}
|
isInFailoverQueue={isInFailoverQueue}
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ interface ProviderListProps {
|
|||||||
onDuplicate: (provider: Provider) => void;
|
onDuplicate: (provider: Provider) => void;
|
||||||
onConfigureUsage?: (provider: Provider) => void;
|
onConfigureUsage?: (provider: Provider) => void;
|
||||||
onOpenWebsite: (url: string) => void;
|
onOpenWebsite: (url: string) => void;
|
||||||
|
onOpenTerminal?: (provider: Provider) => void;
|
||||||
onCreate?: () => void;
|
onCreate?: () => void;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
isProxyRunning?: boolean; // 代理服务运行状态
|
isProxyRunning?: boolean; // 代理服务运行状态
|
||||||
@@ -58,6 +59,7 @@ export function ProviderList({
|
|||||||
onDuplicate,
|
onDuplicate,
|
||||||
onConfigureUsage,
|
onConfigureUsage,
|
||||||
onOpenWebsite,
|
onOpenWebsite,
|
||||||
|
onOpenTerminal,
|
||||||
onCreate,
|
onCreate,
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
isProxyRunning = false,
|
isProxyRunning = false,
|
||||||
@@ -203,6 +205,7 @@ export function ProviderList({
|
|||||||
onDuplicate={onDuplicate}
|
onDuplicate={onDuplicate}
|
||||||
onConfigureUsage={onConfigureUsage}
|
onConfigureUsage={onConfigureUsage}
|
||||||
onOpenWebsite={onOpenWebsite}
|
onOpenWebsite={onOpenWebsite}
|
||||||
|
onOpenTerminal={onOpenTerminal}
|
||||||
onTest={handleTest}
|
onTest={handleTest}
|
||||||
isTesting={isChecking(provider.id)}
|
isTesting={isChecking(provider.id)}
|
||||||
isProxyRunning={isProxyRunning}
|
isProxyRunning={isProxyRunning}
|
||||||
@@ -311,6 +314,7 @@ interface SortableProviderCardProps {
|
|||||||
onDuplicate: (provider: Provider) => void;
|
onDuplicate: (provider: Provider) => void;
|
||||||
onConfigureUsage?: (provider: Provider) => void;
|
onConfigureUsage?: (provider: Provider) => void;
|
||||||
onOpenWebsite: (url: string) => void;
|
onOpenWebsite: (url: string) => void;
|
||||||
|
onOpenTerminal?: (provider: Provider) => void;
|
||||||
onTest: (provider: Provider) => void;
|
onTest: (provider: Provider) => void;
|
||||||
isTesting: boolean;
|
isTesting: boolean;
|
||||||
isProxyRunning: boolean;
|
isProxyRunning: boolean;
|
||||||
@@ -333,6 +337,7 @@ function SortableProviderCard({
|
|||||||
onDuplicate,
|
onDuplicate,
|
||||||
onConfigureUsage,
|
onConfigureUsage,
|
||||||
onOpenWebsite,
|
onOpenWebsite,
|
||||||
|
onOpenTerminal,
|
||||||
onTest,
|
onTest,
|
||||||
isTesting,
|
isTesting,
|
||||||
isProxyRunning,
|
isProxyRunning,
|
||||||
@@ -371,6 +376,7 @@ function SortableProviderCard({
|
|||||||
onConfigureUsage ? (item) => onConfigureUsage(item) : () => undefined
|
onConfigureUsage ? (item) => onConfigureUsage(item) : () => undefined
|
||||||
}
|
}
|
||||||
onOpenWebsite={onOpenWebsite}
|
onOpenWebsite={onOpenWebsite}
|
||||||
|
onOpenTerminal={onOpenTerminal}
|
||||||
onTest={onTest}
|
onTest={onTest}
|
||||||
isTesting={isTesting}
|
isTesting={isTesting}
|
||||||
isProxyRunning={isProxyRunning}
|
isProxyRunning={isProxyRunning}
|
||||||
|
|||||||
@@ -65,6 +65,15 @@ export const providersApi = {
|
|||||||
handler(payload);
|
handler(payload);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开指定提供商的终端
|
||||||
|
* 任何提供商都可以打开终端,不受是否为当前激活提供商的限制
|
||||||
|
* 终端会使用该提供商特定的 API 配置,不影响全局设置
|
||||||
|
*/
|
||||||
|
async openTerminal(providerId: string, appId: AppId): Promise<boolean> {
|
||||||
|
return await invoke("open_provider_terminal", { providerId, app: appId });
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user