├── .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 |