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:
@@ -3,9 +3,12 @@ use std::path::PathBuf;
|
|||||||
|
|
||||||
const CLAUDE_DIR: &str = ".claude";
|
const CLAUDE_DIR: &str = ".claude";
|
||||||
const CLAUDE_CONFIG_FILE: &str = "config.json";
|
const CLAUDE_CONFIG_FILE: &str = "config.json";
|
||||||
const CLAUDE_CONFIG_PAYLOAD: &str = "{\n \"primaryApiKey\": \"any\"\n}\n";
|
|
||||||
|
|
||||||
fn claude_dir() -> Result<PathBuf, String> {
|
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())?;
|
let home = dirs::home_dir().ok_or_else(|| "无法获取用户主目录".to_string())?;
|
||||||
Ok(home.join(CLAUDE_DIR))
|
Ok(home.join(CLAUDE_DIR))
|
||||||
}
|
}
|
||||||
@@ -45,17 +48,37 @@ fn is_managed_config(content: &str) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn write_claude_config() -> Result<bool, String> {
|
pub fn write_claude_config() -> Result<bool, String> {
|
||||||
|
// 增量写入:仅设置 primaryApiKey = "any",保留其它字段
|
||||||
let path = claude_config_path()?;
|
let path = claude_config_path()?;
|
||||||
ensure_claude_dir_exists()?;
|
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)
|
let mut changed = false;
|
||||||
.map_err(|e| format!("写入 Claude 配置失败: {}", e))?;
|
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> {
|
pub fn clear_claude_config() -> Result<bool, String> {
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ pub struct AppSettings {
|
|||||||
pub show_in_tray: bool,
|
pub show_in_tray: bool,
|
||||||
#[serde(default = "default_minimize_to_tray_on_close")]
|
#[serde(default = "default_minimize_to_tray_on_close")]
|
||||||
pub minimize_to_tray_on_close: bool,
|
pub minimize_to_tray_on_close: bool,
|
||||||
|
/// 是否启用 Claude 插件联动
|
||||||
|
#[serde(default)]
|
||||||
|
pub enable_claude_plugin_integration: bool,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub claude_config_dir: Option<String>,
|
pub claude_config_dir: Option<String>,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
@@ -49,6 +52,7 @@ impl Default for AppSettings {
|
|||||||
Self {
|
Self {
|
||||||
show_in_tray: true,
|
show_in_tray: true,
|
||||||
minimize_to_tray_on_close: true,
|
minimize_to_tray_on_close: true,
|
||||||
|
enable_claude_plugin_integration: false,
|
||||||
claude_config_dir: None,
|
claude_config_dir: None,
|
||||||
codex_config_dir: None,
|
codex_config_dir: None,
|
||||||
language: None,
|
language: None,
|
||||||
|
|||||||
@@ -183,9 +183,14 @@ function App() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// 同步 Claude 插件配置(写入/移除固定 JSON)
|
// 同步 Claude 插件配置(按设置决定是否联动;开启时:非官方写入,官方移除)
|
||||||
const syncClaudePlugin = async (providerId: string, silent = false) => {
|
const syncClaudePlugin = async (providerId: string, silent = false) => {
|
||||||
try {
|
try {
|
||||||
|
const settings = await window.api.getSettings();
|
||||||
|
if (!(settings as any)?.enableClaudePluginIntegration) {
|
||||||
|
// 未开启联动:不执行写入/移除
|
||||||
|
return;
|
||||||
|
}
|
||||||
const provider = providers[providerId];
|
const provider = providers[providerId];
|
||||||
if (!provider) return;
|
if (!provider) return;
|
||||||
const isOfficial = provider.category === "official";
|
const isOfficial = provider.category === "official";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Provider } from "../types";
|
import { Provider } from "../types";
|
||||||
import { Play, Edit3, Trash2, CheckCircle2, Users, Check } from "lucide-react";
|
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 插件按钮,统一在“设置”中控制
|
||||||
|
|
||||||
// 检查 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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 对供应商列表进行排序
|
// 对供应商列表进行排序
|
||||||
const sortedProviders = Object.values(providers).sort((a, b) => {
|
const sortedProviders = Object.values(providers).sort((a, b) => {
|
||||||
@@ -201,34 +153,6 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 ml-4">
|
<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
|
<button
|
||||||
onClick={() => onSwitch(provider.id)}
|
onClick={() => onSwitch(provider.id)}
|
||||||
disabled={isCurrent}
|
disabled={isCurrent}
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ export default function SettingsModal({
|
|||||||
const [settings, setSettings] = useState<Settings>({
|
const [settings, setSettings] = useState<Settings>({
|
||||||
showInTray: true,
|
showInTray: true,
|
||||||
minimizeToTrayOnClose: true,
|
minimizeToTrayOnClose: true,
|
||||||
|
enableClaudePluginIntegration: false,
|
||||||
claudeConfigDir: undefined,
|
claudeConfigDir: undefined,
|
||||||
codexConfigDir: undefined,
|
codexConfigDir: undefined,
|
||||||
language: persistedLanguage,
|
language: persistedLanguage,
|
||||||
@@ -116,6 +117,11 @@ export default function SettingsModal({
|
|||||||
setSettings({
|
setSettings({
|
||||||
showInTray,
|
showInTray,
|
||||||
minimizeToTrayOnClose,
|
minimizeToTrayOnClose,
|
||||||
|
enableClaudePluginIntegration:
|
||||||
|
typeof (loadedSettings as any)?.enableClaudePluginIntegration ===
|
||||||
|
"boolean"
|
||||||
|
? (loadedSettings as any).enableClaudePluginIntegration
|
||||||
|
: false,
|
||||||
claudeConfigDir:
|
claudeConfigDir:
|
||||||
typeof (loadedSettings as any)?.claudeConfigDir === "string"
|
typeof (loadedSettings as any)?.claudeConfigDir === "string"
|
||||||
? (loadedSettings as any).claudeConfigDir
|
? (loadedSettings as any).claudeConfigDir
|
||||||
@@ -184,6 +190,16 @@ export default function SettingsModal({
|
|||||||
language: selectedLanguage,
|
language: selectedLanguage,
|
||||||
};
|
};
|
||||||
await window.api.saveSettings(payload);
|
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);
|
setSettings(payload);
|
||||||
try {
|
try {
|
||||||
window.localStorage.setItem("language", selectedLanguage);
|
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"
|
className="w-4 h-4 text-blue-500 rounded focus:ring-blue-500/20"
|
||||||
/>
|
/>
|
||||||
</label>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -98,6 +98,8 @@
|
|||||||
"windowBehavior": "Window Behavior",
|
"windowBehavior": "Window Behavior",
|
||||||
"minimizeToTray": "Minimize to tray on close",
|
"minimizeToTray": "Minimize to tray on close",
|
||||||
"minimizeToTrayDescription": "When checked, clicking the close button will hide to system tray, otherwise the app will exit directly.",
|
"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",
|
"configFileLocation": "Configuration File Location",
|
||||||
"openFolder": "Open Folder",
|
"openFolder": "Open Folder",
|
||||||
"configDirectoryOverride": "Configuration Directory Override (Advanced)",
|
"configDirectoryOverride": "Configuration Directory Override (Advanced)",
|
||||||
|
|||||||
@@ -98,6 +98,8 @@
|
|||||||
"windowBehavior": "窗口行为",
|
"windowBehavior": "窗口行为",
|
||||||
"minimizeToTray": "关闭时最小化到托盘",
|
"minimizeToTray": "关闭时最小化到托盘",
|
||||||
"minimizeToTrayDescription": "勾选后点击关闭按钮会隐藏到系统托盘,取消则直接退出应用。",
|
"minimizeToTrayDescription": "勾选后点击关闭按钮会隐藏到系统托盘,取消则直接退出应用。",
|
||||||
|
"enableClaudePluginIntegration": "应用到Claude Code 插件",
|
||||||
|
"enableClaudePluginIntegrationDescription": "开启后可以在 Vscode Claude Code 插件里使用第三方供应商",
|
||||||
"configFileLocation": "配置文件位置",
|
"configFileLocation": "配置文件位置",
|
||||||
"openFolder": "打开文件夹",
|
"openFolder": "打开文件夹",
|
||||||
"configDirectoryOverride": "配置目录覆盖(高级)",
|
"configDirectoryOverride": "配置目录覆盖(高级)",
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ export interface Settings {
|
|||||||
showInTray: boolean;
|
showInTray: boolean;
|
||||||
// 点击关闭按钮时是否最小化到托盘而不是关闭应用
|
// 点击关闭按钮时是否最小化到托盘而不是关闭应用
|
||||||
minimizeToTrayOnClose: boolean;
|
minimizeToTrayOnClose: boolean;
|
||||||
|
// 启用 Claude 插件联动(写入 ~/.claude/config.json 的 primaryApiKey)
|
||||||
|
enableClaudePluginIntegration?: boolean;
|
||||||
// 覆盖 Claude Code 配置目录(可选)
|
// 覆盖 Claude Code 配置目录(可选)
|
||||||
claudeConfigDir?: string;
|
claudeConfigDir?: string;
|
||||||
// 覆盖 Codex 配置目录(可选)
|
// 覆盖 Codex 配置目录(可选)
|
||||||
|
|||||||
Reference in New Issue
Block a user