feat(settings): add 'Apply to Claude Code extension' toggle

- Apply immediately on save (write or remove primaryApiKey)
- Honor setting on provider switch (enabled: write for non-official, remove for official; disabled: no auto writes)
- Remove per-provider Claude plugin buttons from ProviderList
- Upsert primaryApiKey=any preserving other fields; respect override dir
- Add zh/en i18n for the new setting
This commit is contained in:
Jason
2025-10-10 16:35:21 +08:00
parent 70d8d2cc43
commit c350e64687
8 changed files with 87 additions and 87 deletions

View File

@@ -3,9 +3,12 @@ use std::path::PathBuf;
const CLAUDE_DIR: &str = ".claude";
const CLAUDE_CONFIG_FILE: &str = "config.json";
const CLAUDE_CONFIG_PAYLOAD: &str = "{\n \"primaryApiKey\": \"any\"\n}\n";
fn claude_dir() -> Result<PathBuf, String> {
// 优先使用设置中的覆盖目录
if let Some(dir) = crate::settings::get_claude_override_dir() {
return Ok(dir);
}
let home = dirs::home_dir().ok_or_else(|| "无法获取用户主目录".to_string())?;
Ok(home.join(CLAUDE_DIR))
}
@@ -45,17 +48,37 @@ fn is_managed_config(content: &str) -> bool {
}
pub fn write_claude_config() -> Result<bool, String> {
// 增量写入:仅设置 primaryApiKey = "any",保留其它字段
let path = claude_config_path()?;
ensure_claude_dir_exists()?;
let need_write = match read_claude_config()? {
Some(existing) => existing != CLAUDE_CONFIG_PAYLOAD,
None => true,
// 尝试读取并解析为对象
let mut obj = match read_claude_config()? {
Some(existing) => match serde_json::from_str::<serde_json::Value>(&existing) {
Ok(serde_json::Value::Object(map)) => serde_json::Value::Object(map),
_ => serde_json::json!({}),
},
None => serde_json::json!({}),
};
if need_write {
fs::write(&path, CLAUDE_CONFIG_PAYLOAD)
.map_err(|e| format!("写入 Claude 配置失败: {}", e))?;
let mut changed = false;
if let Some(map) = obj.as_object_mut() {
let cur = map.get("primaryApiKey").and_then(|v| v.as_str()).unwrap_or("");
if cur != "any" {
map.insert("primaryApiKey".to_string(), serde_json::Value::String("any".to_string()));
changed = true;
}
}
if changed || !path.exists() {
let serialized = serde_json::to_string_pretty(&obj)
.map_err(|e| format!("序列化 Claude 配置失败: {}", e))?;
fs::write(&path, format!("{}\n", serialized))
.map_err(|e| format!("写入 Claude 配置失败: {}", e))?;
Ok(true)
} else {
Ok(false)
}
Ok(need_write)
}
pub fn clear_claude_config() -> Result<bool, String> {

View File

@@ -22,6 +22,9 @@ pub struct AppSettings {
pub show_in_tray: bool,
#[serde(default = "default_minimize_to_tray_on_close")]
pub minimize_to_tray_on_close: bool,
/// 是否启用 Claude 插件联动
#[serde(default)]
pub enable_claude_plugin_integration: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub claude_config_dir: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -49,6 +52,7 @@ impl Default for AppSettings {
Self {
show_in_tray: true,
minimize_to_tray_on_close: true,
enable_claude_plugin_integration: false,
claude_config_dir: None,
codex_config_dir: None,
language: None,

View File

@@ -183,9 +183,14 @@ function App() {
});
};
// 同步 Claude 插件配置(写入/移除固定 JSON
// 同步 Claude 插件配置(按设置决定是否联动;开启时:非官方写入,官方移除
const syncClaudePlugin = async (providerId: string, silent = false) => {
try {
const settings = await window.api.getSettings();
if (!(settings as any)?.enableClaudePluginIntegration) {
// 未开启联动:不执行写入/移除
return;
}
const provider = providers[providerId];
if (!provider) return;
const isOfficial = provider.category === "official";

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import React from "react";
import { useTranslation } from "react-i18next";
import { Provider } from "../types";
import { Play, Edit3, Trash2, CheckCircle2, Users, Check } from "lucide-react";
@@ -58,55 +58,7 @@ const ProviderList: React.FC<ProviderListProps> = ({
}
};
const [claudeApplied, setClaudeApplied] = useState<boolean>(false);
// 检查 Claude 插件配置是否已应用
useEffect(() => {
const checkClaude = async () => {
if (appType !== "claude" || !currentProviderId) {
setClaudeApplied(false);
return;
}
try {
const applied = await window.api.isClaudePluginApplied();
setClaudeApplied(applied);
} catch (error) {
console.error(t("console.setupListenerFailed"), error);
setClaudeApplied(false);
}
};
checkClaude();
}, [appType, currentProviderId, providers, t]);
const handleApplyToClaudePlugin = async () => {
try {
await window.api.applyClaudePluginConfig({ official: false });
onNotify?.(t("notifications.appliedToClaudePlugin"), "success", 3000);
setClaudeApplied(true);
} catch (error: any) {
console.error(error);
const msg =
error && error.message
? error.message
: t("notifications.syncClaudePluginFailed");
onNotify?.(msg, "error", 5000);
}
};
const handleRemoveFromClaudePlugin = async () => {
try {
await window.api.applyClaudePluginConfig({ official: true });
onNotify?.(t("notifications.removedFromClaudePlugin"), "success", 3000);
setClaudeApplied(false);
} catch (error: any) {
console.error(error);
const msg =
error && error.message
? error.message
: t("notifications.syncClaudePluginFailed");
onNotify?.(msg, "error", 5000);
}
};
// 列表页不再提供 Claude 插件按钮,统一在“设置”中控制
// 对供应商列表进行排序
const sortedProviders = Object.values(providers).sort((a, b) => {
@@ -201,34 +153,6 @@ const ProviderList: React.FC<ProviderListProps> = ({
</div>
<div className="flex items-center gap-2 ml-4">
{appType === "claude" ? (
<div className="flex-shrink-0">
{provider.category !== "official" && isCurrent && (
<button
onClick={() =>
claudeApplied
? handleRemoveFromClaudePlugin()
: handleApplyToClaudePlugin()
}
className={cn(
"inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium rounded-md transition-colors w-full whitespace-nowrap justify-center",
claudeApplied
? "border border-gray-300 text-gray-600 hover:border-red-300 hover:text-red-600 hover:bg-red-50 dark:border-gray-600 dark:text-gray-400 dark:hover:border-red-800 dark:hover:text-red-400 dark:hover:bg-red-900/20"
: "border border-gray-300 text-gray-700 hover:border-green-300 hover:text-green-600 hover:bg-green-50 dark:border-gray-600 dark:text-gray-300 dark:hover:border-green-700 dark:hover:text-green-400 dark:hover:bg-green-900/20",
)}
title={
claudeApplied
? t("provider.removeFromClaudePlugin")
: t("provider.applyToClaudePlugin")
}
>
{claudeApplied
? t("provider.removeFromClaudePlugin")
: t("provider.applyToClaudePlugin")}
</button>
)}
</div>
) : null}
<button
onClick={() => onSwitch(provider.id)}
disabled={isCurrent}

View File

@@ -50,6 +50,7 @@ export default function SettingsModal({
const [settings, setSettings] = useState<Settings>({
showInTray: true,
minimizeToTrayOnClose: true,
enableClaudePluginIntegration: false,
claudeConfigDir: undefined,
codexConfigDir: undefined,
language: persistedLanguage,
@@ -116,6 +117,11 @@ export default function SettingsModal({
setSettings({
showInTray,
minimizeToTrayOnClose,
enableClaudePluginIntegration:
typeof (loadedSettings as any)?.enableClaudePluginIntegration ===
"boolean"
? (loadedSettings as any).enableClaudePluginIntegration
: false,
claudeConfigDir:
typeof (loadedSettings as any)?.claudeConfigDir === "string"
? (loadedSettings as any).claudeConfigDir
@@ -184,6 +190,16 @@ export default function SettingsModal({
language: selectedLanguage,
};
await window.api.saveSettings(payload);
// 立即生效:根据开关无条件写入/移除 ~/.claude/config.json
try {
if (payload.enableClaudePluginIntegration) {
await window.api.applyClaudePluginConfig({ official: false });
} else {
await window.api.applyClaudePluginConfig({ official: true });
}
} catch (e) {
console.warn("[Settings] Apply Claude plugin config on save failed", e);
}
setSettings(payload);
try {
window.localStorage.setItem("language", selectedLanguage);
@@ -506,6 +522,28 @@ export default function SettingsModal({
className="w-4 h-4 text-blue-500 rounded focus:ring-blue-500/20"
/>
</label>
{/* Claude 插件联动开关 */}
<label className="flex items-center justify-between">
<div>
<span className="text-sm text-gray-900 dark:text-gray-100">
{t("settings.enableClaudePluginIntegration")}
</span>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1 max-w-[34rem]">
{t("settings.enableClaudePluginIntegrationDescription")}
</p>
</div>
<input
type="checkbox"
checked={!!settings.enableClaudePluginIntegration}
onChange={(e) =>
setSettings((prev) => ({
...prev,
enableClaudePluginIntegration: e.target.checked,
}))
}
className="w-4 h-4 text-blue-500 rounded focus:ring-blue-500/20"
/>
</label>
</div>
</div>

View File

@@ -98,6 +98,8 @@
"windowBehavior": "Window Behavior",
"minimizeToTray": "Minimize to tray on close",
"minimizeToTrayDescription": "When checked, clicking the close button will hide to system tray, otherwise the app will exit directly.",
"enableClaudePluginIntegration": "Apply to Claude Code extension",
"enableClaudePluginIntegrationDescription": "When enabled, you can use third-party providers in the VS Code Claude Code extension",
"configFileLocation": "Configuration File Location",
"openFolder": "Open Folder",
"configDirectoryOverride": "Configuration Directory Override (Advanced)",

View File

@@ -98,6 +98,8 @@
"windowBehavior": "窗口行为",
"minimizeToTray": "关闭时最小化到托盘",
"minimizeToTrayDescription": "勾选后点击关闭按钮会隐藏到系统托盘,取消则直接退出应用。",
"enableClaudePluginIntegration": "应用到Claude Code 插件",
"enableClaudePluginIntegrationDescription": "开启后可以在 Vscode Claude Code 插件里使用第三方供应商",
"configFileLocation": "配置文件位置",
"openFolder": "打开文件夹",
"configDirectoryOverride": "配置目录覆盖(高级)",

View File

@@ -41,6 +41,8 @@ export interface Settings {
showInTray: boolean;
// 点击关闭按钮时是否最小化到托盘而不是关闭应用
minimizeToTrayOnClose: boolean;
// 启用 Claude 插件联动(写入 ~/.claude/config.json 的 primaryApiKey
enableClaudePluginIntegration?: boolean;
// 覆盖 Claude Code 配置目录(可选)
claudeConfigDir?: string;
// 覆盖 Codex 配置目录(可选)