feat: add Tool Search domain restriction bypass with active-installation patching

Resolve the active `claude` command from PATH and apply an equal-length
byte patch to remove the domain whitelist check. Backups are stored in
~/.cc-switch/toolsearch-backups/ (SHA-256 of path) so they survive
Claude Code version upgrades. The patch auto-reapplies on app startup
when the setting is enabled.

Frontend checks PatchResult.success and rolls back the setting on failure.
This commit is contained in:
Jason
2026-03-14 21:20:05 +08:00
parent 7097a0d710
commit 9439153f05
13 changed files with 699 additions and 2 deletions
+2
View File
@@ -19,6 +19,7 @@ mod settings;
pub mod skill;
mod stream_check;
mod sync_support;
mod toolsearch;
mod usage;
mod webdav_sync;
mod workspace;
@@ -41,6 +42,7 @@ pub use session_manager::*;
pub use settings::*;
pub use skill::*;
pub use stream_check::*;
pub use toolsearch::*;
pub use usage::*;
pub use webdav_sync::*;
pub use workspace::*;
+21
View File
@@ -0,0 +1,21 @@
#![allow(non_snake_case)]
/// Check Tool Search patch status for the active Claude Code installation
#[tauri::command]
pub async fn check_toolsearch_status() -> Result<crate::toolsearch_patch::ToolSearchStatus, String>
{
crate::toolsearch_patch::check_toolsearch_status().map_err(|e| e.to_string())
}
/// Apply Tool Search patch (bypass domain restriction) to the active installation
#[tauri::command]
pub async fn apply_toolsearch_patch() -> Result<Vec<crate::toolsearch_patch::PatchResult>, String> {
crate::toolsearch_patch::apply_toolsearch_patch().map_err(|e| e.to_string())
}
/// Restore Tool Search patch (re-enable domain restriction) for the active installation
#[tauri::command]
pub async fn restore_toolsearch_patch() -> Result<Vec<crate::toolsearch_patch::PatchResult>, String>
{
crate::toolsearch_patch::restore_toolsearch_patch().map_err(|e| e.to_string())
}
+26
View File
@@ -25,6 +25,7 @@ mod services;
mod session_manager;
mod settings;
mod store;
mod toolsearch_patch;
mod tray;
mod usage_script;
@@ -805,6 +806,27 @@ pub fn run() {
}
}
// Tool Search bypass: auto-apply patch on startup if enabled
if settings.tool_search_bypass {
match crate::toolsearch_patch::apply_toolsearch_patch() {
Ok(results) => {
let success = results.iter().filter(|r| r.success).count();
let total = results.len();
if success > 0 {
log::info!("✓ Tool Search patch auto-applied ({success}/{total})");
}
for r in results.iter().filter(|r| !r.success) {
if let Some(err) = &r.error {
log::warn!("✗ Tool Search patch failed for {}: {err}", r.path);
}
}
}
Err(e) => {
log::warn!("✗ Tool Search auto-patch skipped: {e}");
}
}
}
Ok(())
})
.invoke_handler(tauri::generate_handler![
@@ -851,6 +873,10 @@ pub fn run() {
commands::is_claude_plugin_applied,
commands::apply_claude_onboarding_skip,
commands::clear_claude_onboarding_skip,
// Tool Search patch
commands::check_toolsearch_status,
commands::apply_toolsearch_patch,
commands::restore_toolsearch_patch,
// Claude MCP management
commands::get_claude_mcp_status,
commands::read_claude_mcp_config,
+4
View File
@@ -181,6 +181,9 @@ pub struct AppSettings {
/// 是否跳过 Claude Code 初次安装确认
#[serde(default)]
pub skip_claude_onboarding: bool,
/// 是否解除 Tool Search 域名限制
#[serde(default)]
pub tool_search_bypass: bool,
/// 是否开机自启
#[serde(default)]
pub launch_on_startup: bool,
@@ -286,6 +289,7 @@ impl Default for AppSettings {
minimize_to_tray_on_close: true,
enable_claude_plugin_integration: false,
skip_claude_onboarding: false,
tool_search_bypass: false,
launch_on_startup: false,
silent_startup: false,
enable_local_proxy: false,
+569
View File
@@ -0,0 +1,569 @@
//! Tool Search domain restriction bypass patch for Claude Code.
//!
//! Resolves the current active `claude` command from PATH and patches the
//! domain whitelist check
//! `return["api.anthropic.com"].includes(x)}catch{return!1}`
//! to always return true via equal-length byte replacement.
use regex::bytes::Regex;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use std::process::Command;
use sha2::{Digest, Sha256};
use crate::error::AppError;
const BACKUP_SUFFIX: &str = ".toolsearch-bak";
/// Encode bytes as lowercase hex string (avoids adding `hex` crate dependency).
fn to_hex(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
// Regex matching the domain whitelist check with any JS identifier as variable name
const PATCH_TARGET_PATTERN: &str =
r#"return\["api\.anthropic\.com"\]\.includes\([A-Za-z_$][A-Za-z0-9_$]*\)\}catch\{return!1\}"#;
// Regex matching already-patched code
const PATCHED_PATTERN: &str = r#"return!0/\* *\*/\}catch\{return!0\}"#;
/// Single Claude Code installation info
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ClaudeInstallation {
pub path: String,
pub source: String,
pub patched: bool,
pub has_backup: bool,
}
/// Result of a patch/restore operation on one installation
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PatchResult {
pub path: String,
pub success: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
/// Overall Tool Search patch status
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ToolSearchStatus {
pub installations: Vec<ClaudeInstallation>,
pub all_patched: bool,
pub any_found: bool,
}
// ── Patch status detection ──────────────────────────────────────────
fn get_patch_status(data: &[u8]) -> &'static str {
let target_re = Regex::new(PATCH_TARGET_PATTERN).unwrap();
let patched_re = Regex::new(PATCHED_PATTERN).unwrap();
if target_re.is_match(data) {
"unpatched"
} else if patched_re.is_match(data) {
"patched"
} else {
"unknown"
}
}
/// Build equal-length replacement bytes: `return!0/* */}catch{return!0}`
fn build_patched_bytes(original_len: usize) -> Result<Vec<u8>, AppError> {
let prefix = b"return!0/*";
let suffix = b"*/}catch{return!0}";
let padding = original_len
.checked_sub(prefix.len() + suffix.len())
.ok_or_else(|| AppError::Config("Patch template too long for match".into()))?;
let mut out = Vec::with_capacity(original_len);
out.extend_from_slice(prefix);
out.extend(std::iter::repeat_n(b' ', padding));
out.extend_from_slice(suffix);
Ok(out)
}
/// Apply byte-level patch to file data, returns (patched_data, replacement_count)
fn patch_bytes(data: &[u8]) -> Result<(Vec<u8>, usize), AppError> {
let re = Regex::new(PATCH_TARGET_PATTERN).unwrap();
let mut count = 0usize;
let result = re.replace_all(data, |caps: &regex::bytes::Captures| {
count += 1;
build_patched_bytes(caps[0].len()).unwrap_or_else(|_| caps[0].to_vec())
});
Ok((result.into_owned(), count))
}
// ── Installation detection ──────────────────────────────────────────
/// Run a command and return stdout, or empty string on failure
fn run_cmd(cmd: &str, args: &[&str]) -> String {
Command::new(cmd)
.args(args)
.output()
.ok()
.filter(|o| o.status.success())
.and_then(|o| String::from_utf8(o.stdout).ok())
.map(|s| s.trim().to_string())
.unwrap_or_default()
}
/// Search a package directory for JS files containing the domain check
fn find_patch_target_in_pkg(pkg_dir: &Path) -> Option<PathBuf> {
let marker = b"api.anthropic.com";
// Check cli.js first (most common)
let cli_js = pkg_dir.join("cli.js");
if cli_js.is_file() {
if let Ok(data) = std::fs::read(&cli_js) {
if data.windows(marker.len()).any(|w| w == marker) {
return Some(cli_js);
}
}
}
// Search other JS files
find_js_with_marker(pkg_dir)
}
fn find_js_with_marker(dir: &Path) -> Option<PathBuf> {
let marker = b"api.anthropic.com";
let entries = match std::fs::read_dir(dir) {
Ok(e) => e,
Err(_) => return None,
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
if let Some(found) = find_js_with_marker(&path) {
return Some(found);
}
} else if path.extension().and_then(|e| e.to_str()) == Some("js") {
if let Ok(meta) = path.metadata() {
if meta.len() < 1000 {
continue;
}
}
if let Ok(data) = std::fs::read(&path) {
if data.windows(marker.len()).any(|w| w == marker) {
return Some(path);
}
}
}
}
None
}
/// Resolve symlinks to actual file path
fn resolve_target(path: &Path) -> PathBuf {
std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
}
fn get_patch_status_for_path(path: &Path) -> Option<&'static str> {
let data = std::fs::read(path).ok()?;
Some(get_patch_status(&data))
}
fn package_dir_from_ancestors(path: &Path) -> Option<PathBuf> {
for ancestor in path.ancestors() {
if ancestor.file_name().and_then(|v| v.to_str()) == Some("claude-code")
&& ancestor.parent().and_then(|v| v.file_name()).and_then(|v| v.to_str())
== Some("@anthropic-ai")
{
return Some(ancestor.to_path_buf());
}
}
None
}
fn push_candidate_package_dir(
candidates: &mut Vec<PathBuf>,
seen: &mut std::collections::HashSet<PathBuf>,
path: PathBuf,
) {
if path.is_dir() && seen.insert(path.clone()) {
candidates.push(path);
}
}
fn resolve_active_patch_target(command_path: &Path) -> Option<PathBuf> {
let resolved_command = resolve_target(command_path);
if matches!(
get_patch_status_for_path(&resolved_command),
Some("patched" | "unpatched")
) {
return Some(resolved_command);
}
let mut candidates = Vec::new();
let mut seen = std::collections::HashSet::new();
for path in [command_path, resolved_command.as_path()] {
if let Some(pkg_dir) = package_dir_from_ancestors(path) {
push_candidate_package_dir(&mut candidates, &mut seen, pkg_dir);
}
if let Some(bin_dir) = path.parent() {
if let Some(prefix) = bin_dir.parent() {
push_candidate_package_dir(
&mut candidates,
&mut seen,
prefix
.join("lib")
.join("node_modules")
.join("@anthropic-ai")
.join("claude-code"),
);
push_candidate_package_dir(
&mut candidates,
&mut seen,
prefix
.join("node_modules")
.join("@anthropic-ai")
.join("claude-code"),
);
}
}
}
candidates
.into_iter()
.find_map(|pkg_dir| find_patch_target_in_pkg(&pkg_dir).map(|p| resolve_target(&p)))
}
#[cfg(target_os = "windows")]
fn find_active_command_path() -> Option<PathBuf> {
run_cmd("where.exe", &["claude"])
.lines()
.next()
.map(PathBuf::from)
.filter(|path| path.is_file())
}
#[cfg(not(target_os = "windows"))]
fn find_active_command_path() -> Option<PathBuf> {
run_cmd("which", &["claude"])
.lines()
.next()
.map(PathBuf::from)
.filter(|path| path.is_file())
}
fn find_active_installation() -> Option<(PathBuf, String)> {
let command_path = find_active_command_path()?;
let patch_target = resolve_active_patch_target(&command_path)?;
Some((
patch_target,
format!("active claude ({})", command_path.display()),
))
}
fn require_active_installation() -> Result<(PathBuf, String), AppError> {
find_active_installation().ok_or_else(|| {
AppError::Config("No active Claude Code installation found in PATH".into())
})
}
// ── macOS codesign ──────────────────────────────────────────────────
#[cfg(target_os = "macos")]
fn codesign_adhoc(path: &Path) -> Result<(), AppError> {
let output = Command::new("codesign")
.args(["--force", "--sign", "-"])
.arg(path)
.output()
.map_err(|e| AppError::IoContext {
context: format!("Failed to run codesign for {}", path.display()),
source: e,
})?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(AppError::Config(format!(
"codesign failed for {}: {}",
path.display(),
stderr.trim()
)));
}
Ok(())
}
#[cfg(not(target_os = "macos"))]
fn codesign_adhoc(_path: &Path) -> Result<(), AppError> {
Ok(())
}
// ── Backup directory helpers ─────────────────────────────────────────
/// Get the centralized backup directory: `~/.cc-switch/toolsearch-backups/`
fn get_backup_dir() -> Result<PathBuf, AppError> {
let dir = crate::config::get_app_config_dir().join("toolsearch-backups");
if !dir.exists() {
std::fs::create_dir_all(&dir).map_err(|e| AppError::io(&dir, e))?;
}
Ok(dir)
}
/// Derive a stable backup filename from the original path using SHA-256.
fn backup_name_for(path: &Path) -> String {
let mut hasher = Sha256::new();
hasher.update(path.to_string_lossy().as_bytes());
to_hex(&hasher.finalize())
}
/// Get the backup file path for a given target file.
fn get_backup_path(path: &Path) -> Result<PathBuf, AppError> {
let dir = get_backup_dir()?;
let name = backup_name_for(path);
Ok(dir.join(format!("{name}.bak")))
}
/// Get the metadata file path (records original path for debugging).
fn get_meta_path(path: &Path) -> Result<PathBuf, AppError> {
let dir = get_backup_dir()?;
let name = backup_name_for(path);
Ok(dir.join(format!("{name}.meta")))
}
// ── Patch / Restore single file ─────────────────────────────────────
fn patch_single_file(path: &Path) -> Result<(), AppError> {
let data = std::fs::read(path).map_err(|e| AppError::io(path, e))?;
let status = get_patch_status(&data);
if status == "patched" {
return Ok(()); // Already patched
}
if status == "unknown" {
return Err(AppError::Config(format!(
"Target pattern not found in {}, possibly incompatible version",
path.display()
)));
}
let (patched_data, count) = patch_bytes(&data)?;
if count == 0 {
return Err(AppError::Config(format!(
"No replacements made in {}",
path.display()
)));
}
// Create backup in centralized directory
let backup_path = get_backup_path(path)?;
std::fs::copy(path, &backup_path).map_err(|e| AppError::io(&backup_path, e))?;
// Write metadata file for debugging
let meta_path = get_meta_path(path)?;
let _ = std::fs::write(&meta_path, path.to_string_lossy().as_bytes());
// Write patched data
if let Err(e) = std::fs::write(path, &patched_data) {
// Try rename trick on Windows
#[cfg(target_os = "windows")]
{
if let Ok(()) = write_via_rename(path, &patched_data) {
codesign_adhoc(path)?;
return Ok(());
}
}
return Err(AppError::io(path, e));
}
codesign_adhoc(path)?;
Ok(())
}
#[cfg(target_os = "windows")]
fn write_via_rename(target: &Path, data: &[u8]) -> Result<(), AppError> {
let tmp_path = target.with_extension("tmp");
let old_path = target.with_extension("old");
let _ = std::fs::remove_file(&tmp_path);
let _ = std::fs::remove_file(&old_path);
std::fs::write(&tmp_path, data).map_err(|e| AppError::io(&tmp_path, e))?;
std::fs::rename(target, &old_path).map_err(|e| AppError::io(target, e))?;
std::fs::rename(&tmp_path, target).map_err(|e| AppError::io(target, e))?;
let _ = std::fs::remove_file(&old_path);
Ok(())
}
fn restore_single_file(path: &Path) -> Result<(), AppError> {
let current_status = get_patch_status_for_path(path);
if matches!(current_status, Some("unpatched")) {
return Ok(());
}
// Try centralized backup directory first
let backup_path = get_backup_path(path)?;
// Fallback: legacy backup path (adjacent `.toolsearch-bak` file)
let legacy_backup = PathBuf::from(format!("{}{}", path.display(), BACKUP_SUFFIX));
let actual_backup = if backup_path.is_file() {
&backup_path
} else if legacy_backup.is_file() {
&legacy_backup
} else {
return Err(AppError::Config(format!(
"No backup found for {}",
path.display()
)));
};
let backup_data = std::fs::read(actual_backup).map_err(|e| AppError::io(actual_backup, e))?;
if let Err(e) = std::fs::write(path, &backup_data) {
#[cfg(target_os = "windows")]
{
if let Ok(()) = write_via_rename(path, &backup_data) {
codesign_adhoc(path)?;
return Ok(());
}
}
return Err(AppError::io(path, e));
}
codesign_adhoc(path)?;
Ok(())
}
// ── Public API ──────────────────────────────────────────────────────
/// Check Tool Search patch status for the current active Claude Code installation.
pub fn check_toolsearch_status() -> Result<ToolSearchStatus, AppError> {
let installations = find_active_installation()
.into_iter()
.map(|(path, source)| {
let data = std::fs::read(&path).unwrap_or_default();
let status = get_patch_status(&data);
// Check centralized backup first, then legacy
let has_backup = get_backup_path(&path)
.map(|p| p.is_file())
.unwrap_or(false)
|| PathBuf::from(format!("{}{}", path.display(), BACKUP_SUFFIX)).is_file();
ClaudeInstallation {
path: path.to_string_lossy().to_string(),
source: source.clone(),
patched: status == "patched",
has_backup,
}
})
.collect::<Vec<_>>();
let any_found = !installations.is_empty();
let all_patched = any_found && installations.iter().all(|i| i.patched);
Ok(ToolSearchStatus {
installations,
all_patched,
any_found,
})
}
/// Apply the Tool Search patch to the current active Claude Code installation.
pub fn apply_toolsearch_patch() -> Result<Vec<PatchResult>, AppError> {
let (path, _) = require_active_installation()?;
Ok(vec![match patch_single_file(&path) {
Ok(()) => PatchResult {
path: path.to_string_lossy().to_string(),
success: true,
error: None,
},
Err(e) => PatchResult {
path: path.to_string_lossy().to_string(),
success: false,
error: Some(e.to_string()),
},
}])
}
/// Restore the current active Claude Code installation from backup.
pub fn restore_toolsearch_patch() -> Result<Vec<PatchResult>, AppError> {
let (path, _) = require_active_installation()?;
Ok(vec![match restore_single_file(&path) {
Ok(()) => PatchResult {
path: path.to_string_lossy().to_string(),
success: true,
error: None,
},
Err(e) => PatchResult {
path: path.to_string_lossy().to_string(),
success: false,
error: Some(e.to_string()),
},
}])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_patch_bytes_replaces_correctly() {
let input = br#"return["api.anthropic.com"].includes(x)}catch{return!1}"#;
let (patched, count) = patch_bytes(input).unwrap();
assert_eq!(count, 1);
assert_eq!(patched.len(), input.len());
assert!(patched.starts_with(b"return!0/*"));
assert!(patched.ends_with(b"*/}catch{return!0}"));
}
#[test]
fn test_patch_status_detection() {
let unpatched = br#"return["api.anthropic.com"].includes(x)}catch{return!1}"#;
assert_eq!(get_patch_status(unpatched), "unpatched");
let (patched, _) = patch_bytes(unpatched).unwrap();
assert_eq!(get_patch_status(&patched), "patched");
assert_eq!(get_patch_status(b"some random data"), "unknown");
}
#[test]
fn test_build_patched_bytes_length() {
for len in 50..70 {
let result = build_patched_bytes(len).unwrap();
assert_eq!(result.len(), len);
}
}
#[test]
fn test_resolve_active_patch_target_from_npm_style_bin_path() {
let tmp = tempfile::tempdir().unwrap();
let prefix = tmp.path().join("prefix");
let bin_dir = prefix.join("bin");
let pkg_dir = prefix
.join("lib")
.join("node_modules")
.join("@anthropic-ai")
.join("claude-code");
std::fs::create_dir_all(&bin_dir).unwrap();
std::fs::create_dir_all(&pkg_dir).unwrap();
let command_path = bin_dir.join("claude");
std::fs::write(&command_path, b"#!/usr/bin/env node\n").unwrap();
let cli_path = pkg_dir.join("cli.js");
std::fs::write(
&cli_path,
br#"return["api.anthropic.com"].includes(x)}catch{return!1}"#,
)
.unwrap();
assert_eq!(
resolve_active_patch_target(&command_path),
Some(resolve_target(&cli_path))
);
}
#[test]
fn test_restore_single_file_is_noop_when_current_target_is_unpatched() {
let tmp = tempfile::NamedTempFile::new().unwrap();
std::fs::write(
tmp.path(),
br#"return["api.anthropic.com"].includes(x)}catch{return!1}"#,
)
.unwrap();
assert!(restore_single_file(tmp.path()).is_ok());
}
}
+9 -1
View File
@@ -1,6 +1,6 @@
import { useTranslation } from "react-i18next";
import type { SettingsFormState } from "@/hooks/useSettings";
import { AppWindow, MonitorUp, Power, EyeOff } from "lucide-react";
import { AppWindow, MonitorUp, Power, EyeOff, Search } from "lucide-react";
import { ToggleRow } from "@/components/ui/toggle-row";
import { AnimatePresence, motion } from "framer-motion";
@@ -66,6 +66,14 @@ export function WindowSettings({ settings, onChange }: WindowSettingsProps) {
onCheckedChange={(value) => onChange({ skipClaudeOnboarding: value })}
/>
<ToggleRow
icon={<Search className="h-4 w-4 text-amber-500" />}
title={t("settings.toolSearchBypass")}
description={t("settings.toolSearchBypassDescription")}
checked={!!settings.toolSearchBypass}
onCheckedChange={(value) => onChange({ toolSearchBypass: value })}
/>
<ToggleRow
icon={<AppWindow className="h-4 w-4 text-blue-500" />}
title={t("settings.minimizeToTray")}
+39 -1
View File
@@ -197,6 +197,44 @@ export function useSettings(): UseSettingsResult {
}
}
// Tool Search bypass: apply/restore patch when toggled
const nextToolSearchBypass = updates.toolSearchBypass;
if (
nextToolSearchBypass !== undefined &&
nextToolSearchBypass !== (data?.toolSearchBypass ?? false)
) {
try {
const results = nextToolSearchBypass
? await settingsApi.applyToolSearchPatch()
: await settingsApi.restoreToolSearchPatch();
const failed = results.find((r) => !r.success);
if (failed) {
throw new Error(failed.error ?? "Tool Search patch failed");
}
} catch (error) {
console.warn(
"[useSettings] Failed to sync Tool Search bypass",
error,
);
// Rollback: revert the setting we already saved
const rolledBack = {
...payload,
toolSearchBypass: !nextToolSearchBypass,
};
await saveMutation.mutateAsync(rolledBack);
updateSettings({ toolSearchBypass: !nextToolSearchBypass });
toast.error(
nextToolSearchBypass
? t("notifications.toolSearchPatchFailed", {
defaultValue: "Tool Search 补丁操作失败",
})
: t("notifications.toolSearchRestoreFailed", {
defaultValue: "Tool Search 恢复操作失败",
}),
);
}
}
// 持久化语言偏好
try {
if (typeof window !== "undefined" && updates.language) {
@@ -228,7 +266,7 @@ export function useSettings(): UseSettingsResult {
throw error;
}
},
[data, saveMutation, settings, t],
[data, saveMutation, settings, t, updateSettings],
);
// 完整保存设置(用于 Advanced 标签页的手动保存)
+3
View File
@@ -85,6 +85,7 @@ export function useSettingsForm(): UseSettingsFormResult {
data.enableClaudePluginIntegration ?? false,
silentStartup: data.silentStartup ?? false,
skipClaudeOnboarding: data.skipClaudeOnboarding ?? false,
toolSearchBypass: data.toolSearchBypass ?? false,
claudeConfigDir: sanitizeDir(data.claudeConfigDir),
codexConfigDir: sanitizeDir(data.codexConfigDir),
geminiConfigDir: sanitizeDir(data.geminiConfigDir),
@@ -107,6 +108,7 @@ export function useSettingsForm(): UseSettingsFormResult {
minimizeToTrayOnClose: true,
enableClaudePluginIntegration: false,
skipClaudeOnboarding: false,
toolSearchBypass: false,
language: readPersistedLanguage(),
} as SettingsFormState);
@@ -143,6 +145,7 @@ export function useSettingsForm(): UseSettingsFormResult {
serverData.enableClaudePluginIntegration ?? false,
silentStartup: serverData.silentStartup ?? false,
skipClaudeOnboarding: serverData.skipClaudeOnboarding ?? false,
toolSearchBypass: serverData.toolSearchBypass ?? false,
claudeConfigDir: sanitizeDir(serverData.claudeConfigDir),
codexConfigDir: sanitizeDir(serverData.codexConfigDir),
geminiConfigDir: sanitizeDir(serverData.geminiConfigDir),
+4
View File
@@ -166,6 +166,8 @@
"syncClaudePluginFailed": "Sync Claude plugin failed",
"skipClaudeOnboardingFailed": "Failed to skip Claude Code first-run confirmation",
"clearClaudeOnboardingSkipFailed": "Failed to restore Claude Code first-run confirmation",
"toolSearchPatchFailed": "Failed to apply Tool Search patch",
"toolSearchRestoreFailed": "Failed to restore Tool Search patch",
"updateSuccess": "Provider updated successfully",
"updateFailed": "Failed to update provider: {{error}}",
"deleteSuccess": "Provider deleted",
@@ -463,6 +465,8 @@
"enableClaudePluginIntegrationDescription": "When enabled, the VS Code Claude Code extension provider will switch with this app",
"skipClaudeOnboarding": "Skip Claude Code first-run confirmation",
"skipClaudeOnboardingDescription": "When enabled, Claude Code will skip the first-run confirmation",
"toolSearchBypass": "Bypass Tool Search domain restriction",
"toolSearchBypassDescription": "Remove the Tool Search domain whitelist in the active Claude Code installation. Auto-reapplied after that installation updates",
"appVisibility": {
"title": "Homepage Display",
"description": "Choose which apps to show on the homepage",
+4
View File
@@ -166,6 +166,8 @@
"syncClaudePluginFailed": "Claude プラグインとの同期に失敗しました",
"skipClaudeOnboardingFailed": "Claude Code の初回確認スキップに失敗しました",
"clearClaudeOnboardingSkipFailed": "Claude Code の初回確認の復元に失敗しました",
"toolSearchPatchFailed": "Tool Search パッチの適用に失敗しました",
"toolSearchRestoreFailed": "Tool Search パッチの復元に失敗しました",
"updateSuccess": "プロバイダーを更新しました",
"updateFailed": "プロバイダーの更新に失敗しました: {{error}}",
"deleteSuccess": "プロバイダーを削除しました",
@@ -463,6 +465,8 @@
"enableClaudePluginIntegrationDescription": "オンにすると VS Code の Claude Code 拡張のプロバイダーも同期します",
"skipClaudeOnboarding": "Claude Code の初回確認をスキップ",
"skipClaudeOnboardingDescription": "オンにすると Claude Code の初回インストール確認をスキップします",
"toolSearchBypass": "Tool Search ドメイン制限を解除",
"toolSearchBypassDescription": "現在アクティブな Claude Code インストールの Tool Search ドメインホワイトリスト制限を解除します。そのインストールの更新後は自動で再適用されます",
"appVisibility": {
"title": "ホームページ表示",
"description": "ホームページに表示するアプリを選択",
+4
View File
@@ -166,6 +166,8 @@
"syncClaudePluginFailed": "同步 Claude 插件失败",
"skipClaudeOnboardingFailed": "跳过 Claude Code 初次安装确认失败",
"clearClaudeOnboardingSkipFailed": "恢复 Claude Code 初次安装确认失败",
"toolSearchPatchFailed": "Tool Search 补丁操作失败",
"toolSearchRestoreFailed": "Tool Search 恢复操作失败",
"updateSuccess": "供应商更新成功",
"updateFailed": "更新供应商失败:{{error}}",
"deleteSuccess": "供应商已删除",
@@ -463,6 +465,8 @@
"enableClaudePluginIntegrationDescription": "开启后 Vscode Claude Code 插件的供应商将随本软件切换",
"skipClaudeOnboarding": "跳过 Claude Code 初次安装确认",
"skipClaudeOnboardingDescription": "开启后跳过 Claude Code 初次安装确认",
"toolSearchBypass": "解除 Tool Search 域名限制",
"toolSearchBypassDescription": "解除当前活跃 Claude Code 安装的 Tool Search 域名白名单限制。该安装更新后会自动重新应用",
"appVisibility": {
"title": "主页面显示",
"description": "选择在主页面显示的应用",
+12
View File
@@ -86,6 +86,18 @@ export const settingsApi = {
return await invoke("clear_claude_onboarding_skip");
},
async applyToolSearchPatch(): Promise<
Array<{ path: string; success: boolean; error?: string }>
> {
return await invoke("apply_toolsearch_patch");
},
async restoreToolSearchPatch(): Promise<
Array<{ path: string; success: boolean; error?: string }>
> {
return await invoke("restore_toolsearch_patch");
},
async saveFileDialog(defaultName: string): Promise<string | null> {
return await invoke("save_file_dialog", { defaultName });
},
+2
View File
@@ -226,6 +226,8 @@ export interface Settings {
enableClaudePluginIntegration?: boolean;
// 跳过 Claude Code 初次安装确认(写入 ~/.claude.json 的 hasCompletedOnboarding
skipClaudeOnboarding?: boolean;
// 解除 Tool Search 域名限制
toolSearchBypass?: boolean;
// 是否开机自启
launchOnStartup?: boolean;
// 静默启动(程序启动时不显示主窗口)