├── .node-version
├── src
├── vite-env.d.ts
├── lib
│ ├── query
│ │ ├── index.ts
│ │ └── queryClient.ts
│ ├── api
│ │ ├── types.ts
│ │ ├── index.ts
│ │ ├── deeplink.ts
│ │ ├── prompts.ts
│ │ ├── config.ts
│ │ ├── env.ts
│ │ ├── skills.ts
│ │ ├── usage.ts
│ │ ├── vscode.ts
│ │ └── providers.ts
│ ├── utils.ts
│ ├── schemas
│ │ ├── settings.ts
│ │ ├── mcp.ts
│ │ ├── provider.ts
│ │ └── common.ts
│ ├── platform.ts
│ └── errors
│ │ └── skillErrorParser.ts
├── components
│ ├── providers
│ │ ├── forms
│ │ │ ├── shared
│ │ │ │ ├── index.ts
│ │ │ │ └── EndpointField.tsx
│ │ │ ├── hooks
│ │ │ │ ├── index.ts
│ │ │ │ ├── useCodexTomlValidation.ts
│ │ │ │ ├── useApiKeyState.ts
│ │ │ │ ├── useProviderCategory.ts
│ │ │ │ ├── useCustomEndpoints.ts
│ │ │ │ └── useApiKeyLink.ts
│ │ │ ├── BasicFormFields.tsx
│ │ │ ├── ApiKeyInput.tsx
│ │ │ ├── GeminiConfigEditor.tsx
│ │ │ ├── CodexConfigEditor.tsx
│ │ │ └── CodexCommonConfigModal.tsx
│ │ ├── ProviderEmptyState.tsx
│ │ └── ProviderActions.tsx
│ ├── ui
│ │ ├── label.tsx
│ │ ├── sonner.tsx
│ │ ├── input.tsx
│ │ ├── textarea.tsx
│ │ ├── checkbox.tsx
│ │ ├── badge.tsx
│ │ ├── switch.tsx
│ │ ├── tabs.tsx
│ │ ├── card.tsx
│ │ └── button.tsx
│ ├── mode-toggle.tsx
│ ├── prompts
│ │ ├── PromptToggle.tsx
│ │ └── PromptListItem.tsx
│ ├── settings
│ │ ├── LanguageSettings.tsx
│ │ ├── ThemeSettings.tsx
│ │ └── WindowSettings.tsx
│ ├── ConfirmDialog.tsx
│ ├── UpdateBadge.tsx
│ ├── mcp
│ │ └── useMcpValidation.ts
│ └── AppSwitcher.tsx
├── env.d.ts
├── index.html
├── utils
│ ├── postChangeSync.ts
│ ├── textNormalization.ts
│ ├── uuid.ts
│ ├── providerMetaUtils.ts
│ └── formatters.ts
├── types
│ └── env.ts
├── config
│ ├── codexTemplates.ts
│ ├── geminiProviderPresets.ts
│ └── mcpPresets.ts
├── i18n
│ └── index.ts
├── hooks
│ ├── useSettingsMetadata.ts
│ ├── useMcp.ts
│ └── useSkills.ts
└── assets
│ └── icons
│ └── claude.svg
├── pnpm-workspace.yaml
├── src-tauri
├── build.rs
├── icons
│ ├── 32x32.png
│ ├── 64x64.png
│ ├── icon.icns
│ ├── icon.ico
│ ├── icon.png
│ ├── 128x128.png
│ ├── 128x128@2x.png
│ ├── StoreLogo.png
│ ├── Square30x30Logo.png
│ ├── Square44x44Logo.png
│ ├── Square71x71Logo.png
│ ├── Square89x89Logo.png
│ ├── Square107x107Logo.png
│ ├── Square142x142Logo.png
│ ├── Square150x150Logo.png
│ ├── Square284x284Logo.png
│ ├── Square310x310Logo.png
│ ├── ios
│ │ ├── AppIcon-512@2x.png
│ │ ├── AppIcon-20x20@1x.png
│ │ ├── AppIcon-20x20@2x.png
│ │ ├── AppIcon-20x20@3x.png
│ │ ├── AppIcon-29x29@1x.png
│ │ ├── AppIcon-29x29@2x.png
│ │ ├── AppIcon-29x29@3x.png
│ │ ├── AppIcon-40x40@1x.png
│ │ ├── AppIcon-40x40@2x.png
│ │ ├── AppIcon-40x40@3x.png
│ │ ├── AppIcon-60x60@2x.png
│ │ ├── AppIcon-60x60@3x.png
│ │ ├── AppIcon-76x76@1x.png
│ │ ├── AppIcon-76x76@2x.png
│ │ ├── AppIcon-20x20@2x-1.png
│ │ ├── AppIcon-29x29@2x-1.png
│ │ ├── AppIcon-40x40@2x-1.png
│ │ └── AppIcon-83.5x83.5@2x.png
│ ├── tray
│ │ └── macos
│ │ │ ├── statusTemplate.png
│ │ │ └── statusTemplate@2x.png
│ └── android
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.png
│ │ ├── ic_launcher_round.png
│ │ └── ic_launcher_foreground.png
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.png
│ │ ├── ic_launcher_round.png
│ │ └── ic_launcher_foreground.png
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.png
│ │ ├── ic_launcher_round.png
│ │ └── ic_launcher_foreground.png
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.png
│ │ ├── ic_launcher_round.png
│ │ └── ic_launcher_foreground.png
│ │ └── mipmap-xxxhdpi
│ │ ├── ic_launcher.png
│ │ ├── ic_launcher_round.png
│ │ └── ic_launcher_foreground.png
├── .gitignore
├── src
│ ├── main.rs
│ ├── services
│ │ └── mod.rs
│ ├── commands
│ │ ├── mod.rs
│ │ ├── env.rs
│ │ ├── deeplink.rs
│ │ ├── settings.rs
│ │ ├── plugin.rs
│ │ ├── prompt.rs
│ │ └── misc.rs
│ ├── prompt.rs
│ ├── web_api
│ │ └── handlers
│ │ │ ├── settings.rs
│ │ │ ├── system.rs
│ │ │ ├── health.rs
│ │ │ ├── mod.rs
│ │ │ └── prompts.rs
│ ├── store.rs
│ ├── init_status.rs
│ └── prompt_files.rs
├── capabilities
│ └── default.json
├── Info.plist
├── tests
│ ├── app_type_parse.rs
│ └── support.rs
└── tauri.conf.json
├── assets
├── screenshots
│ ├── add-en.png
│ ├── add-zh.png
│ ├── main-en.png
│ ├── main-zh.png
│ ├── web-prompt.png
│ ├── web-skills.png
│ └── web-settings.png
└── partners
│ ├── banners
│ ├── glm-en.jpg
│ └── glm-zh.jpg
│ └── logos
│ └── packycode.png
├── tests
├── msw
│ ├── server.ts
│ └── tauriMocks.ts
├── utils
│ ├── testQueryClient.ts
│ ├── uuid.test.ts
│ └── providerMetaUtils.test.ts
├── api
│ ├── test-auth.sh
│ ├── test-settings.sh
│ ├── test-mcp.sh
│ └── test-usage.sh
├── components
│ └── ApiKeySection.test.tsx
├── run-all.sh
├── setupTests.ts
├── lib
│ └── providerSchema.test.ts
├── integration
│ ├── test-persistence.sh
│ └── test-full-workflow.sh
├── hooks
│ └── useSettingsMetadata.test.tsx
├── config
│ └── healthCheckMapping.test.ts
└── helpers
│ └── test-data.json
├── docs
├── roadmap.md
├── web-parity-plan.md
└── TEST_DEVELOPMENT_PLAN.md
├── .dockerignore
├── .gitignore
├── ssh-tunnel.sh
├── tsconfig.node.json
├── vitest.config.ts
├── components.json
├── vite.config.mts
├── tsconfig.json
├── vite.config.web.mts
├── Dockerfile.release
├── .gitattributes
├── LICENSE
├── Dockerfile
├── README_i18n.md
├── tailwind.config.js
└── scripts
└── docker-deploy.sh
/.node-version:
--------------------------------------------------------------------------------
1 | v22.4.1
2 |
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | export {};
4 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages: []
2 |
3 | onlyBuiltDependencies:
4 | - '@tailwindcss/oxide'
5 |
--------------------------------------------------------------------------------
/src-tauri/build.rs:
--------------------------------------------------------------------------------
1 | fn main() {
2 | #[cfg(feature = "desktop")]
3 | tauri_build::build();
4 | }
5 |
--------------------------------------------------------------------------------
/src-tauri/icons/32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/src-tauri/icons/32x32.png
--------------------------------------------------------------------------------
/src-tauri/icons/64x64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/src-tauri/icons/64x64.png
--------------------------------------------------------------------------------
/src-tauri/icons/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/src-tauri/icons/icon.icns
--------------------------------------------------------------------------------
/src-tauri/icons/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/src-tauri/icons/icon.ico
--------------------------------------------------------------------------------
/src-tauri/icons/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/src-tauri/icons/icon.png
--------------------------------------------------------------------------------
/src-tauri/icons/128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/src-tauri/icons/128x128.png
--------------------------------------------------------------------------------
/assets/screenshots/add-en.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/assets/screenshots/add-en.png
--------------------------------------------------------------------------------
/assets/screenshots/add-zh.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/assets/screenshots/add-zh.png
--------------------------------------------------------------------------------
/assets/screenshots/main-en.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/assets/screenshots/main-en.png
--------------------------------------------------------------------------------
/assets/screenshots/main-zh.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/assets/screenshots/main-zh.png
--------------------------------------------------------------------------------
/src-tauri/icons/128x128@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/src-tauri/icons/128x128@2x.png
--------------------------------------------------------------------------------
/src-tauri/icons/StoreLogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/src-tauri/icons/StoreLogo.png
--------------------------------------------------------------------------------
/assets/screenshots/web-prompt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/assets/screenshots/web-prompt.png
--------------------------------------------------------------------------------
/assets/screenshots/web-skills.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/assets/screenshots/web-skills.png
--------------------------------------------------------------------------------
/src-tauri/.gitignore:
--------------------------------------------------------------------------------
1 | # Generated by Cargo
2 | # will have compiled files and executables
3 | /target/
4 | /gen/schemas
5 |
--------------------------------------------------------------------------------
/src/lib/query/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./queryClient";
2 | export * from "./queries";
3 | export * from "./mutations";
4 |
--------------------------------------------------------------------------------
/assets/partners/banners/glm-en.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/assets/partners/banners/glm-en.jpg
--------------------------------------------------------------------------------
/assets/partners/banners/glm-zh.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/assets/partners/banners/glm-zh.jpg
--------------------------------------------------------------------------------
/assets/partners/logos/packycode.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/assets/partners/logos/packycode.png
--------------------------------------------------------------------------------
/assets/screenshots/web-settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/assets/screenshots/web-settings.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square30x30Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/src-tauri/icons/Square30x30Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square44x44Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/src-tauri/icons/Square44x44Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square71x71Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/src-tauri/icons/Square71x71Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square89x89Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/src-tauri/icons/Square89x89Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square107x107Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/src-tauri/icons/Square107x107Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square142x142Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/src-tauri/icons/Square142x142Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square150x150Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/src-tauri/icons/Square150x150Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square284x284Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/src-tauri/icons/Square284x284Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square310x310Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/src-tauri/icons/Square310x310Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-512@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/src-tauri/icons/ios/AppIcon-512@2x.png
--------------------------------------------------------------------------------
/src/lib/api/types.ts:
--------------------------------------------------------------------------------
1 | // 前端统一使用 AppId 作为应用标识(与后端命令参数 `app` 一致)
2 | export type AppId = "claude" | "codex" | "gemini"; // 新增 gemini
3 |
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-20x20@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/src-tauri/icons/ios/AppIcon-20x20@1x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-20x20@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/src-tauri/icons/ios/AppIcon-20x20@2x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-20x20@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/src-tauri/icons/ios/AppIcon-20x20@3x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-29x29@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/src-tauri/icons/ios/AppIcon-29x29@1x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-29x29@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/src-tauri/icons/ios/AppIcon-29x29@2x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-29x29@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/src-tauri/icons/ios/AppIcon-29x29@3x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-40x40@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/src-tauri/icons/ios/AppIcon-40x40@1x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-40x40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/src-tauri/icons/ios/AppIcon-40x40@2x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-40x40@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/src-tauri/icons/ios/AppIcon-40x40@3x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-60x60@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/src-tauri/icons/ios/AppIcon-60x60@2x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-60x60@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/src-tauri/icons/ios/AppIcon-60x60@3x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-76x76@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/src-tauri/icons/ios/AppIcon-76x76@1x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-76x76@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/src-tauri/icons/ios/AppIcon-76x76@2x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-20x20@2x-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/src-tauri/icons/ios/AppIcon-20x20@2x-1.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-29x29@2x-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/src-tauri/icons/ios/AppIcon-29x29@2x-1.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-40x40@2x-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/src-tauri/icons/ios/AppIcon-40x40@2x-1.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png
--------------------------------------------------------------------------------
/src-tauri/icons/tray/macos/statusTemplate.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/src-tauri/icons/tray/macos/statusTemplate.png
--------------------------------------------------------------------------------
/src-tauri/icons/tray/macos/statusTemplate@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/src-tauri/icons/tray/macos/statusTemplate@2x.png
--------------------------------------------------------------------------------
/src/components/providers/forms/shared/index.ts:
--------------------------------------------------------------------------------
1 | export { ApiKeySection } from "./ApiKeySection";
2 | export { EndpointField } from "./EndpointField";
3 |
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/tests/msw/server.ts:
--------------------------------------------------------------------------------
1 | import { setupServer } from "msw/node";
2 | import { handlers } from "./handlers";
3 |
4 | export const server = setupServer(...handlers);
5 |
6 |
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/docs/roadmap.md:
--------------------------------------------------------------------------------
1 | - 自动升级自定义路径 ✅
2 | - win 绿色版报毒问题 ✅
3 | - mcp 管理器 ✅
4 | - i18n ✅
5 | - gemini cli
6 | - homebrew 支持 ✅
7 | - memory 管理
8 | - codex 更多预设供应商
9 | - 云同步
10 | - 本地代理
11 |
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Laliet/CC-Switch-Web/HEAD/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/src/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | declare global {
4 | interface ImportMetaEnv {
5 | readonly VITE_MODE?: string;
6 | }
7 | }
8 |
9 | export {};
10 |
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx";
2 | import { twMerge } from "tailwind-merge";
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
--------------------------------------------------------------------------------
/src-tauri/src/main.rs:
--------------------------------------------------------------------------------
1 | // Prevents additional console window on Windows in release, DO NOT REMOVE!!
2 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
3 |
4 | fn main() {
5 | cc_switch_lib::run();
6 | }
7 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | dist-web
4 | target
5 | src-tauri/target
6 | .git
7 | .gitignore
8 | .github
9 | .vscode
10 | .idea
11 | .DS_Store
12 | *.log
13 | npm-debug.log*
14 | pnpm-debug.log*
15 | coverage
16 | .env
17 | .env.*
18 |
--------------------------------------------------------------------------------
/tests/utils/testQueryClient.ts:
--------------------------------------------------------------------------------
1 | import { QueryClient } from "@tanstack/react-query";
2 |
3 | export const createTestQueryClient = () =>
4 | new QueryClient({
5 | defaultOptions: {
6 | queries: {
7 | retry: false,
8 | },
9 | },
10 | });
11 |
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | dist/
3 | dist-web/
4 | release/
5 | target/
6 | src-tauri/target/
7 | .DS_Store
8 | *.log
9 | *.tmp
10 | .env
11 | .env.local
12 | *.tsbuildinfo
13 | .npmrc
14 | *.AppImage
15 | CLAUDE.md
16 | AGENTS.md
17 | GEMINI.md
18 | /.claude
19 | /.codex
20 | /.gemini
21 | /.cc-switch
22 | /.idea
23 | /.vscode
24 | vitest-report.json
25 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Claude Code 供应商切换器
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/lib/query/queryClient.ts:
--------------------------------------------------------------------------------
1 | import { QueryClient } from "@tanstack/react-query";
2 |
3 | export const queryClient = new QueryClient({
4 | defaultOptions: {
5 | queries: {
6 | retry: 1,
7 | refetchOnWindowFocus: false,
8 | staleTime: 1000 * 60 * 5,
9 | },
10 | mutations: {
11 | retry: false,
12 | },
13 | },
14 | });
15 |
--------------------------------------------------------------------------------
/ssh-tunnel.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # CC-Switch SSH 隧道脚本
3 | # 在本地电脑执行此命令
4 |
5 | echo "正在建立 SSH 隧道到 CC-Switch Web 服务器..."
6 | echo "服务器: <替换为你的服务器主机/IP>"
7 | echo "本地端口: 3000"
8 | echo "远程端口: 8080"
9 | echo ""
10 | echo "隧道建立后,请在浏览器访问: http://localhost:3000"
11 | echo "按 Ctrl+C 断开隧道"
12 | echo ""
13 |
14 | echo "示例命令: ssh -N -L 3000:localhost:8080 user@your-server"
15 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "allowSyntheticDefaultImports": true,
8 | "resolveJsonModule": true,
9 | "esModuleInterop": true,
10 | "target": "ES2020",
11 | "strict": true,
12 | "types": ["node"]
13 | },
14 | "include": ["vite.config.mts", "vitest.config.ts"]
15 | }
16 |
--------------------------------------------------------------------------------
/src-tauri/capabilities/default.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "../gen/schemas/desktop-schema.json",
3 | "identifier": "default",
4 | "description": "enables the default permissions",
5 | "windows": [
6 | "main"
7 | ],
8 | "permissions": [
9 | "core:default",
10 | "opener:default",
11 | "updater:default",
12 | "core:window:allow-set-skip-taskbar",
13 | "process:allow-restart",
14 | "dialog:default"
15 | ]
16 | }
17 |
--------------------------------------------------------------------------------
/src-tauri/src/services/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod config;
2 | pub mod env_checker;
3 | pub mod env_manager;
4 | pub mod mcp;
5 | pub mod prompt;
6 | pub mod provider;
7 | pub mod skill;
8 | pub mod speedtest;
9 |
10 | pub use config::ConfigService;
11 | pub use mcp::McpService;
12 | pub use prompt::PromptService;
13 | pub use provider::{ProviderService, ProviderSortUpdate};
14 | pub use skill::{Skill, SkillRepo, SkillService};
15 | pub use speedtest::{EndpointLatency, SpeedtestService};
16 |
--------------------------------------------------------------------------------
/src-tauri/src/commands/mod.rs:
--------------------------------------------------------------------------------
1 | #![allow(non_snake_case)]
2 |
3 | mod config;
4 | mod deeplink;
5 | mod env;
6 | mod import_export;
7 | mod mcp;
8 | mod misc;
9 | mod plugin;
10 | mod prompt;
11 | mod provider;
12 | mod settings;
13 | pub mod skill;
14 |
15 | pub use config::*;
16 | pub use deeplink::*;
17 | pub use env::*;
18 | pub use import_export::*;
19 | pub use mcp::*;
20 | pub use misc::*;
21 | pub use plugin::*;
22 | pub use prompt::*;
23 | pub use provider::*;
24 | pub use settings::*;
25 | pub use skill::*;
26 |
--------------------------------------------------------------------------------
/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import path from "node:path";
2 | import { defineConfig } from "vitest/config";
3 | import react from "@vitejs/plugin-react";
4 |
5 | export default defineConfig({
6 | plugins: [react()],
7 | resolve: {
8 | alias: {
9 | "@": path.resolve(__dirname, "./src"),
10 | },
11 | },
12 | test: {
13 | environment: "jsdom",
14 | setupFiles: ["./tests/setupTests.ts"],
15 | globals: true,
16 | coverage: {
17 | reporter: ["text", "lcov"],
18 | },
19 | },
20 | });
21 |
--------------------------------------------------------------------------------
/src/utils/postChangeSync.ts:
--------------------------------------------------------------------------------
1 | import { settingsApi } from "@/lib/api";
2 |
3 | /**
4 | * 统一的“后置同步”工具:将当前使用的供应商写回对应应用的 live 配置。
5 | * 不抛出异常,由调用方根据返回值决定提示策略。
6 | */
7 | export async function syncCurrentProvidersLiveSafe(): Promise<{
8 | ok: boolean;
9 | error?: Error;
10 | }> {
11 | try {
12 | await settingsApi.syncCurrentProvidersLive();
13 | return { ok: true };
14 | } catch (err) {
15 | const error = err instanceof Error ? err : new Error(String(err ?? ""));
16 | return { ok: false, error };
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": false,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.js",
8 | "css": "src/index.css",
9 | "baseColor": "neutral",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "iconLibrary": "lucide",
14 | "aliases": {
15 | "components": "@/components",
16 | "utils": "@/lib/utils",
17 | "ui": "@/components/ui",
18 | "lib": "@/lib",
19 | "hooks": "@/hooks"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src-tauri/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | CFBundleURLTypes
7 |
8 |
9 | CFBundleURLName
10 | CC Switch Deep Link
11 | CFBundleURLSchemes
12 |
13 | ccswitch
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/lib/api/index.ts:
--------------------------------------------------------------------------------
1 | export type { AppId } from "./types";
2 | export { providersApi } from "./providers";
3 | export { settingsApi } from "./settings";
4 | export { mcpApi } from "./mcp";
5 | export { promptsApi } from "./prompts";
6 | export { usageApi } from "./usage";
7 | export { vscodeApi } from "./vscode";
8 | export { healthCheckApi } from "./healthCheck";
9 | export * as configApi from "./config";
10 | export type { ProviderSwitchEvent } from "./providers";
11 | export type { Prompt } from "./prompts";
12 | export type { HealthStatus, ProviderHealth } from "./healthCheck";
13 |
--------------------------------------------------------------------------------
/src/types/env.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 环境变量冲突检测相关类型定义
3 | */
4 |
5 | /**
6 | * 环境变量冲突信息
7 | */
8 | export interface EnvConflict {
9 | /** 环境变量名称 */
10 | varName: string;
11 | /** 环境变量的值 */
12 | varValue: string;
13 | /** 来源类型: "system" 表示系统环境变量, "file" 表示配置文件 */
14 | sourceType: "system" | "file";
15 | /** 来源路径 (注册表路径或文件路径:行号) */
16 | sourcePath: string;
17 | }
18 |
19 | /**
20 | * 备份信息
21 | */
22 | export interface BackupInfo {
23 | /** 备份文件路径 */
24 | backupPath: string;
25 | /** 备份时间戳 */
26 | timestamp: string;
27 | /** 被备份的环境变量冲突列表 */
28 | conflicts: EnvConflict[];
29 | }
30 |
--------------------------------------------------------------------------------
/src-tauri/src/prompt.rs:
--------------------------------------------------------------------------------
1 | use serde::{Deserialize, Serialize};
2 |
3 | #[derive(Debug, Clone, Serialize, Deserialize)]
4 | pub struct Prompt {
5 | pub id: String,
6 | pub name: String,
7 | pub content: String,
8 | #[serde(skip_serializing_if = "Option::is_none")]
9 | pub description: Option,
10 | #[serde(default)]
11 | pub enabled: bool,
12 | #[serde(rename = "createdAt", skip_serializing_if = "Option::is_none")]
13 | pub created_at: Option,
14 | #[serde(rename = "updatedAt", skip_serializing_if = "Option::is_none")]
15 | pub updated_at: Option,
16 | }
17 |
--------------------------------------------------------------------------------
/src/utils/textNormalization.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 将常见的中文/全角/弯引号统一为 ASCII 引号,以避免 TOML 解析失败。
3 | * - 双引号:” “ „ ‟ " → "
4 | * - 单引号:’ ‘ ' → '
5 | * 保守起见,不替换书名号/角引号(《》、「」等),避免误伤内容语义。
6 | */
7 | export const normalizeQuotes = (text: string): string => {
8 | if (!text) return text;
9 | return (
10 | text
11 | // 双引号族 → "
12 | .replace(/[“”„‟"]/g, '"')
13 | // 单引号族 → '
14 | .replace(/[‘’']/g, "'")
15 | );
16 | };
17 |
18 | /**
19 | * 专用于 TOML 文本的归一化;目前等同于 normalizeQuotes,后续可扩展(如空白、行尾等)。
20 | */
21 | export const normalizeTomlText = (text: string): string =>
22 | normalizeQuotes(text);
23 |
--------------------------------------------------------------------------------
/vite.config.mts:
--------------------------------------------------------------------------------
1 | import path from "node:path";
2 | import { defineConfig } from "vite";
3 | import react from "@vitejs/plugin-react";
4 | import tailwindcss from "@tailwindcss/vite";
5 |
6 | export default defineConfig({
7 | root: "src",
8 | plugins: [react(), tailwindcss()],
9 | base: "./",
10 | build: {
11 | outDir: "../dist",
12 | emptyOutDir: true,
13 | },
14 | server: {
15 | port: 3000,
16 | strictPort: true,
17 | },
18 | resolve: {
19 | alias: {
20 | "@": path.resolve(__dirname, "./src"),
21 | },
22 | },
23 | clearScreen: false,
24 | envPrefix: ["VITE_", "TAURI_"],
25 | });
26 |
--------------------------------------------------------------------------------
/src-tauri/src/web_api/handlers/settings.rs:
--------------------------------------------------------------------------------
1 | #![cfg(feature = "web-server")]
2 |
3 | use std::sync::Arc;
4 |
5 | use axum::{extract::State, Json};
6 |
7 | use crate::{settings, settings::AppSettings, store::AppState};
8 |
9 | use super::{ApiError, ApiResult};
10 |
11 | pub async fn get_settings(State(_state): State>) -> ApiResult {
12 | Ok(Json(settings::get_settings()))
13 | }
14 |
15 | pub async fn save_settings(
16 | State(_state): State>,
17 | Json(settings): Json,
18 | ) -> ApiResult {
19 | settings::update_settings(settings).map_err(ApiError::from)?;
20 | Ok(Json(true))
21 | }
22 |
--------------------------------------------------------------------------------
/src/config/codexTemplates.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Codex 配置模板
3 | * 用于新建自定义供应商时的默认配置
4 | */
5 |
6 | export interface CodexTemplate {
7 | auth: Record;
8 | config: string;
9 | }
10 |
11 | /**
12 | * 获取 Codex 自定义模板
13 | * @returns Codex 模板配置
14 | */
15 | export function getCodexCustomTemplate(): CodexTemplate {
16 | const config = `model_provider = "custom"
17 | model = "gpt-5-codex"
18 | model_reasoning_effort = "high"
19 | disable_response_storage = true
20 |
21 | [model_providers.custom]
22 | name = "custom"
23 | wire_api = "responses"
24 | requires_openai_auth = true`;
25 |
26 | return {
27 | auth: { OPENAI_API_KEY: "" },
28 | config,
29 | };
30 | }
31 |
--------------------------------------------------------------------------------
/src/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as LabelPrimitive from "@radix-ui/react-label";
3 | import { cn } from "@/lib/utils";
4 |
5 | const Label = React.forwardRef<
6 | React.ElementRef,
7 | React.ComponentPropsWithoutRef
8 | >(({ className, ...props }, ref) => (
9 |
17 | ));
18 | Label.displayName = LabelPrimitive.Root.displayName;
19 |
20 | export { Label };
21 |
--------------------------------------------------------------------------------
/src-tauri/src/store.rs:
--------------------------------------------------------------------------------
1 | use crate::app_config::MultiAppConfig;
2 | use crate::error::AppError;
3 | use std::sync::RwLock;
4 |
5 | /// 全局应用状态
6 | pub struct AppState {
7 | pub config: RwLock,
8 | }
9 |
10 | impl AppState {
11 | /// 创建新的应用状态
12 | /// 注意:仅在配置成功加载时返回;不会在失败时回退默认值。
13 | pub fn try_new() -> Result {
14 | let config = MultiAppConfig::load()?;
15 | Ok(Self {
16 | config: RwLock::new(config),
17 | })
18 | }
19 |
20 | /// 保存配置到文件
21 | pub fn save(&self) -> Result<(), AppError> {
22 | let config = self.config.read().map_err(AppError::from)?;
23 |
24 | config.save()
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
5 | "module": "ESNext",
6 | "skipLibCheck": true,
7 | "moduleResolution": "bundler",
8 | "allowImportingTsExtensions": true,
9 | "resolveJsonModule": true,
10 | "isolatedModules": true,
11 | "noEmit": true,
12 | "jsx": "react-jsx",
13 | "strict": true,
14 | "noUnusedLocals": true,
15 | "noUnusedParameters": true,
16 | "noFallthroughCasesInSwitch": true,
17 | "baseUrl": ".",
18 | "paths": {
19 | "@/*": ["src/*"]
20 | },
21 | "types": ["vitest/globals"]
22 | },
23 | "include": ["src/**/*", "tests/**/*"],
24 | "references": [{ "path": "./tsconfig.node.json" }]
25 | }
26 |
--------------------------------------------------------------------------------
/src/lib/schemas/settings.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | const directorySchema = z
4 | .string()
5 | .trim()
6 | .min(1, "路径不能为空")
7 | .optional()
8 | .or(z.literal(""));
9 |
10 | export const settingsSchema = z.object({
11 | showInTray: z.boolean(),
12 | minimizeToTrayOnClose: z.boolean(),
13 | enableClaudePluginIntegration: z.boolean().optional(),
14 | claudeConfigDir: directorySchema.nullable().optional(),
15 | codexConfigDir: directorySchema.nullable().optional(),
16 | language: z.enum(["en", "zh"]).optional(),
17 | customEndpointsClaude: z.record(z.string(), z.unknown()).optional(),
18 | customEndpointsCodex: z.record(z.string(), z.unknown()).optional(),
19 | });
20 |
21 | export type SettingsFormData = z.infer;
22 |
--------------------------------------------------------------------------------
/src-tauri/tests/app_type_parse.rs:
--------------------------------------------------------------------------------
1 | use std::str::FromStr;
2 |
3 | use cc_switch_lib::AppType;
4 |
5 | #[test]
6 | fn parse_known_apps_case_insensitive_and_trim() {
7 | assert!(matches!(AppType::from_str("claude"), Ok(AppType::Claude)));
8 | assert!(matches!(AppType::from_str("codex"), Ok(AppType::Codex)));
9 | assert!(matches!(
10 | AppType::from_str(" ClAuDe \n"),
11 | Ok(AppType::Claude)
12 | ));
13 | assert!(matches!(AppType::from_str("\tcoDeX\t"), Ok(AppType::Codex)));
14 | }
15 |
16 | #[test]
17 | fn parse_unknown_app_returns_localized_error_message() {
18 | let err = AppType::from_str("unknown").unwrap_err();
19 | let msg = err.to_string();
20 | assert!(msg.contains("可选值") || msg.contains("Allowed"));
21 | assert!(msg.contains("unknown"));
22 | }
23 |
--------------------------------------------------------------------------------
/src-tauri/src/commands/env.rs:
--------------------------------------------------------------------------------
1 | use crate::services::env_checker::{check_env_conflicts as check_conflicts, EnvConflict};
2 | use crate::services::env_manager::{
3 | delete_env_vars as delete_vars, restore_from_backup, BackupInfo,
4 | };
5 |
6 | /// Check environment variable conflicts for a specific app
7 | #[tauri::command]
8 | pub fn check_env_conflicts(app: String) -> Result, String> {
9 | check_conflicts(&app)
10 | }
11 |
12 | /// Delete environment variables with backup
13 | #[tauri::command]
14 | pub fn delete_env_vars(conflicts: Vec) -> Result {
15 | delete_vars(conflicts)
16 | }
17 |
18 | /// Restore environment variables from backup file
19 | #[tauri::command]
20 | pub fn restore_env_backup(backup_path: String) -> Result<(), String> {
21 | restore_from_backup(backup_path)
22 | }
23 |
--------------------------------------------------------------------------------
/vite.config.web.mts:
--------------------------------------------------------------------------------
1 | import path from "node:path";
2 | import { defineConfig } from "vite";
3 | import react from "@vitejs/plugin-react";
4 | import tailwindcss from "@tailwindcss/vite";
5 |
6 | export default defineConfig({
7 | root: "src",
8 | plugins: [react(), tailwindcss()],
9 | base: "/",
10 | build: {
11 | outDir: "../dist-web",
12 | emptyOutDir: true,
13 | },
14 | server: {
15 | port: 4173,
16 | strictPort: true,
17 | proxy: {
18 | "/api": {
19 | target: "http://localhost:3000",
20 | changeOrigin: true,
21 | },
22 | },
23 | },
24 | resolve: {
25 | alias: {
26 | "@": path.resolve(__dirname, "./src"),
27 | },
28 | },
29 | define: {
30 | "import.meta.env.VITE_MODE": JSON.stringify("web"),
31 | },
32 | clearScreen: false,
33 | envPrefix: ["VITE_"],
34 | });
35 |
--------------------------------------------------------------------------------
/Dockerfile.release:
--------------------------------------------------------------------------------
1 | # syntax=docker/dockerfile:1.7
2 | FROM debian:bookworm-slim
3 |
4 | ARG TARGETARCH
5 |
6 | RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates libssl3 \
7 | && rm -rf /var/lib/apt/lists/*
8 |
9 | RUN useradd -m ccswitch
10 |
11 | RUN --mount=type=bind,source=release-assets,target=/release-assets,readonly \
12 | set -eux; \
13 | case "${TARGETARCH}" in \
14 | amd64) install -m 0755 /release-assets/cc-switch-server-linux-x86_64 /usr/local/bin/cc-switch-server ;; \
15 | arm64) install -m 0755 /release-assets/cc-switch-server-linux-aarch64 /usr/local/bin/cc-switch-server ;; \
16 | *) echo "Unsupported TARGETARCH: ${TARGETARCH}" >&2; exit 1 ;; \
17 | esac
18 |
19 | USER ccswitch
20 | WORKDIR /home/ccswitch
21 | ENV HOST=0.0.0.0 PORT=3000
22 | EXPOSE 3000
23 | CMD ["cc-switch-server"]
24 |
--------------------------------------------------------------------------------
/src/lib/platform.ts:
--------------------------------------------------------------------------------
1 | // 轻量平台检测,避免在 SSR 或无 navigator 的环境报错
2 | export const isMac = (): boolean => {
3 | try {
4 | const ua = navigator.userAgent || "";
5 | const plat = (navigator.platform || "").toLowerCase();
6 | return /mac/i.test(ua) || plat.includes("mac");
7 | } catch {
8 | return false;
9 | }
10 | };
11 |
12 | export const isWindows = (): boolean => {
13 | try {
14 | const ua = navigator.userAgent || "";
15 | return /windows|win32|win64/i.test(ua);
16 | } catch {
17 | return false;
18 | }
19 | };
20 |
21 | export const isLinux = (): boolean => {
22 | try {
23 | const ua = navigator.userAgent || "";
24 | // WebKitGTK/Chromium 在 Linux/Wayland/X11 下 UA 通常包含 Linux 或 X11
25 | return (
26 | /linux|x11/i.test(ua) && !/android/i.test(ua) && !isMac() && !isWindows()
27 | );
28 | } catch {
29 | return false;
30 | }
31 | };
32 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
4 | # Explicitly declare text files you want to always be normalized and converted
5 | # to native line endings on checkout.
6 | *.rs text eol=lf
7 | *.toml text eol=lf
8 | *.json text eol=lf
9 | *.md text eol=lf
10 | *.yml text eol=lf
11 | *.yaml text eol=lf
12 | *.txt text eol=lf
13 |
14 | # TypeScript/JavaScript files
15 | *.ts text eol=lf
16 | *.tsx text eol=lf
17 | *.js text eol=lf
18 | *.jsx text eol=lf
19 |
20 | # HTML/CSS files
21 | *.html text eol=lf
22 | *.css text eol=lf
23 | *.scss text eol=lf
24 |
25 | # Shell scripts
26 | *.sh text eol=lf
27 |
28 | # Denote all files that are truly binary and should not be modified.
29 | *.png binary
30 | *.jpg binary
31 | *.jpeg binary
32 | *.gif binary
33 | *.ico binary
34 | *.woff binary
35 | *.woff2 binary
36 | *.ttf binary
37 | *.exe binary
38 | *.dll binary
--------------------------------------------------------------------------------
/src/components/ui/sonner.tsx:
--------------------------------------------------------------------------------
1 | import { Toaster as SonnerToaster } from "sonner";
2 |
3 | export function Toaster() {
4 | return (
5 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/src/components/providers/forms/hooks/index.ts:
--------------------------------------------------------------------------------
1 | export { useProviderCategory } from "./useProviderCategory";
2 | export { useApiKeyState } from "./useApiKeyState";
3 | export { useBaseUrlState } from "./useBaseUrlState";
4 | export { useModelState } from "./useModelState";
5 | export { useCodexConfigState } from "./useCodexConfigState";
6 | export { useApiKeyLink } from "./useApiKeyLink";
7 | export { useCustomEndpoints } from "./useCustomEndpoints";
8 | export { useTemplateValues } from "./useTemplateValues";
9 | export { useCommonConfigSnippet } from "./useCommonConfigSnippet";
10 | export { useCodexCommonConfig } from "./useCodexCommonConfig";
11 | export { useSpeedTestEndpoints } from "./useSpeedTestEndpoints";
12 | export { useCodexTomlValidation } from "./useCodexTomlValidation";
13 | export { useGeminiConfigState } from "./useGeminiConfigState";
14 | export { useGeminiCommonConfig } from "./useGeminiCommonConfig";
15 |
--------------------------------------------------------------------------------
/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { cn } from "@/lib/utils";
3 |
4 | export type InputProps = React.InputHTMLAttributes;
5 |
6 | const Input = React.forwardRef(
7 | ({ className, type, ...props }, ref) => {
8 | return (
9 |
18 | );
19 | },
20 | );
21 | Input.displayName = "Input";
22 |
23 | export { Input };
24 |
--------------------------------------------------------------------------------
/tests/api/test-auth.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | # Authentication coverage for CC-Switch web API.
3 |
4 | set -o pipefail
5 |
6 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
7 | source "$SCRIPT_DIR/../helpers/common.sh"
8 |
9 | require_command curl
10 |
11 | log_step "Auth: request without credentials"
12 | no_auth_status=$(curl -s -o /dev/null -w "%{http_code}" -X POST "${API_BASE}/config/export")
13 | assert_status_code 401 "$no_auth_status" "Unauthenticated requests are rejected"
14 |
15 | log_step "Auth: request with wrong password"
16 | bad_pwd_status=$(curl -s -o /dev/null -w "%{http_code}" -u "$USERNAME:wrong-password" -X POST "${API_BASE}/config/export")
17 | assert_status_code 401 "$bad_pwd_status" "Wrong password is rejected"
18 |
19 | log_step "Auth: request with correct credentials"
20 | api_post "/config/export" >/dev/null
21 | assert_status_code 200 "$LAST_STATUS" "Authorized request succeeds"
22 |
23 | print_summary
24 | exit $?
25 |
--------------------------------------------------------------------------------
/src/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { cn } from "@/lib/utils";
3 |
4 | export type TextareaProps = React.TextareaHTMLAttributes;
5 |
6 | const Textarea = React.forwardRef(
7 | ({ className, ...props }, ref) => {
8 | return (
9 |
21 | );
22 | },
23 | );
24 | Textarea.displayName = "Textarea";
25 |
26 | export { Textarea };
27 |
--------------------------------------------------------------------------------
/src/components/mode-toggle.tsx:
--------------------------------------------------------------------------------
1 | import { Moon, Sun } from "lucide-react";
2 | import { useTranslation } from "react-i18next";
3 | import { Button } from "@/components/ui/button";
4 | import { useTheme } from "@/components/theme-provider";
5 |
6 | export function ModeToggle() {
7 | const { theme, setTheme } = useTheme();
8 | const { t } = useTranslation();
9 |
10 | const toggleTheme = () => {
11 | // 如果当前是 dark 或 system(且系统是暗色),切换到 light
12 | // 否则切换到 dark
13 | if (theme === "dark") {
14 | setTheme("light");
15 | } else {
16 | setTheme("dark");
17 | }
18 | };
19 |
20 | return (
21 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/src/lib/api/deeplink.ts:
--------------------------------------------------------------------------------
1 | import { invoke } from "./adapter";
2 |
3 | export interface DeepLinkImportRequest {
4 | version: string;
5 | resource: string;
6 | app: "claude" | "codex" | "gemini";
7 | name: string;
8 | homepage: string;
9 | endpoint: string;
10 | apiKey: string;
11 | model?: string;
12 | notes?: string;
13 | }
14 |
15 | export const deeplinkApi = {
16 | /**
17 | * Parse a deep link URL
18 | * @param url The ccswitch:// URL to parse
19 | * @returns Parsed deep link request
20 | */
21 | parseDeeplink: async (url: string): Promise => {
22 | return invoke("parse_deeplink", { url });
23 | },
24 |
25 | /**
26 | * Import a provider from a deep link request
27 | * @param request The deep link import request
28 | * @returns The ID of the imported provider
29 | */
30 | importFromDeeplink: async (
31 | request: DeepLinkImportRequest,
32 | ): Promise => {
33 | return invoke("import_from_deeplink", { request });
34 | },
35 | };
36 |
--------------------------------------------------------------------------------
/tests/components/ApiKeySection.test.tsx:
--------------------------------------------------------------------------------
1 | import { render, screen } from "@testing-library/react";
2 | import { describe, expect, it } from "vitest";
3 | import { ApiKeySection } from "@/components/providers/forms/shared";
4 |
5 | describe("ApiKeySection websiteUrl safety", () => {
6 | it("renders an external link only for http/https", () => {
7 | const { rerender } = render(
8 | {}}
11 | shouldShowLink
12 | websiteUrl=" https://example.com/get-key "
13 | />,
14 | );
15 |
16 | expect(
17 | screen.getByRole("link", { name: /获取 API Key/i }),
18 | ).toHaveAttribute("href", "https://example.com/get-key");
19 |
20 | rerender(
21 | {}}
24 | shouldShowLink
25 | websiteUrl="javascript:alert(1)"
26 | />,
27 | );
28 |
29 | expect(screen.queryByRole("link", { name: /获取 API Key/i })).toBeNull();
30 | });
31 | });
32 |
33 |
--------------------------------------------------------------------------------
/src-tauri/src/commands/deeplink.rs:
--------------------------------------------------------------------------------
1 | use crate::deeplink::{import_provider_from_deeplink, parse_deeplink_url, DeepLinkImportRequest};
2 | use crate::store::AppState;
3 | use tauri::State;
4 |
5 | /// Parse a deep link URL and return the parsed request for frontend confirmation
6 | #[tauri::command]
7 | pub fn parse_deeplink(url: String) -> Result {
8 | log::info!("Parsing deep link URL: {url}");
9 | parse_deeplink_url(&url).map_err(|e| e.to_string())
10 | }
11 |
12 | /// Import a provider from a deep link request (after user confirmation)
13 | #[tauri::command]
14 | pub fn import_from_deeplink(
15 | state: State,
16 | request: DeepLinkImportRequest,
17 | ) -> Result {
18 | log::info!(
19 | "Importing provider from deep link: {} for app {}",
20 | request.name,
21 | request.app
22 | );
23 |
24 | let provider_id = import_provider_from_deeplink(&state, request).map_err(|e| e.to_string())?;
25 |
26 | log::info!("Successfully imported provider with ID: {provider_id}");
27 |
28 | Ok(provider_id)
29 | }
30 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Jason Young
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/src/components/providers/ProviderEmptyState.tsx:
--------------------------------------------------------------------------------
1 | import { Users } from "lucide-react";
2 | import { useTranslation } from "react-i18next";
3 | import { Button } from "@/components/ui/button";
4 |
5 | interface ProviderEmptyStateProps {
6 | onCreate?: () => void;
7 | }
8 |
9 | export function ProviderEmptyState({ onCreate }: ProviderEmptyStateProps) {
10 | const { t } = useTranslation();
11 |
12 | return (
13 |
14 |
15 |
16 |
17 |
{t("provider.noProviders")}
18 |
19 | {t("provider.noProvidersDescription")}
20 |
21 | {onCreate && (
22 |
25 | )}
26 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:20-bookworm AS node-builder
2 | WORKDIR /app
3 | COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
4 | RUN corepack enable && pnpm install --frozen-lockfile
5 | COPY tsconfig.json tsconfig.node.json tailwind.config.js vite.config.mts vite.config.web.mts vitest.config.ts ./
6 | COPY src ./src
7 | RUN pnpm build:web
8 |
9 | FROM rust:1.83-bookworm AS rust-builder
10 | RUN apt-get update && apt-get install -y --no-install-recommends libssl-dev pkg-config \
11 | && rm -rf /var/lib/apt/lists/*
12 | WORKDIR /app
13 | COPY src-tauri ./src-tauri
14 | COPY --from=node-builder /app/dist-web ./dist-web
15 | WORKDIR /app/src-tauri
16 | RUN cargo build --release --no-default-features --features web-server --example server
17 |
18 | FROM debian:bookworm-slim
19 | RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates libssl3 \
20 | && rm -rf /var/lib/apt/lists/*
21 | RUN useradd -m ccswitch
22 | COPY --from=rust-builder /app/src-tauri/target/release/examples/server /usr/local/bin/cc-switch-server
23 | USER ccswitch
24 | WORKDIR /home/ccswitch
25 | ENV HOST=0.0.0.0 PORT=3000
26 | EXPOSE 3000
27 | CMD ["cc-switch-server"]
28 |
--------------------------------------------------------------------------------
/tests/run-all.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | # Run all bash-based CC-Switch API/integration tests.
3 |
4 | set -o pipefail
5 |
6 | ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
7 |
8 | TEST_SCRIPTS=(
9 | "api/test-auth.sh"
10 | "api/test-providers.sh"
11 | "api/test-usage.sh"
12 | "api/test-settings.sh"
13 | "api/test-mcp.sh"
14 | "integration/test-full-workflow.sh"
15 | "integration/test-persistence.sh"
16 | )
17 |
18 | if [ "$#" -gt 0 ]; then
19 | TEST_SCRIPTS=("$@")
20 | fi
21 |
22 | OVERALL_FAILED=0
23 | TOTAL=0
24 | FAILED=0
25 |
26 | for script in "${TEST_SCRIPTS[@]}"; do
27 | ((TOTAL++))
28 | echo ""
29 | echo "=================================="
30 | echo "Running $script"
31 | echo "=================================="
32 | if bash "$ROOT_DIR/$script"; then
33 | echo "-> $script passed"
34 | else
35 | echo "-> $script failed"
36 | OVERALL_FAILED=1
37 | ((FAILED++))
38 | fi
39 | done
40 |
41 | PASSED=$((TOTAL - FAILED))
42 | echo ""
43 | echo "=================================="
44 | echo "Bash test report"
45 | echo "Total: $TOTAL | Passed: $PASSED | Failed: $FAILED"
46 | echo "=================================="
47 |
48 | exit $OVERALL_FAILED
49 |
--------------------------------------------------------------------------------
/src-tauri/src/init_status.rs:
--------------------------------------------------------------------------------
1 | use serde::Serialize;
2 | use std::sync::{OnceLock, RwLock};
3 |
4 | #[derive(Debug, Clone, Serialize)]
5 | pub struct InitErrorPayload {
6 | pub path: String,
7 | pub error: String,
8 | }
9 |
10 | static INIT_ERROR: OnceLock>> = OnceLock::new();
11 |
12 | fn cell() -> &'static RwLock