mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-03-20 13:57:06 +08:00
feat(settings): add OpenCode support to environment check and one-click install
- Add OpenCode version detection with Go path scanning - Add GitHub Releases API for fetching latest OpenCode version - Add OpenCode install command to one-click install section - Update i18n hints to include OpenCode across all locales - Fix SettingsPage indentation formatting
This commit is contained in:
@@ -89,7 +89,7 @@ pub struct ToolVersion {
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_tool_versions() -> Result<Vec<ToolVersion>, String> {
|
||||
let tools = vec!["claude", "codex", "gemini"];
|
||||
let tools = vec!["claude", "codex", "gemini", "opencode"];
|
||||
let mut results = Vec::new();
|
||||
|
||||
// 使用全局 HTTP 客户端(已包含代理配置)
|
||||
@@ -116,6 +116,7 @@ pub async fn get_tool_versions() -> Result<Vec<ToolVersion>, String> {
|
||||
"claude" => fetch_npm_latest_version(&client, "@anthropic-ai/claude-code").await,
|
||||
"codex" => fetch_npm_latest_version(&client, "@openai/codex").await,
|
||||
"gemini" => fetch_npm_latest_version(&client, "@google/gemini-cli").await,
|
||||
"opencode" => fetch_github_latest_version(&client, "anomalyco/opencode").await,
|
||||
_ => None,
|
||||
};
|
||||
|
||||
@@ -148,6 +149,29 @@ async fn fetch_npm_latest_version(client: &reqwest::Client, package: &str) -> Op
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function to fetch latest version from GitHub releases
|
||||
async fn fetch_github_latest_version(client: &reqwest::Client, repo: &str) -> Option<String> {
|
||||
let url = format!("https://api.github.com/repos/{repo}/releases/latest");
|
||||
match client
|
||||
.get(&url)
|
||||
.header("User-Agent", "cc-switch")
|
||||
.header("Accept", "application/vnd.github+json")
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(resp) => {
|
||||
if let Ok(json) = resp.json::<serde_json::Value>().await {
|
||||
json.get("tag_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.strip_prefix('v').unwrap_or(s).to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 预编译的版本号正则表达式
|
||||
static VERSION_RE: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"\d+\.\d+\.\d+(-[\w.]+)?").expect("Invalid version regex"));
|
||||
@@ -224,7 +248,7 @@ fn try_get_version_wsl(tool: &str, distro: &str) -> (Option<String>, Option<Stri
|
||||
|
||||
// 防御性断言:tool 只能是预定义的值
|
||||
debug_assert!(
|
||||
["claude", "codex", "gemini"].contains(&tool),
|
||||
["claude", "codex", "gemini", "opencode"].contains(&tool),
|
||||
"unexpected tool name: {tool}"
|
||||
);
|
||||
|
||||
@@ -348,6 +372,14 @@ fn scan_cli_version(tool: &str) -> (Option<String>, Option<String>) {
|
||||
}
|
||||
}
|
||||
|
||||
// 添加 Go 路径支持 (opencode 使用 go install 安装)
|
||||
if tool == "opencode" {
|
||||
search_paths.push(home.join("go/bin")); // go install 默认路径
|
||||
if let Ok(gopath) = std::env::var("GOPATH") {
|
||||
search_paths.push(std::path::PathBuf::from(gopath).join("bin"));
|
||||
}
|
||||
}
|
||||
|
||||
// 在每个路径中查找工具
|
||||
for path in &search_paths {
|
||||
let tool_path = if cfg!(target_os = "windows") {
|
||||
@@ -405,6 +437,7 @@ fn wsl_distro_for_tool(tool: &str) -> Option<String> {
|
||||
"claude" => crate::settings::get_claude_override_dir(),
|
||||
"codex" => crate::settings::get_codex_override_dir(),
|
||||
"gemini" => crate::settings::get_gemini_override_dir(),
|
||||
"opencode" => crate::settings::get_opencode_override_dir(),
|
||||
_ => None,
|
||||
}?;
|
||||
|
||||
|
||||
@@ -37,7 +37,9 @@ curl -fsSL https://claude.ai/install.sh | bash
|
||||
# Codex
|
||||
npm i -g @openai/codex@latest
|
||||
# Gemini CLI
|
||||
npm i -g @google/gemini-cli@latest`;
|
||||
npm i -g @google/gemini-cli@latest
|
||||
# OpenCode
|
||||
curl -fsSL https://opencode.ai/install | bash`;
|
||||
|
||||
export function AboutSection({ isPortable }: AboutSectionProps) {
|
||||
// ... (use hooks as before) ...
|
||||
@@ -315,10 +317,14 @@ export function AboutSection({ isPortable }: AboutSectionProps) {
|
||||
{isLoadingTools ? t("common.refreshing") : t("common.refresh")}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="grid gap-3 sm:grid-cols-3 px-1">
|
||||
{["claude", "codex", "gemini"].map((toolName, index) => {
|
||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4 px-1">
|
||||
{["claude", "codex", "gemini", "opencode"].map((toolName, index) => {
|
||||
const tool = toolVersions.find((item) => item.name === toolName);
|
||||
const displayName = tool?.name ?? toolName;
|
||||
// Special case for OpenCode (capital C), others use capitalize
|
||||
const displayName =
|
||||
toolName === "opencode"
|
||||
? "OpenCode"
|
||||
: toolName.charAt(0).toUpperCase() + toolName.slice(1);
|
||||
const title = tool?.version || tool?.error || t("common.unknown");
|
||||
|
||||
return (
|
||||
@@ -333,9 +339,7 @@ export function AboutSection({ isPortable }: AboutSectionProps) {
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Terminal className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium capitalize">
|
||||
{displayName}
|
||||
</span>
|
||||
<span className="text-sm font-medium">{displayName}</span>
|
||||
</div>
|
||||
{isLoadingTools ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
|
||||
|
||||
@@ -232,405 +232,405 @@ export function SettingsPage({
|
||||
<div className="flex-1 min-h-0 flex flex-col">
|
||||
<div className="flex-1 overflow-y-auto overflow-x-hidden pr-2">
|
||||
<TabsContent value="general" className="space-y-6 mt-0">
|
||||
{settings ? (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="space-y-6"
|
||||
>
|
||||
<LanguageSettings
|
||||
value={settings.language}
|
||||
onChange={(lang) => handleAutoSave({ language: lang })}
|
||||
/>
|
||||
<ThemeSettings />
|
||||
<AppVisibilitySettings
|
||||
settings={settings}
|
||||
onChange={handleAutoSave}
|
||||
/>
|
||||
<WindowSettings
|
||||
settings={settings}
|
||||
onChange={handleAutoSave}
|
||||
/>
|
||||
<SkillSyncMethodSettings
|
||||
value={settings.skillSyncMethod ?? "auto"}
|
||||
onChange={(method) =>
|
||||
handleAutoSave({ skillSyncMethod: method })
|
||||
}
|
||||
/>
|
||||
<TerminalSettings
|
||||
value={settings.preferredTerminal}
|
||||
onChange={(terminal) =>
|
||||
handleAutoSave({ preferredTerminal: terminal })
|
||||
}
|
||||
/>
|
||||
</motion.div>
|
||||
) : null}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="advanced" className="space-y-6 mt-0 pb-4">
|
||||
{settings ? (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="space-y-4"
|
||||
>
|
||||
<Accordion
|
||||
type="multiple"
|
||||
defaultValue={[]}
|
||||
className="w-full space-y-4"
|
||||
{settings ? (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="space-y-6"
|
||||
>
|
||||
<AccordionItem
|
||||
value="directory"
|
||||
className="rounded-xl glass-card overflow-hidden"
|
||||
>
|
||||
<AccordionTrigger className="px-6 py-4 hover:no-underline hover:bg-muted/50 data-[state=open]:bg-muted/50">
|
||||
<div className="flex items-center gap-3">
|
||||
<FolderSearch className="h-5 w-5 text-primary" />
|
||||
<div className="text-left">
|
||||
<h3 className="text-base font-semibold">
|
||||
{t("settings.advanced.configDir.title")}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground font-normal">
|
||||
{t("settings.advanced.configDir.description")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-6 pb-6 pt-4 border-t border-border/50">
|
||||
<DirectorySettings
|
||||
appConfigDir={appConfigDir}
|
||||
resolvedDirs={resolvedDirs}
|
||||
onAppConfigChange={updateAppConfigDir}
|
||||
onBrowseAppConfig={browseAppConfigDir}
|
||||
onResetAppConfig={resetAppConfigDir}
|
||||
claudeDir={settings.claudeConfigDir}
|
||||
codexDir={settings.codexConfigDir}
|
||||
geminiDir={settings.geminiConfigDir}
|
||||
opencodeDir={settings.opencodeConfigDir}
|
||||
onDirectoryChange={updateDirectory}
|
||||
onBrowseDirectory={browseDirectory}
|
||||
onResetDirectory={resetDirectory}
|
||||
/>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<LanguageSettings
|
||||
value={settings.language}
|
||||
onChange={(lang) => handleAutoSave({ language: lang })}
|
||||
/>
|
||||
<ThemeSettings />
|
||||
<AppVisibilitySettings
|
||||
settings={settings}
|
||||
onChange={handleAutoSave}
|
||||
/>
|
||||
<WindowSettings
|
||||
settings={settings}
|
||||
onChange={handleAutoSave}
|
||||
/>
|
||||
<SkillSyncMethodSettings
|
||||
value={settings.skillSyncMethod ?? "auto"}
|
||||
onChange={(method) =>
|
||||
handleAutoSave({ skillSyncMethod: method })
|
||||
}
|
||||
/>
|
||||
<TerminalSettings
|
||||
value={settings.preferredTerminal}
|
||||
onChange={(terminal) =>
|
||||
handleAutoSave({ preferredTerminal: terminal })
|
||||
}
|
||||
/>
|
||||
</motion.div>
|
||||
) : null}
|
||||
</TabsContent>
|
||||
|
||||
<AccordionItem
|
||||
value="proxy"
|
||||
className="rounded-xl glass-card overflow-hidden [&[data-state=open]>.accordion-header]:bg-muted/50"
|
||||
<TabsContent value="advanced" className="space-y-6 mt-0 pb-4">
|
||||
{settings ? (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="space-y-4"
|
||||
>
|
||||
<Accordion
|
||||
type="multiple"
|
||||
defaultValue={[]}
|
||||
className="w-full space-y-4"
|
||||
>
|
||||
<AccordionPrimitive.Header className="accordion-header flex items-center justify-between px-6 py-4 hover:bg-muted/50">
|
||||
<AccordionPrimitive.Trigger className="flex flex-1 items-center justify-between hover:no-underline [&[data-state=open]>svg]:rotate-180">
|
||||
<AccordionItem
|
||||
value="directory"
|
||||
className="rounded-xl glass-card overflow-hidden"
|
||||
>
|
||||
<AccordionTrigger className="px-6 py-4 hover:no-underline hover:bg-muted/50 data-[state=open]:bg-muted/50">
|
||||
<div className="flex items-center gap-3">
|
||||
<Server className="h-5 w-5 text-green-500" />
|
||||
<FolderSearch className="h-5 w-5 text-primary" />
|
||||
<div className="text-left">
|
||||
<h3 className="text-base font-semibold">
|
||||
{t("settings.advanced.proxy.title")}
|
||||
{t("settings.advanced.configDir.title")}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground font-normal">
|
||||
{t("settings.advanced.proxy.description")}
|
||||
{t("settings.advanced.configDir.description")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
|
||||
<div className="flex items-center gap-4 pl-4">
|
||||
<Badge
|
||||
variant={isRunning ? "default" : "secondary"}
|
||||
className="gap-1.5 h-6"
|
||||
>
|
||||
<Activity
|
||||
className={`h-3 w-3 ${isRunning ? "animate-pulse" : ""}`}
|
||||
/>
|
||||
{isRunning
|
||||
? t("settings.advanced.proxy.running")
|
||||
: t("settings.advanced.proxy.stopped")}
|
||||
</Badge>
|
||||
<Switch
|
||||
checked={isRunning}
|
||||
onCheckedChange={handleToggleProxy}
|
||||
disabled={isProxyPending}
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-6 pb-6 pt-4 border-t border-border/50">
|
||||
<DirectorySettings
|
||||
appConfigDir={appConfigDir}
|
||||
resolvedDirs={resolvedDirs}
|
||||
onAppConfigChange={updateAppConfigDir}
|
||||
onBrowseAppConfig={browseAppConfigDir}
|
||||
onResetAppConfig={resetAppConfigDir}
|
||||
claudeDir={settings.claudeConfigDir}
|
||||
codexDir={settings.codexConfigDir}
|
||||
geminiDir={settings.geminiConfigDir}
|
||||
opencodeDir={settings.opencodeConfigDir}
|
||||
onDirectoryChange={updateDirectory}
|
||||
onBrowseDirectory={browseDirectory}
|
||||
onResetDirectory={resetDirectory}
|
||||
/>
|
||||
</div>
|
||||
</AccordionPrimitive.Header>
|
||||
<AccordionContent className="px-6 pb-6 pt-0 border-t border-border/50">
|
||||
<ProxyPanel />
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem
|
||||
value="failover"
|
||||
className="rounded-xl glass-card overflow-hidden"
|
||||
>
|
||||
<AccordionTrigger className="px-6 py-4 hover:no-underline hover:bg-muted/50 data-[state=open]:bg-muted/50">
|
||||
<div className="flex items-center gap-3">
|
||||
<Activity className="h-5 w-5 text-orange-500" />
|
||||
<div className="text-left">
|
||||
<h3 className="text-base font-semibold">
|
||||
{t("settings.advanced.failover.title")}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground font-normal">
|
||||
{t("settings.advanced.failover.description")}
|
||||
</p>
|
||||
<AccordionItem
|
||||
value="proxy"
|
||||
className="rounded-xl glass-card overflow-hidden [&[data-state=open]>.accordion-header]:bg-muted/50"
|
||||
>
|
||||
<AccordionPrimitive.Header className="accordion-header flex items-center justify-between px-6 py-4 hover:bg-muted/50">
|
||||
<AccordionPrimitive.Trigger className="flex flex-1 items-center justify-between hover:no-underline [&[data-state=open]>svg]:rotate-180">
|
||||
<div className="flex items-center gap-3">
|
||||
<Server className="h-5 w-5 text-green-500" />
|
||||
<div className="text-left">
|
||||
<h3 className="text-base font-semibold">
|
||||
{t("settings.advanced.proxy.title")}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground font-normal">
|
||||
{t("settings.advanced.proxy.description")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
|
||||
<div className="flex items-center gap-4 pl-4">
|
||||
<Badge
|
||||
variant={isRunning ? "default" : "secondary"}
|
||||
className="gap-1.5 h-6"
|
||||
>
|
||||
<Activity
|
||||
className={`h-3 w-3 ${isRunning ? "animate-pulse" : ""}`}
|
||||
/>
|
||||
{isRunning
|
||||
? t("settings.advanced.proxy.running")
|
||||
: t("settings.advanced.proxy.stopped")}
|
||||
</Badge>
|
||||
<Switch
|
||||
checked={isRunning}
|
||||
onCheckedChange={handleToggleProxy}
|
||||
disabled={isProxyPending}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-6 pb-6 pt-4 border-t border-border/50">
|
||||
<div className="space-y-6">
|
||||
{/* 代理未运行时的提示 */}
|
||||
{!isRunning && (
|
||||
<div className="p-4 rounded-lg bg-yellow-500/10 border border-yellow-500/20">
|
||||
<p className="text-sm text-yellow-600 dark:text-yellow-400">
|
||||
{t("proxy.failover.proxyRequired", {
|
||||
defaultValue:
|
||||
"需要先启动代理服务才能配置故障转移",
|
||||
})}
|
||||
</AccordionPrimitive.Header>
|
||||
<AccordionContent className="px-6 pb-6 pt-0 border-t border-border/50">
|
||||
<ProxyPanel />
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem
|
||||
value="failover"
|
||||
className="rounded-xl glass-card overflow-hidden"
|
||||
>
|
||||
<AccordionTrigger className="px-6 py-4 hover:no-underline hover:bg-muted/50 data-[state=open]:bg-muted/50">
|
||||
<div className="flex items-center gap-3">
|
||||
<Activity className="h-5 w-5 text-orange-500" />
|
||||
<div className="text-left">
|
||||
<h3 className="text-base font-semibold">
|
||||
{t("settings.advanced.failover.title")}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground font-normal">
|
||||
{t("settings.advanced.failover.description")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-6 pb-6 pt-4 border-t border-border/50">
|
||||
<div className="space-y-6">
|
||||
{/* 代理未运行时的提示 */}
|
||||
{!isRunning && (
|
||||
<div className="p-4 rounded-lg bg-yellow-500/10 border border-yellow-500/20">
|
||||
<p className="text-sm text-yellow-600 dark:text-yellow-400">
|
||||
{t("proxy.failover.proxyRequired", {
|
||||
defaultValue:
|
||||
"需要先启动代理服务才能配置故障转移",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 故障转移设置 - 按应用分组 */}
|
||||
<Tabs defaultValue="claude" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="claude">Claude</TabsTrigger>
|
||||
<TabsTrigger value="codex">Codex</TabsTrigger>
|
||||
<TabsTrigger value="gemini">Gemini</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent
|
||||
value="claude"
|
||||
className="mt-4 space-y-6"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold">
|
||||
{t("proxy.failoverQueue.title")}
|
||||
</h4>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("proxy.failoverQueue.description")}
|
||||
</p>
|
||||
{/* 故障转移设置 - 按应用分组 */}
|
||||
<Tabs defaultValue="claude" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="claude">Claude</TabsTrigger>
|
||||
<TabsTrigger value="codex">Codex</TabsTrigger>
|
||||
<TabsTrigger value="gemini">Gemini</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent
|
||||
value="claude"
|
||||
className="mt-4 space-y-6"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold">
|
||||
{t("proxy.failoverQueue.title")}
|
||||
</h4>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("proxy.failoverQueue.description")}
|
||||
</p>
|
||||
</div>
|
||||
<FailoverQueueManager
|
||||
appType="claude"
|
||||
disabled={!isRunning}
|
||||
/>
|
||||
</div>
|
||||
<FailoverQueueManager
|
||||
appType="claude"
|
||||
disabled={!isRunning}
|
||||
/>
|
||||
</div>
|
||||
<div className="border-t border-border/50 pt-6">
|
||||
<AutoFailoverConfigPanel
|
||||
appType="claude"
|
||||
disabled={!isRunning}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent
|
||||
value="codex"
|
||||
className="mt-4 space-y-6"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold">
|
||||
{t("proxy.failoverQueue.title")}
|
||||
</h4>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("proxy.failoverQueue.description")}
|
||||
</p>
|
||||
<div className="border-t border-border/50 pt-6">
|
||||
<AutoFailoverConfigPanel
|
||||
appType="claude"
|
||||
disabled={!isRunning}
|
||||
/>
|
||||
</div>
|
||||
<FailoverQueueManager
|
||||
appType="codex"
|
||||
disabled={!isRunning}
|
||||
/>
|
||||
</div>
|
||||
<div className="border-t border-border/50 pt-6">
|
||||
<AutoFailoverConfigPanel
|
||||
appType="codex"
|
||||
disabled={!isRunning}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent
|
||||
value="gemini"
|
||||
className="mt-4 space-y-6"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold">
|
||||
{t("proxy.failoverQueue.title")}
|
||||
</h4>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("proxy.failoverQueue.description")}
|
||||
</p>
|
||||
</TabsContent>
|
||||
<TabsContent
|
||||
value="codex"
|
||||
className="mt-4 space-y-6"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold">
|
||||
{t("proxy.failoverQueue.title")}
|
||||
</h4>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("proxy.failoverQueue.description")}
|
||||
</p>
|
||||
</div>
|
||||
<FailoverQueueManager
|
||||
appType="codex"
|
||||
disabled={!isRunning}
|
||||
/>
|
||||
</div>
|
||||
<FailoverQueueManager
|
||||
appType="gemini"
|
||||
disabled={!isRunning}
|
||||
/>
|
||||
</div>
|
||||
<div className="border-t border-border/50 pt-6">
|
||||
<AutoFailoverConfigPanel
|
||||
appType="gemini"
|
||||
disabled={!isRunning}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem
|
||||
value="rectifier"
|
||||
className="rounded-xl glass-card overflow-hidden"
|
||||
>
|
||||
<AccordionTrigger className="px-6 py-4 hover:no-underline hover:bg-muted/50 data-[state=open]:bg-muted/50">
|
||||
<div className="flex items-center gap-3">
|
||||
<Zap className="h-5 w-5 text-purple-500" />
|
||||
<div className="text-left">
|
||||
<h3 className="text-base font-semibold">
|
||||
{t("settings.advanced.rectifier.title")}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground font-normal">
|
||||
{t("settings.advanced.rectifier.description")}
|
||||
</p>
|
||||
<div className="border-t border-border/50 pt-6">
|
||||
<AutoFailoverConfigPanel
|
||||
appType="codex"
|
||||
disabled={!isRunning}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent
|
||||
value="gemini"
|
||||
className="mt-4 space-y-6"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold">
|
||||
{t("proxy.failoverQueue.title")}
|
||||
</h4>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("proxy.failoverQueue.description")}
|
||||
</p>
|
||||
</div>
|
||||
<FailoverQueueManager
|
||||
appType="gemini"
|
||||
disabled={!isRunning}
|
||||
/>
|
||||
</div>
|
||||
<div className="border-t border-border/50 pt-6">
|
||||
<AutoFailoverConfigPanel
|
||||
appType="gemini"
|
||||
disabled={!isRunning}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-6 pb-6 pt-4 border-t border-border/50">
|
||||
<RectifierConfigPanel />
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem
|
||||
value="test"
|
||||
className="rounded-xl glass-card overflow-hidden"
|
||||
>
|
||||
<AccordionTrigger className="px-6 py-4 hover:no-underline hover:bg-muted/50 data-[state=open]:bg-muted/50">
|
||||
<div className="flex items-center gap-3">
|
||||
<Activity className="h-5 w-5 text-indigo-500" />
|
||||
<div className="text-left">
|
||||
<h3 className="text-base font-semibold">
|
||||
{t("settings.advanced.modelTest.title")}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground font-normal">
|
||||
{t("settings.advanced.modelTest.description")}
|
||||
</p>
|
||||
<AccordionItem
|
||||
value="rectifier"
|
||||
className="rounded-xl glass-card overflow-hidden"
|
||||
>
|
||||
<AccordionTrigger className="px-6 py-4 hover:no-underline hover:bg-muted/50 data-[state=open]:bg-muted/50">
|
||||
<div className="flex items-center gap-3">
|
||||
<Zap className="h-5 w-5 text-purple-500" />
|
||||
<div className="text-left">
|
||||
<h3 className="text-base font-semibold">
|
||||
{t("settings.advanced.rectifier.title")}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground font-normal">
|
||||
{t("settings.advanced.rectifier.description")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-6 pb-6 pt-4 border-t border-border/50">
|
||||
<ModelTestConfigPanel />
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-6 pb-6 pt-4 border-t border-border/50">
|
||||
<RectifierConfigPanel />
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem
|
||||
value="pricing"
|
||||
className="rounded-xl glass-card overflow-hidden"
|
||||
>
|
||||
<AccordionTrigger className="px-6 py-4 hover:no-underline hover:bg-muted/50 data-[state=open]:bg-muted/50">
|
||||
<div className="flex items-center gap-3">
|
||||
<Coins className="h-5 w-5 text-yellow-500" />
|
||||
<div className="text-left">
|
||||
<h3 className="text-base font-semibold">
|
||||
{t("settings.advanced.pricing.title")}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground font-normal">
|
||||
{t("settings.advanced.pricing.description")}
|
||||
</p>
|
||||
<AccordionItem
|
||||
value="test"
|
||||
className="rounded-xl glass-card overflow-hidden"
|
||||
>
|
||||
<AccordionTrigger className="px-6 py-4 hover:no-underline hover:bg-muted/50 data-[state=open]:bg-muted/50">
|
||||
<div className="flex items-center gap-3">
|
||||
<Activity className="h-5 w-5 text-indigo-500" />
|
||||
<div className="text-left">
|
||||
<h3 className="text-base font-semibold">
|
||||
{t("settings.advanced.modelTest.title")}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground font-normal">
|
||||
{t("settings.advanced.modelTest.description")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-6 pb-6 pt-4 border-t border-border/50">
|
||||
<PricingConfigPanel />
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-6 pb-6 pt-4 border-t border-border/50">
|
||||
<ModelTestConfigPanel />
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem
|
||||
value="globalProxy"
|
||||
className="rounded-xl glass-card overflow-hidden"
|
||||
>
|
||||
<AccordionTrigger className="px-6 py-4 hover:no-underline hover:bg-muted/50 data-[state=open]:bg-muted/50">
|
||||
<div className="flex items-center gap-3">
|
||||
<Globe className="h-5 w-5 text-cyan-500" />
|
||||
<div className="text-left">
|
||||
<h3 className="text-base font-semibold">
|
||||
{t("settings.advanced.globalProxy.title")}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground font-normal">
|
||||
{t("settings.advanced.globalProxy.description")}
|
||||
</p>
|
||||
<AccordionItem
|
||||
value="pricing"
|
||||
className="rounded-xl glass-card overflow-hidden"
|
||||
>
|
||||
<AccordionTrigger className="px-6 py-4 hover:no-underline hover:bg-muted/50 data-[state=open]:bg-muted/50">
|
||||
<div className="flex items-center gap-3">
|
||||
<Coins className="h-5 w-5 text-yellow-500" />
|
||||
<div className="text-left">
|
||||
<h3 className="text-base font-semibold">
|
||||
{t("settings.advanced.pricing.title")}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground font-normal">
|
||||
{t("settings.advanced.pricing.description")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-6 pb-6 pt-4 border-t border-border/50">
|
||||
<GlobalProxySettings />
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-6 pb-6 pt-4 border-t border-border/50">
|
||||
<PricingConfigPanel />
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem
|
||||
value="data"
|
||||
className="rounded-xl glass-card overflow-hidden"
|
||||
>
|
||||
<AccordionTrigger className="px-6 py-4 hover:no-underline hover:bg-muted/50 data-[state=open]:bg-muted/50">
|
||||
<div className="flex items-center gap-3">
|
||||
<Database className="h-5 w-5 text-blue-500" />
|
||||
<div className="text-left">
|
||||
<h3 className="text-base font-semibold">
|
||||
{t("settings.advanced.data.title")}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground font-normal">
|
||||
{t("settings.advanced.data.description")}
|
||||
</p>
|
||||
<AccordionItem
|
||||
value="globalProxy"
|
||||
className="rounded-xl glass-card overflow-hidden"
|
||||
>
|
||||
<AccordionTrigger className="px-6 py-4 hover:no-underline hover:bg-muted/50 data-[state=open]:bg-muted/50">
|
||||
<div className="flex items-center gap-3">
|
||||
<Globe className="h-5 w-5 text-cyan-500" />
|
||||
<div className="text-left">
|
||||
<h3 className="text-base font-semibold">
|
||||
{t("settings.advanced.globalProxy.title")}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground font-normal">
|
||||
{t("settings.advanced.globalProxy.description")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-6 pb-6 pt-4 border-t border-border/50">
|
||||
<ImportExportSection
|
||||
status={importStatus}
|
||||
selectedFile={selectedFile}
|
||||
errorMessage={errorMessage}
|
||||
backupId={backupId}
|
||||
isImporting={isImporting}
|
||||
onSelectFile={selectImportFile}
|
||||
onImport={importConfig}
|
||||
onExport={exportConfig}
|
||||
onClear={clearSelection}
|
||||
/>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-6 pb-6 pt-4 border-t border-border/50">
|
||||
<GlobalProxySettings />
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem
|
||||
value="logConfig"
|
||||
className="rounded-xl glass-card overflow-hidden"
|
||||
>
|
||||
<AccordionTrigger className="px-6 py-4 hover:no-underline hover:bg-muted/50 data-[state=open]:bg-muted/50">
|
||||
<div className="flex items-center gap-3">
|
||||
<ScrollText className="h-5 w-5 text-cyan-500" />
|
||||
<div className="text-left">
|
||||
<h3 className="text-base font-semibold">
|
||||
{t("settings.advanced.logConfig.title")}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground font-normal">
|
||||
{t("settings.advanced.logConfig.description")}
|
||||
</p>
|
||||
<AccordionItem
|
||||
value="data"
|
||||
className="rounded-xl glass-card overflow-hidden"
|
||||
>
|
||||
<AccordionTrigger className="px-6 py-4 hover:no-underline hover:bg-muted/50 data-[state=open]:bg-muted/50">
|
||||
<div className="flex items-center gap-3">
|
||||
<Database className="h-5 w-5 text-blue-500" />
|
||||
<div className="text-left">
|
||||
<h3 className="text-base font-semibold">
|
||||
{t("settings.advanced.data.title")}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground font-normal">
|
||||
{t("settings.advanced.data.description")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-6 pb-6 pt-4 border-t border-border/50">
|
||||
<LogConfigPanel />
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</motion.div>
|
||||
) : null}
|
||||
</TabsContent>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-6 pb-6 pt-4 border-t border-border/50">
|
||||
<ImportExportSection
|
||||
status={importStatus}
|
||||
selectedFile={selectedFile}
|
||||
errorMessage={errorMessage}
|
||||
backupId={backupId}
|
||||
isImporting={isImporting}
|
||||
onSelectFile={selectImportFile}
|
||||
onImport={importConfig}
|
||||
onExport={exportConfig}
|
||||
onClear={clearSelection}
|
||||
/>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<TabsContent value="about" className="mt-0">
|
||||
<AboutSection isPortable={isPortable} />
|
||||
</TabsContent>
|
||||
<AccordionItem
|
||||
value="logConfig"
|
||||
className="rounded-xl glass-card overflow-hidden"
|
||||
>
|
||||
<AccordionTrigger className="px-6 py-4 hover:no-underline hover:bg-muted/50 data-[state=open]:bg-muted/50">
|
||||
<div className="flex items-center gap-3">
|
||||
<ScrollText className="h-5 w-5 text-cyan-500" />
|
||||
<div className="text-left">
|
||||
<h3 className="text-base font-semibold">
|
||||
{t("settings.advanced.logConfig.title")}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground font-normal">
|
||||
{t("settings.advanced.logConfig.description")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-6 pb-6 pt-4 border-t border-border/50">
|
||||
<LogConfigPanel />
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</motion.div>
|
||||
) : null}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="usage" className="mt-0">
|
||||
<UsageDashboard />
|
||||
</TabsContent>
|
||||
<TabsContent value="about" className="mt-0">
|
||||
<AboutSection isPortable={isPortable} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="usage" className="mt-0">
|
||||
<UsageDashboard />
|
||||
</TabsContent>
|
||||
</div>
|
||||
|
||||
{activeTab === "advanced" && settings && (
|
||||
@@ -639,10 +639,7 @@ export function SettingsPage({
|
||||
style={{ backgroundColor: "hsl(var(--background))" }}
|
||||
>
|
||||
<div className="px-6 flex items-center justify-end gap-3">
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={isSaving}
|
||||
>
|
||||
<Button onClick={handleSave} disabled={isSaving}>
|
||||
{isSaving ? (
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
|
||||
@@ -352,7 +352,7 @@
|
||||
"viewReleaseNotes": "View release notes for this version",
|
||||
"viewCurrentReleaseNotes": "View current version release notes",
|
||||
"oneClickInstall": "One-click Install",
|
||||
"oneClickInstallHint": "Install Claude Code / Codex / Gemini CLI",
|
||||
"oneClickInstallHint": "Install Claude Code / Codex / Gemini CLI / OpenCode",
|
||||
"localEnvCheck": "Local environment check",
|
||||
"installCommandsCopied": "Install commands copied",
|
||||
"installCommandsCopyFailed": "Copy failed, please copy manually.",
|
||||
|
||||
@@ -352,7 +352,7 @@
|
||||
"viewReleaseNotes": "このバージョンのリリースノートを見る",
|
||||
"viewCurrentReleaseNotes": "現在のバージョンのリリースノートを見る",
|
||||
"oneClickInstall": "ワンクリックインストール",
|
||||
"oneClickInstallHint": "Claude Code / Codex / Gemini CLI をインストール",
|
||||
"oneClickInstallHint": "Claude Code / Codex / Gemini CLI / OpenCode をインストール",
|
||||
"localEnvCheck": "ローカル環境チェック",
|
||||
"installCommandsCopied": "インストールコマンドをコピーしました",
|
||||
"installCommandsCopyFailed": "コピーに失敗しました。手動でコピーしてください。",
|
||||
|
||||
@@ -352,7 +352,7 @@
|
||||
"viewReleaseNotes": "查看该版本更新日志",
|
||||
"viewCurrentReleaseNotes": "查看当前版本更新日志",
|
||||
"oneClickInstall": "一键安装",
|
||||
"oneClickInstallHint": "安装 Claude Code / Codex / Gemini CLI",
|
||||
"oneClickInstallHint": "安装 Claude Code / Codex / Gemini CLI / OpenCode",
|
||||
"localEnvCheck": "本地环境检查",
|
||||
"installCommandsCopied": "安装命令已复制",
|
||||
"installCommandsCopyFailed": "复制失败,请手动复制。",
|
||||
|
||||
Reference in New Issue
Block a user