Add Claude plugin sync alongside VS Code integration
This commit is contained in:
103
src-tauri/src/claude_plugin.rs
Normal file
103
src-tauri/src/claude_plugin.rs
Normal file
@@ -0,0 +1,103 @@
|
||||
use std::fs;
|
||||
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> {
|
||||
let home = dirs::home_dir().ok_or_else(|| "无法获取用户主目录".to_string())?;
|
||||
Ok(home.join(CLAUDE_DIR))
|
||||
}
|
||||
|
||||
pub fn claude_config_path() -> Result<PathBuf, String> {
|
||||
Ok(claude_dir()?.join(CLAUDE_CONFIG_FILE))
|
||||
}
|
||||
|
||||
pub fn ensure_claude_dir_exists() -> Result<PathBuf, String> {
|
||||
let dir = claude_dir()?;
|
||||
if !dir.exists() {
|
||||
fs::create_dir_all(&dir).map_err(|e| format!("创建 Claude 配置目录失败: {}", e))?;
|
||||
}
|
||||
Ok(dir)
|
||||
}
|
||||
|
||||
pub fn read_claude_config() -> Result<Option<String>, String> {
|
||||
let path = claude_config_path()?;
|
||||
if path.exists() {
|
||||
let content =
|
||||
fs::read_to_string(&path).map_err(|e| format!("读取 Claude 配置失败: {}", e))?;
|
||||
Ok(Some(content))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn is_managed_config(content: &str) -> bool {
|
||||
match serde_json::from_str::<serde_json::Value>(content) {
|
||||
Ok(value) => value
|
||||
.get("primaryApiKey")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|val| val == "any")
|
||||
.unwrap_or(false),
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_claude_config() -> Result<bool, String> {
|
||||
let path = claude_config_path()?;
|
||||
ensure_claude_dir_exists()?;
|
||||
let need_write = match read_claude_config()? {
|
||||
Some(existing) => existing != CLAUDE_CONFIG_PAYLOAD,
|
||||
None => true,
|
||||
};
|
||||
if need_write {
|
||||
fs::write(&path, CLAUDE_CONFIG_PAYLOAD)
|
||||
.map_err(|e| format!("写入 Claude 配置失败: {}", e))?;
|
||||
}
|
||||
Ok(need_write)
|
||||
}
|
||||
|
||||
pub fn clear_claude_config() -> Result<bool, String> {
|
||||
let path = claude_config_path()?;
|
||||
if !path.exists() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let content = match read_claude_config()? {
|
||||
Some(content) => content,
|
||||
None => return Ok(false),
|
||||
};
|
||||
|
||||
let mut value = match serde_json::from_str::<serde_json::Value>(&content) {
|
||||
Ok(value) => value,
|
||||
Err(_) => return Ok(false),
|
||||
};
|
||||
|
||||
let obj = match value.as_object_mut() {
|
||||
Some(obj) => obj,
|
||||
None => return Ok(false),
|
||||
};
|
||||
|
||||
if obj.remove("primaryApiKey").is_none() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let serialized = serde_json::to_string_pretty(&value)
|
||||
.map_err(|e| format!("序列化 Claude 配置失败: {}", e))?;
|
||||
fs::write(&path, format!("{}\n", serialized))
|
||||
.map_err(|e| format!("写入 Claude 配置失败: {}", e))?;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub fn claude_config_status() -> Result<(bool, PathBuf), String> {
|
||||
let path = claude_config_path()?;
|
||||
Ok((path.exists(), path))
|
||||
}
|
||||
|
||||
pub fn is_claude_config_applied() -> Result<bool, String> {
|
||||
match read_claude_config()? {
|
||||
Some(content) => Ok(is_managed_config(&content)),
|
||||
None => Ok(false),
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ use tauri_plugin_dialog::DialogExt;
|
||||
use tauri_plugin_opener::OpenerExt;
|
||||
|
||||
use crate::app_config::AppType;
|
||||
use crate::claude_plugin;
|
||||
use crate::codex_config;
|
||||
use crate::config::{self, get_claude_settings_path, ConfigStatus};
|
||||
use crate::provider::Provider;
|
||||
@@ -730,3 +731,37 @@ pub async fn write_vscode_settings(content: String) -> Result<bool, String> {
|
||||
Err("未找到 VS Code 用户设置文件".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Claude 插件:获取 ~/.claude/config.json 状态
|
||||
#[tauri::command]
|
||||
pub async fn get_claude_plugin_status() -> Result<ConfigStatus, String> {
|
||||
match claude_plugin::claude_config_status() {
|
||||
Ok((exists, path)) => Ok(ConfigStatus {
|
||||
exists,
|
||||
path: path.to_string_lossy().to_string(),
|
||||
}),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
/// Claude 插件:读取配置内容(若不存在返回 Ok(None))
|
||||
#[tauri::command]
|
||||
pub async fn read_claude_plugin_config() -> Result<Option<String>, String> {
|
||||
claude_plugin::read_claude_config()
|
||||
}
|
||||
|
||||
/// Claude 插件:写入/清除固定配置
|
||||
#[tauri::command]
|
||||
pub async fn apply_claude_plugin_config(official: bool) -> Result<bool, String> {
|
||||
if official {
|
||||
claude_plugin::clear_claude_config()
|
||||
} else {
|
||||
claude_plugin::write_claude_config()
|
||||
}
|
||||
}
|
||||
|
||||
/// Claude 插件:检测是否已写入目标配置
|
||||
#[tauri::command]
|
||||
pub async fn is_claude_plugin_applied() -> Result<bool, String> {
|
||||
claude_plugin::is_claude_config_applied()
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
mod app_config;
|
||||
mod claude_plugin;
|
||||
mod codex_config;
|
||||
mod commands;
|
||||
mod config;
|
||||
@@ -418,6 +419,10 @@ pub fn run() {
|
||||
commands::get_vscode_settings_status,
|
||||
commands::read_vscode_settings,
|
||||
commands::write_vscode_settings,
|
||||
commands::get_claude_plugin_status,
|
||||
commands::read_claude_plugin_config,
|
||||
commands::apply_claude_plugin_config,
|
||||
commands::is_claude_plugin_applied,
|
||||
update_tray_menu,
|
||||
]);
|
||||
|
||||
|
||||
34
src/App.tsx
34
src/App.tsx
@@ -102,6 +102,10 @@ function App() {
|
||||
if (data.appType === "codex" && isAutoSyncEnabled) {
|
||||
await syncCodexToVSCode(data.providerId, true);
|
||||
}
|
||||
|
||||
if (data.appType === "claude") {
|
||||
await syncClaudePlugin(data.providerId, true);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(t("console.setupListenerFailed"), error);
|
||||
@@ -240,6 +244,32 @@ function App() {
|
||||
}
|
||||
};
|
||||
|
||||
// 同步 Claude 插件配置(写入/移除固定 JSON)
|
||||
const syncClaudePlugin = async (providerId: string, silent = false) => {
|
||||
try {
|
||||
const provider = providers[providerId];
|
||||
if (!provider) return;
|
||||
const isOfficial = provider.category === "official";
|
||||
await window.api.applyClaudePluginConfig({ official: isOfficial });
|
||||
if (!silent) {
|
||||
showNotification(
|
||||
isOfficial
|
||||
? t("notifications.removedFromClaudePlugin")
|
||||
: t("notifications.appliedToClaudePlugin"),
|
||||
"success",
|
||||
2000,
|
||||
);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("同步 Claude 插件失败:", error);
|
||||
if (!silent) {
|
||||
const message =
|
||||
error?.message || t("notifications.syncClaudePluginFailed");
|
||||
showNotification(message, "error", 5000);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSwitchProvider = async (id: string) => {
|
||||
const success = await window.api.switchProvider(id, activeApp);
|
||||
if (success) {
|
||||
@@ -258,6 +288,10 @@ function App() {
|
||||
if (activeApp === "codex" && isAutoSyncEnabled) {
|
||||
await syncCodexToVSCode(id, true); // silent模式,不显示通知
|
||||
}
|
||||
|
||||
if (activeApp === "claude") {
|
||||
await syncClaudePlugin(id, true);
|
||||
}
|
||||
} else {
|
||||
showNotification(t("notifications.switchFailed"), "error");
|
||||
}
|
||||
|
||||
@@ -70,6 +70,7 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
||||
// VS Code 按钮:仅在 Codex + 当前供应商显示;按钮文案根据是否"已应用"变化
|
||||
const [vscodeAppliedFor, setVscodeAppliedFor] = useState<string | null>(null);
|
||||
const { enableAutoSync, disableAutoSync } = useVSCodeAutoSync();
|
||||
const [claudeApplied, setClaudeApplied] = useState<boolean>(false);
|
||||
|
||||
// 当当前供应商或 appType 变化时,尝试读取 VS Code settings 并检测状态
|
||||
useEffect(() => {
|
||||
@@ -104,6 +105,24 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
||||
check();
|
||||
}, [appType, currentProviderId, providers]);
|
||||
|
||||
// 检查 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("检测 Claude 插件配置失败:", error);
|
||||
setClaudeApplied(false);
|
||||
}
|
||||
};
|
||||
checkClaude();
|
||||
}, [appType, currentProviderId, providers]);
|
||||
|
||||
const handleApplyToVSCode = async (provider: Provider) => {
|
||||
try {
|
||||
const status = await window.api.getVSCodeSettingsStatus();
|
||||
@@ -181,6 +200,36 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
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) => {
|
||||
// 按添加时间排序
|
||||
@@ -272,9 +321,10 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
||||
|
||||
<div className="flex items-center gap-2 ml-4">
|
||||
{/* VS Code 按钮占位容器 - 始终保持空间,避免布局跳动 */}
|
||||
{appType === "codex" ? (
|
||||
<div className="w-[130px]">
|
||||
{provider.category !== "official" && isCurrent && (
|
||||
<div className="w-[130px]">
|
||||
{appType === "codex" &&
|
||||
provider.category !== "official" &&
|
||||
isCurrent && (
|
||||
<button
|
||||
onClick={() =>
|
||||
vscodeAppliedFor === provider.id
|
||||
@@ -298,8 +348,34 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
||||
: t("provider.applyToVSCode")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{appType === "claude" &&
|
||||
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>
|
||||
<button
|
||||
onClick={() => onSwitch(provider.id)}
|
||||
disabled={isCurrent}
|
||||
|
||||
@@ -39,7 +39,9 @@
|
||||
"configError": "Configuration Error",
|
||||
"notConfigured": "Not configured for official website",
|
||||
"applyToVSCode": "Apply to VS Code",
|
||||
"removeFromVSCode": "Remove from VS Code"
|
||||
"removeFromVSCode": "Remove from VS Code",
|
||||
"applyToClaudePlugin": "Apply to Claude plugin",
|
||||
"removeFromClaudePlugin": "Remove from Claude plugin"
|
||||
},
|
||||
"notifications": {
|
||||
"providerSaved": "Provider configuration saved",
|
||||
@@ -54,7 +56,10 @@
|
||||
"missingBaseUrl": "Current configuration missing base_url, cannot write to VS Code",
|
||||
"saveFailed": "Save failed: {{error}}",
|
||||
"saveFailedGeneric": "Save failed, please try again",
|
||||
"syncVSCodeFailed": "Sync to VS Code failed"
|
||||
"syncVSCodeFailed": "Sync to VS Code failed",
|
||||
"appliedToClaudePlugin": "Applied to Claude plugin",
|
||||
"removedFromClaudePlugin": "Removed from Claude plugin",
|
||||
"syncClaudePluginFailed": "Sync Claude plugin failed"
|
||||
},
|
||||
"confirm": {
|
||||
"deleteProvider": "Delete Provider",
|
||||
|
||||
@@ -39,7 +39,9 @@
|
||||
"configError": "配置错误",
|
||||
"notConfigured": "未配置官网地址",
|
||||
"applyToVSCode": "应用到 VS Code",
|
||||
"removeFromVSCode": "从 VS Code 移除"
|
||||
"removeFromVSCode": "从 VS Code 移除",
|
||||
"applyToClaudePlugin": "应用到 Claude 插件",
|
||||
"removeFromClaudePlugin": "从 Claude 插件移除"
|
||||
},
|
||||
"notifications": {
|
||||
"providerSaved": "供应商配置已保存",
|
||||
@@ -54,7 +56,10 @@
|
||||
"missingBaseUrl": "当前配置缺少 base_url,无法写入 VS Code",
|
||||
"saveFailed": "保存失败:{{error}}",
|
||||
"saveFailedGeneric": "保存失败,请重试",
|
||||
"syncVSCodeFailed": "同步 VS Code 失败"
|
||||
"syncVSCodeFailed": "同步 VS Code 失败",
|
||||
"appliedToClaudePlugin": "已应用到 Claude 插件",
|
||||
"removedFromClaudePlugin": "已从 Claude 插件移除",
|
||||
"syncClaudePluginFailed": "同步 Claude 插件失败"
|
||||
},
|
||||
"confirm": {
|
||||
"deleteProvider": "删除供应商",
|
||||
|
||||
@@ -306,6 +306,46 @@ export const tauriAPI = {
|
||||
throw new Error(`写入 VS Code 设置失败: ${String(error)}`);
|
||||
}
|
||||
},
|
||||
|
||||
// Claude 插件:获取 ~/.claude/config.json 状态
|
||||
getClaudePluginStatus: async (): Promise<ConfigStatus> => {
|
||||
try {
|
||||
return await invoke<ConfigStatus>("get_claude_plugin_status");
|
||||
} catch (error) {
|
||||
console.error("获取 Claude 插件状态失败:", error);
|
||||
return { exists: false, path: "", error: String(error) };
|
||||
}
|
||||
},
|
||||
|
||||
// Claude 插件:读取配置内容
|
||||
readClaudePluginConfig: async (): Promise<string | null> => {
|
||||
try {
|
||||
return await invoke<string | null>("read_claude_plugin_config");
|
||||
} catch (error) {
|
||||
throw new Error(`读取 Claude 插件配置失败: ${String(error)}`);
|
||||
}
|
||||
},
|
||||
|
||||
// Claude 插件:应用或移除固定配置
|
||||
applyClaudePluginConfig: async (options: {
|
||||
official: boolean;
|
||||
}): Promise<boolean> => {
|
||||
const { official } = options;
|
||||
try {
|
||||
return await invoke<boolean>("apply_claude_plugin_config", { official });
|
||||
} catch (error) {
|
||||
throw new Error(`写入 Claude 插件配置失败: ${String(error)}`);
|
||||
}
|
||||
},
|
||||
|
||||
// Claude 插件:检测是否已应用目标配置
|
||||
isClaudePluginApplied: async (): Promise<boolean> => {
|
||||
try {
|
||||
return await invoke<boolean>("is_claude_plugin_applied");
|
||||
} catch (error) {
|
||||
throw new Error(`检测 Claude 插件配置失败: ${String(error)}`);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// 创建全局 API 对象,兼容现有代码
|
||||
|
||||
7
src/vite-env.d.ts
vendored
7
src/vite-env.d.ts
vendored
@@ -46,6 +46,13 @@ declare global {
|
||||
getVSCodeSettingsStatus: () => Promise<ConfigStatus>;
|
||||
readVSCodeSettings: () => Promise<string>;
|
||||
writeVSCodeSettings: (content: string) => Promise<boolean>;
|
||||
// Claude 插件配置能力
|
||||
getClaudePluginStatus: () => Promise<ConfigStatus>;
|
||||
readClaudePluginConfig: () => Promise<string | null>;
|
||||
applyClaudePluginConfig: (options: {
|
||||
official: boolean;
|
||||
}) => Promise<boolean>;
|
||||
isClaudePluginApplied: () => Promise<boolean>;
|
||||
};
|
||||
platform: {
|
||||
isMac: boolean;
|
||||
|
||||
Reference in New Issue
Block a user