From 77b22e2b680860616caab0347232293ad6b7d68d Mon Sep 17 00:00:00 2001 From: SaladDay Date: Thu, 30 Jul 2026 10:39:46 +0000 Subject: [PATCH] refactor: remove superseded dead code --- package.json | 4 - pnpm-lock.yaml | 51 --- scripts/extract-icons.js | 212 ----------- scripts/filter-icons.js | 96 ----- src-tauri/src/lib.rs | 1 - src-tauri/src/provider_defaults.rs | 238 ------------ src-tauri/src/proxy/health.rs | 7 - src-tauri/src/proxy/mod.rs | 8 +- src-tauri/src/proxy/response_handler.rs | 232 ------------ src-tauri/src/proxy/session.rs | 287 -------------- src-tauri/src/proxy/types.rs | 9 - src-tauri/src/proxy/usage/calculator.rs | 27 -- src-tauri/src/proxy/usage/mod.rs | 2 +- src-tauri/src/proxy/usage/parser.rs | 169 --------- src/components/ColorPicker.tsx | 79 ---- src/components/icons/TerminalIcons.tsx | 190 ---------- src/components/mode-toggle.tsx | 25 -- src/components/prompts/PromptFormModal.tsx | 164 -------- .../providers/HealthStatusIndicator.tsx | 51 --- src/components/providers/forms/hooks/index.ts | 1 - .../forms/hooks/useCustomEndpoints.ts | 92 ----- .../proxy/CircuitBreakerConfigPanel.tsx | 356 ------------------ src/components/skills/RepoManager.tsx | 202 ---------- src/components/usage/DataSourceBar.tsx | 126 ------- src/config/iconInference.ts | 77 ---- src/hooks/useProxyConfig.ts | 48 --- src/i18n/locales/en.json | 49 +-- src/i18n/locales/ja.json | 49 +-- src/i18n/locales/zh-TW.json | 49 +-- src/i18n/locales/zh.json | 49 +-- src/icons/extracted/index.ts | 4 +- src/lib/schemas/common.ts | 98 ----- src/lib/schemas/mcp.ts | 42 --- src/lib/schemas/settings.ts | 80 ---- 34 files changed, 8 insertions(+), 3166 deletions(-) delete mode 100644 scripts/extract-icons.js delete mode 100644 scripts/filter-icons.js delete mode 100644 src-tauri/src/provider_defaults.rs delete mode 100644 src-tauri/src/proxy/health.rs delete mode 100644 src-tauri/src/proxy/response_handler.rs delete mode 100644 src/components/ColorPicker.tsx delete mode 100644 src/components/icons/TerminalIcons.tsx delete mode 100644 src/components/mode-toggle.tsx delete mode 100644 src/components/prompts/PromptFormModal.tsx delete mode 100644 src/components/providers/HealthStatusIndicator.tsx delete mode 100644 src/components/providers/forms/hooks/useCustomEndpoints.ts delete mode 100644 src/components/proxy/CircuitBreakerConfigPanel.tsx delete mode 100644 src/components/skills/RepoManager.tsx delete mode 100644 src/components/usage/DataSourceBar.tsx delete mode 100644 src/config/iconInference.ts delete mode 100644 src/hooks/useProxyConfig.ts delete mode 100644 src/lib/schemas/common.ts delete mode 100644 src/lib/schemas/mcp.ts delete mode 100644 src/lib/schemas/settings.ts diff --git a/package.json b/package.json index 86ca39d01..839b5a5aa 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,6 @@ "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", "@hookform/resolvers": "^5.2.2", - "@lobehub/icons-static-svg": "^1.73.0", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-collapsible": "^1.1.12", @@ -65,14 +64,12 @@ "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", - "@radix-ui/react-visually-hidden": "^1.2.4", "@tanstack/react-query": "^5.90.3", "@tanstack/react-virtual": "^3.13.23", "@tauri-apps/api": "^2.8.0", "@tauri-apps/plugin-dialog": "^2.4.0", "@tauri-apps/plugin-log": "2.8.0", "@tauri-apps/plugin-process": "^2.0.0", - "@tauri-apps/plugin-store": "^2.0.0", "@tauri-apps/plugin-updater": "^2.0.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -81,7 +78,6 @@ "flexsearch": "^0.8.212", "framer-motion": "^12.23.25", "i18next": "^25.5.2", - "jsonc-parser": "^3.2.1", "lucide-react": "^0.542.0", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b9a556367..39971d031 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,9 +41,6 @@ importers: '@hookform/resolvers': specifier: ^5.2.2 version: 5.2.2(react-hook-form@7.65.0(react@18.3.1)) - '@lobehub/icons-static-svg': - specifier: ^1.73.0 - version: 1.73.0 '@radix-ui/react-accordion': specifier: ^1.2.12 version: 1.2.12(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -83,9 +80,6 @@ importers: '@radix-ui/react-tooltip': specifier: ^1.2.8 version: 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-visually-hidden': - specifier: ^1.2.4 - version: 1.2.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tanstack/react-query': specifier: ^5.90.3 version: 5.90.3(react@18.3.1) @@ -104,9 +98,6 @@ importers: '@tauri-apps/plugin-process': specifier: ^2.0.0 version: 2.3.0 - '@tauri-apps/plugin-store': - specifier: ^2.0.0 - version: 2.4.0 '@tauri-apps/plugin-updater': specifier: ^2.0.0 version: 2.9.0 @@ -131,9 +122,6 @@ importers: i18next: specifier: ^25.5.2 version: 25.5.2(typescript@5.9.2) - jsonc-parser: - specifier: ^3.2.1 - version: 3.3.1 lucide-react: specifier: ^0.542.0 version: 0.542.0(react@18.3.1) @@ -835,9 +823,6 @@ packages: '@lezer/markdown@1.6.0': resolution: {integrity: sha512-AXb98u3M6BEzTnreBnGtQaF7xFTiMA92Dsy5tqEjpacbjRxDSFdN4bKJo9uvU4cEEOS7D2B9MT7kvDgOEIzJSw==} - '@lobehub/icons-static-svg@1.73.0': - resolution: {integrity: sha512-ydKUCDoopdmulbjDZo/gppaODd5Ju5nPneVcN9A5dAz9IJZUMkLms8bqostMLrqcdMQ8resKjLuV9RhJaWhaag==} - '@marijn/find-cluster-break@1.0.2': resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==} @@ -1319,19 +1304,6 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-visually-hidden@1.2.4': - resolution: {integrity: sha512-kaeiyGCe844dkb9AVF+rb4yTyb1LiLN/e3es3nLiRyN4dC8AduBYPMnnNlDjX2VDOcvDEiPnRNMJeWCfsX0txg==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - '@radix-ui/rect@1.1.1': resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} @@ -1571,9 +1543,6 @@ packages: '@tauri-apps/plugin-process@2.3.0': resolution: {integrity: sha512-0DNj6u+9csODiV4seSxxRbnLpeGYdojlcctCuLOCgpH9X3+ckVZIEj6H7tRQ7zqWr7kSTEWnrxtAdBb0FbtrmQ==} - '@tauri-apps/plugin-store@2.4.0': - resolution: {integrity: sha512-PjBnlnH6jyI71MGhrPaxUUCsOzc7WO1mbc4gRhME0m2oxLgCqbksw6JyeKQimuzv4ysdpNO3YbmaY2haf82a3A==} - '@tauri-apps/plugin-updater@2.9.0': resolution: {integrity: sha512-j++sgY8XpeDvzImTrzWA08OqqGqgkNyxczLD7FjNJJx/uXxMZFz5nDcfkyoI/rCjYuj2101Tci/r/HFmOmoxCg==} @@ -2267,9 +2236,6 @@ packages: engines: {node: '>=6'} hasBin: true - jsonc-parser@3.3.1: - resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} - launch-ide@1.3.0: resolution: {integrity: sha512-pxiF+HVNMV0dDc6Z0q89RDmzMF9XmSGaOn4ueTegjMy3cUkezc3zrki5PCiz68zZIqAuhW7iwoWX7JO4Kn6B0A==} @@ -3692,8 +3658,6 @@ snapshots: '@lezer/common': 1.2.3 '@lezer/highlight': 1.2.1 - '@lobehub/icons-static-svg@1.73.0': {} - '@marijn/find-cluster-break@1.0.2': {} '@mswjs/interceptors@0.40.0': @@ -4197,15 +4161,6 @@ snapshots: '@types/react': 18.3.23 '@types/react-dom': 18.3.7(@types/react@18.3.23) - '@radix-ui/react-visually-hidden@1.2.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@radix-ui/react-primitive': 2.1.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - optionalDependencies: - '@types/react': 18.3.23 - '@types/react-dom': 18.3.7(@types/react@18.3.23) - '@radix-ui/rect@1.1.1': {} '@reduxjs/toolkit@2.11.0(react-redux@9.2.0(@types/react@18.3.23)(react@18.3.1)(redux@5.0.1))(react@18.3.1)': @@ -4362,10 +4317,6 @@ snapshots: dependencies: '@tauri-apps/api': 2.8.0 - '@tauri-apps/plugin-store@2.4.0': - dependencies: - '@tauri-apps/api': 2.8.0 - '@tauri-apps/plugin-updater@2.9.0': dependencies: '@tauri-apps/api': 2.8.0 @@ -5099,8 +5050,6 @@ snapshots: json5@2.2.3: {} - jsonc-parser@3.3.1: {} - launch-ide@1.3.0: dependencies: chalk: 4.1.1 diff --git a/scripts/extract-icons.js b/scripts/extract-icons.js deleted file mode 100644 index 59c91f3a6..000000000 --- a/scripts/extract-icons.js +++ /dev/null @@ -1,212 +0,0 @@ -const fs = require('fs'); -const path = require('path'); - -// 要提取的图标列表(按分类组织) -const ICONS_TO_EXTRACT = { - // AI 服务商(必需) - aiProviders: [ - 'openai', 'anthropic', 'claude', 'google', 'gemini', - 'deepseek', 'kimi', 'moonshot', 'stepfun', 'zhipu', 'minimax', - 'baidu', 'alibaba', 'tencent', 'meta', 'microsoft', - 'cohere', 'perplexity', 'mistral', 'huggingface' - ], - - // 云平台 - cloudPlatforms: [ - 'aws', 'azure', 'huawei', 'cloudflare' - ], - - // 开发工具 - devTools: [ - 'github', 'gitlab', 'docker', 'kubernetes', 'vscode' - ], - - // 其他 - others: [ - 'settings', 'folder', 'file', 'link' - ] -}; - -// 合并所有图标 -const ALL_ICONS = [ - ...ICONS_TO_EXTRACT.aiProviders, - ...ICONS_TO_EXTRACT.cloudPlatforms, - ...ICONS_TO_EXTRACT.devTools, - ...ICONS_TO_EXTRACT.others -]; - -// 提取逻辑 -const OUTPUT_DIR = path.join(__dirname, '../src/icons/extracted'); -const SOURCE_DIR = path.join(__dirname, '../node_modules/@lobehub/icons-static-svg/icons'); - -// 确保输出目录存在 -if (!fs.existsSync(OUTPUT_DIR)) { - fs.mkdirSync(OUTPUT_DIR, { recursive: true }); -} - -console.log('🎨 CC-Switch Icon Extractor\n'); -console.log('========================================'); -console.log('📦 Extracting icons...\n'); - -let extracted = 0; -let notFound = []; - -// 提取图标 -ALL_ICONS.forEach(iconName => { - const sourceFile = path.join(SOURCE_DIR, `${iconName}.svg`); - const targetFile = path.join(OUTPUT_DIR, `${iconName}.svg`); - - if (fs.existsSync(sourceFile)) { - fs.copyFileSync(sourceFile, targetFile); - console.log(` ✓ ${iconName}.svg`); - extracted++; - } else if (fs.existsSync(targetFile)) { - console.log(` ✓ ${iconName}.svg (kept local custom icon)`); - extracted++; - } else { - console.log(` ✗ ${iconName}.svg (not found)`); - notFound.push(iconName); - } -}); - -// 生成索引文件 -console.log('\n📝 Generating index file...\n'); - -const indexContent = `// Auto-generated icon index -// Do not edit manually - -export const icons: Record = { -${ALL_ICONS.filter(name => !notFound.includes(name)) - .map(name => { - const svg = fs.readFileSync(path.join(OUTPUT_DIR, `${name}.svg`), 'utf-8'); - const escaped = svg.replace(/`/g, '\\`').replace(/\$/g, '\\$'); - return ` '${name}': \`${escaped}\`,`; - }) - .join('\n')} -}; - -export const iconList = Object.keys(icons); - -export function getIcon(name: string): string { - return icons[name.toLowerCase()] || ''; -} - -export function hasIcon(name: string): boolean { - return name.toLowerCase() in icons; -} -`; - -fs.writeFileSync(path.join(OUTPUT_DIR, 'index.ts'), indexContent); -console.log('✓ Generated: src/icons/extracted/index.ts'); - -// 生成图标元数据 -const metadataContent = `// Icon metadata for search and categorization -import { IconMetadata } from '@/types/icon'; - -export const iconMetadata: Record = { - // AI Providers - openai: { name: 'openai', displayName: 'OpenAI', category: 'ai-provider', keywords: ['gpt', 'chatgpt'], defaultColor: '#00A67E' }, - anthropic: { name: 'anthropic', displayName: 'Anthropic', category: 'ai-provider', keywords: ['claude'], defaultColor: '#D4915D' }, - claude: { name: 'claude', displayName: 'Claude', category: 'ai-provider', keywords: ['anthropic'], defaultColor: '#D4915D' }, - google: { name: 'google', displayName: 'Google', category: 'ai-provider', keywords: ['gemini', 'bard'], defaultColor: '#4285F4' }, - gemini: { name: 'gemini', displayName: 'Gemini', category: 'ai-provider', keywords: ['google'], defaultColor: '#4285F4' }, - deepseek: { name: 'deepseek', displayName: 'DeepSeek', category: 'ai-provider', keywords: ['deep', 'seek'], defaultColor: '#1E88E5' }, - moonshot: { name: 'moonshot', displayName: 'Moonshot', category: 'ai-provider', keywords: ['kimi', 'moonshot'], defaultColor: '#6366F1' }, - kimi: { name: 'kimi', displayName: 'Kimi', category: 'ai-provider', keywords: ['moonshot'], defaultColor: '#6366F1' }, - stepfun: { name: 'stepfun', displayName: 'StepFun', category: 'ai-provider', keywords: ['stepfun', 'step', 'jieyue', '阶跃星辰'], defaultColor: '#005AFF' }, - zhipu: { name: 'zhipu', displayName: 'Zhipu AI', category: 'ai-provider', keywords: ['chatglm', 'glm'], defaultColor: '#0F62FE' }, - minimax: { name: 'minimax', displayName: 'MiniMax', category: 'ai-provider', keywords: ['minimax'], defaultColor: '#FF6B6B' }, - baidu: { name: 'baidu', displayName: 'Baidu', category: 'ai-provider', keywords: ['ernie', 'wenxin'], defaultColor: '#2932E1' }, - alibaba: { name: 'alibaba', displayName: 'Alibaba', category: 'ai-provider', keywords: ['qwen', 'tongyi'], defaultColor: '#FF6A00' }, - tencent: { name: 'tencent', displayName: 'Tencent', category: 'ai-provider', keywords: ['hunyuan'], defaultColor: '#00A4FF' }, - meta: { name: 'meta', displayName: 'Meta', category: 'ai-provider', keywords: ['facebook', 'llama'], defaultColor: '#0081FB' }, - microsoft: { name: 'microsoft', displayName: 'Microsoft', category: 'ai-provider', keywords: ['copilot', 'azure'], defaultColor: '#00A4EF' }, - cohere: { name: 'cohere', displayName: 'Cohere', category: 'ai-provider', keywords: ['cohere'], defaultColor: '#39594D' }, - perplexity: { name: 'perplexity', displayName: 'Perplexity', category: 'ai-provider', keywords: ['perplexity'], defaultColor: '#20808D' }, - mistral: { name: 'mistral', displayName: 'Mistral', category: 'ai-provider', keywords: ['mistral'], defaultColor: '#FF7000' }, - huggingface: { name: 'huggingface', displayName: 'Hugging Face', category: 'ai-provider', keywords: ['huggingface', 'hf'], defaultColor: '#FFD21E' }, - - // Cloud Platforms - aws: { name: 'aws', displayName: 'AWS', category: 'cloud', keywords: ['amazon', 'cloud'], defaultColor: '#FF9900' }, - azure: { name: 'azure', displayName: 'Azure', category: 'cloud', keywords: ['microsoft', 'cloud'], defaultColor: '#0078D4' }, - huawei: { name: 'huawei', displayName: 'Huawei', category: 'cloud', keywords: ['huawei', 'cloud'], defaultColor: '#FF0000' }, - cloudflare: { name: 'cloudflare', displayName: 'Cloudflare', category: 'cloud', keywords: ['cloudflare', 'cdn'], defaultColor: '#F38020' }, - - // Dev Tools - github: { name: 'github', displayName: 'GitHub', category: 'tool', keywords: ['git', 'version control'], defaultColor: '#181717' }, - gitlab: { name: 'gitlab', displayName: 'GitLab', category: 'tool', keywords: ['git', 'version control'], defaultColor: '#FC6D26' }, - docker: { name: 'docker', displayName: 'Docker', category: 'tool', keywords: ['container'], defaultColor: '#2496ED' }, - kubernetes: { name: 'kubernetes', displayName: 'Kubernetes', category: 'tool', keywords: ['k8s', 'container'], defaultColor: '#326CE5' }, - vscode: { name: 'vscode', displayName: 'VS Code', category: 'tool', keywords: ['editor', 'ide'], defaultColor: '#007ACC' }, - - // Others - settings: { name: 'settings', displayName: 'Settings', category: 'other', keywords: ['config', 'preferences'], defaultColor: '#6B7280' }, - folder: { name: 'folder', displayName: 'Folder', category: 'other', keywords: ['directory'], defaultColor: '#6B7280' }, - file: { name: 'file', displayName: 'File', category: 'other', keywords: ['document'], defaultColor: '#6B7280' }, - link: { name: 'link', displayName: 'Link', category: 'other', keywords: ['url', 'hyperlink'], defaultColor: '#6B7280' }, -}; - -export function getIconMetadata(name: string): IconMetadata | undefined { - return iconMetadata[name.toLowerCase()]; -} - -export function searchIcons(query: string): string[] { - const lowerQuery = query.toLowerCase(); - return Object.values(iconMetadata) - .filter(meta => - meta.name.includes(lowerQuery) || - meta.displayName.toLowerCase().includes(lowerQuery) || - meta.keywords.some(k => k.includes(lowerQuery)) - ) - .map(meta => meta.name); -} -`; - -fs.writeFileSync(path.join(OUTPUT_DIR, 'metadata.ts'), metadataContent); -console.log('✓ Generated: src/icons/extracted/metadata.ts'); - -// 生成 README -const readmeContent = `# Extracted Icons - -This directory contains extracted icons from @lobehub/icons-static-svg. - -## Statistics -- Total extracted: ${extracted} icons -- Not found: ${notFound.length} icons - -## Extracted Icons -${ALL_ICONS.filter(name => !notFound.includes(name)).map(name => `- ${name}`).join('\n')} - -${notFound.length > 0 ? `\n## Not Found\n${notFound.map(name => `- ${name}`).join('\n')}` : ''} - -## Usage - -\`\`\`typescript -import { getIcon, hasIcon, iconList } from './extracted'; - -// Get icon SVG -const svg = getIcon('openai'); - -// Check if icon exists -if (hasIcon('openai')) { - // ... -} - -// Get all available icons -console.log(iconList); -\`\`\` - ---- -Last updated: ${new Date().toISOString()} -Generated by: scripts/extract-icons.js -`; - -fs.writeFileSync(path.join(OUTPUT_DIR, 'README.md'), readmeContent); -console.log('✓ Generated: src/icons/extracted/README.md'); - -console.log('\n========================================'); -console.log('✅ Extraction complete!\n'); -console.log(` ✓ Extracted: ${extracted} icons`); -console.log(` ✗ Not found: ${notFound.length} icons`); -console.log(` 📉 Bundle size reduction: ~${Math.round((1 - extracted / 723) * 100)}%`); -console.log('========================================\n'); diff --git a/scripts/filter-icons.js b/scripts/filter-icons.js deleted file mode 100644 index add6e5ee2..000000000 --- a/scripts/filter-icons.js +++ /dev/null @@ -1,96 +0,0 @@ -const fs = require('fs'); -const path = require('path'); - -const ICONS_DIR = path.join(__dirname, '../src/icons/extracted'); - -// List of "Famous" icons to keep -// Based on common AI providers and tools -const KEEP_LIST = [ - // AI Providers - 'openai', 'anthropic', 'claude', 'google', 'gemini', 'gemma', 'palm', - 'microsoft', 'azure', 'copilot', 'meta', 'llama', - 'alibaba', 'qwen', 'tencent', 'hunyuan', 'baidu', 'wenxin', - 'bytedance', 'doubao', 'deepseek', 'moonshot', 'kimi', 'stepfun', - 'zhipu', 'chatglm', 'glm', 'minimax', 'mistral', 'cohere', - 'perplexity', 'huggingface', 'midjourney', 'stability', - 'xai', 'grok', 'yi', 'zeroone', 'ollama', - 'packycode', - - // Cloud/Tools - 'aws', 'googlecloud', 'huawei', 'cloudflare', - 'github', 'githubcopilot', 'vercel', 'notion', 'discord', - 'gitlab', 'docker', 'kubernetes', 'vscode', 'settings', 'folder', 'file', 'link' -]; - -// Get all SVG files -const files = fs.readdirSync(ICONS_DIR).filter(file => file.endsWith('.svg')); - -console.log(`Scanning ${files.length} files...`); - -let keptCount = 0; -let deletedCount = 0; -let renamedCount = 0; - -// First pass: Identify files to keep and prefer color versions -const fileMap = {}; // name -> { hasColor: bool, hasMono: bool } - -files.forEach(file => { - const isColor = file.endsWith('-color.svg'); - const baseName = isColor ? file.replace('-color.svg', '') : file.replace('.svg', ''); - - if (!fileMap[baseName]) { - fileMap[baseName] = { hasColor: false, hasMono: false }; - } - - if (isColor) { - fileMap[baseName].hasColor = true; - } else { - fileMap[baseName].hasMono = true; - } -}); - -// Second pass: Process files -Object.keys(fileMap).forEach(baseName => { - const info = fileMap[baseName]; - const shouldKeep = KEEP_LIST.includes(baseName); - - if (!shouldKeep) { - // Delete both versions if not in keep list - if (info.hasColor) { - fs.unlinkSync(path.join(ICONS_DIR, `${baseName}-color.svg`)); - deletedCount++; - } - if (info.hasMono) { - fs.unlinkSync(path.join(ICONS_DIR, `${baseName}.svg`)); - deletedCount++; - } - return; - } - - // If keeping, prefer color - if (info.hasColor) { - // Rename color version to base version (overwrite mono if exists) - const colorPath = path.join(ICONS_DIR, `${baseName}-color.svg`); - const targetPath = path.join(ICONS_DIR, `${baseName}.svg`); - - try { - // If mono exists, it will be overwritten/replaced - fs.renameSync(colorPath, targetPath); - renamedCount++; - keptCount++; - } catch (e) { - console.error(`Error renaming ${baseName}:`, e); - } - } else if (info.hasMono) { - // Keep mono if no color version - keptCount++; - } -}); - -console.log(`\nCleanup complete:`); -console.log(`- Kept: ${keptCount}`); -console.log(`- Deleted: ${deletedCount}`); -console.log(`- Renamed (Color -> Standard): ${renamedCount}`); - -// Regenerate index and metadata -require('./generate-icon-index.js'); diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 1e6cd62fa..39edcf098 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -28,7 +28,6 @@ mod panic_hook; mod prompt; mod prompt_files; mod provider; -mod provider_defaults; mod proxy; mod services; mod session_manager; diff --git a/src-tauri/src/provider_defaults.rs b/src-tauri/src/provider_defaults.rs deleted file mode 100644 index 3fb2ad068..000000000 --- a/src-tauri/src/provider_defaults.rs +++ /dev/null @@ -1,238 +0,0 @@ -use once_cell::sync::Lazy; -use std::collections::HashMap; - -/// 供应商图标信息 -#[derive(Debug, Clone)] -#[allow(dead_code)] -pub struct ProviderIcon { - pub name: &'static str, - pub color: &'static str, -} - -/// 供应商名称到图标的默认映射 -#[allow(dead_code)] -pub static DEFAULT_PROVIDER_ICONS: Lazy> = Lazy::new(|| { - let mut m = HashMap::new(); - - // AI 服务商 - m.insert( - "openai", - ProviderIcon { - name: "openai", - color: "#00A67E", - }, - ); - m.insert( - "anthropic", - ProviderIcon { - name: "anthropic", - color: "#D4915D", - }, - ); - m.insert( - "claude", - ProviderIcon { - name: "claude", - color: "#D4915D", - }, - ); - m.insert( - "google", - ProviderIcon { - name: "google", - color: "#4285F4", - }, - ); - m.insert( - "gemini", - ProviderIcon { - name: "gemini", - color: "#4285F4", - }, - ); - m.insert( - "deepseek", - ProviderIcon { - name: "deepseek", - color: "#1E88E5", - }, - ); - m.insert( - "kimi", - ProviderIcon { - name: "kimi", - color: "#6366F1", - }, - ); - m.insert( - "moonshot", - ProviderIcon { - name: "moonshot", - color: "#6366F1", - }, - ); - m.insert( - "zhipu", - ProviderIcon { - name: "zhipu", - color: "#0F62FE", - }, - ); - m.insert( - "minimax", - ProviderIcon { - name: "minimax", - color: "#FF6B6B", - }, - ); - m.insert( - "baidu", - ProviderIcon { - name: "baidu", - color: "#2932E1", - }, - ); - m.insert( - "alibaba", - ProviderIcon { - name: "alibaba", - color: "#FF6A00", - }, - ); - m.insert( - "tencent", - ProviderIcon { - name: "tencent", - color: "#00A4FF", - }, - ); - m.insert( - "meta", - ProviderIcon { - name: "meta", - color: "#0081FB", - }, - ); - m.insert( - "microsoft", - ProviderIcon { - name: "microsoft", - color: "#00A4EF", - }, - ); - m.insert( - "cohere", - ProviderIcon { - name: "cohere", - color: "#39594D", - }, - ); - m.insert( - "perplexity", - ProviderIcon { - name: "perplexity", - color: "#20808D", - }, - ); - m.insert( - "mistral", - ProviderIcon { - name: "mistral", - color: "#FF7000", - }, - ); - m.insert( - "huggingface", - ProviderIcon { - name: "huggingface", - color: "#FFD21E", - }, - ); - - // 云平台 - m.insert( - "aws", - ProviderIcon { - name: "aws", - color: "#FF9900", - }, - ); - m.insert( - "azure", - ProviderIcon { - name: "azure", - color: "#0078D4", - }, - ); - m.insert( - "huawei", - ProviderIcon { - name: "huawei", - color: "#FF0000", - }, - ); - m.insert( - "cloudflare", - ProviderIcon { - name: "cloudflare", - color: "#F38020", - }, - ); - - m -}); - -/// 根据供应商名称智能推断图标 -#[allow(dead_code)] -pub fn infer_provider_icon(provider_name: &str) -> Option { - let name_lower = provider_name.to_lowercase(); - - // 精确匹配 - if let Some(icon) = DEFAULT_PROVIDER_ICONS.get(name_lower.as_str()) { - return Some(icon.clone()); - } - - // 模糊匹配(包含关键词) - for (key, icon) in DEFAULT_PROVIDER_ICONS.iter() { - if name_lower.contains(key) { - return Some(icon.clone()); - } - } - - None -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_exact_match() { - let icon = infer_provider_icon("openai"); - assert!(icon.is_some()); - let icon = icon.unwrap(); - assert_eq!(icon.name, "openai"); - assert_eq!(icon.color, "#00A67E"); - } - - #[test] - fn test_fuzzy_match() { - let icon = infer_provider_icon("OpenAI Official"); - assert!(icon.is_some()); - let icon = icon.unwrap(); - assert_eq!(icon.name, "openai"); - } - - #[test] - fn test_case_insensitive() { - let icon = infer_provider_icon("ANTHROPIC"); - assert!(icon.is_some()); - assert_eq!(icon.unwrap().name, "anthropic"); - } - - #[test] - fn test_no_match() { - let icon = infer_provider_icon("unknown provider"); - assert!(icon.is_none()); - } -} diff --git a/src-tauri/src/proxy/health.rs b/src-tauri/src/proxy/health.rs deleted file mode 100644 index 4cdea8e21..000000000 --- a/src-tauri/src/proxy/health.rs +++ /dev/null @@ -1,7 +0,0 @@ -//! 健康检查器 -//! -//! 负责定期检查Provider健康状态(占位实现) - -// 占位实现,稍后添加完整逻辑 -#[allow(dead_code)] -pub struct HealthChecker; diff --git a/src-tauri/src/proxy/mod.rs b/src-tauri/src/proxy/mod.rs index cd843e20a..d1dc85808 100644 --- a/src-tauri/src/proxy/mod.rs +++ b/src-tauri/src/proxy/mod.rs @@ -15,7 +15,6 @@ pub mod gemini_url; pub mod handler_config; pub mod handler_context; mod handlers; -mod health; pub mod http_client; pub mod hyper_client; pub(crate) mod json_canonical; @@ -24,7 +23,6 @@ pub mod media_sanitizer; pub mod model_mapper; pub mod provider_router; pub mod providers; -pub mod response_handler; pub mod response_processor; pub(crate) mod server; pub mod session; @@ -47,11 +45,7 @@ pub use error::ProxyError; #[allow(unused_imports)] pub use provider_router::ProviderRouter; #[allow(unused_imports)] -pub use response_handler::{NonStreamHandler, ResponseType, StreamHandler}; -#[allow(unused_imports)] -pub use session::{ - extract_session_id, ClientFormat, ProxySession, SessionIdResult, SessionIdSource, -}; +pub use session::{extract_session_id, SessionIdResult, SessionIdSource}; #[allow(unused_imports)] pub use types::{ProxyConfig, ProxyServerInfo, ProxyStatus}; diff --git a/src-tauri/src/proxy/response_handler.rs b/src-tauri/src/proxy/response_handler.rs deleted file mode 100644 index d40ec0b06..000000000 --- a/src-tauri/src/proxy/response_handler.rs +++ /dev/null @@ -1,232 +0,0 @@ -//! Response Handler - 统一响应处理 -//! -//! 提供流式和非流式响应的统一处理接口 - -use super::session::ProxySession; -use super::usage::parser::TokenUsage; -use super::ProxyError; -use crate::proxy::sse::{strip_sse_field, take_sse_block}; -use bytes::Bytes; -use futures::stream::{Stream, StreamExt}; -use serde_json::Value; -use std::sync::Arc; -use std::time::{Duration, Instant}; -use tokio::sync::Mutex; -use tokio::time::timeout; - -/// 响应类型 -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[allow(dead_code)] -pub enum ResponseType { - /// 流式响应 (SSE) - Stream, - /// 非流式响应 - NonStream, -} - -impl ResponseType { - /// 从 Content-Type 检测响应类型 - #[allow(dead_code)] - pub fn from_content_type(content_type: &str) -> Self { - if content_type.contains("text/event-stream") { - ResponseType::Stream - } else { - ResponseType::NonStream - } - } -} - -/// 流式响应处理器 -#[allow(dead_code)] -pub struct StreamHandler { - /// 空闲超时时间 - idle_timeout: Duration, - /// 收集的事件 - events: Arc>>, -} - -#[allow(dead_code)] -impl StreamHandler { - /// 创建新的流式处理器 - pub fn new(idle_timeout_secs: u64) -> Self { - Self { - idle_timeout: Duration::from_secs(idle_timeout_secs), - events: Arc::new(Mutex::new(Vec::new())), - } - } - - /// 处理流式响应,返回分流后的客户端流 - /// - /// 客户端流立即返回,内部流在后台收集事件 - pub fn handle_stream( - &self, - stream: S, - ) -> impl Stream> + Send - where - S: Stream> + Send + 'static, - { - let events = self.events.clone(); - let idle_timeout = self.idle_timeout; - - async_stream::stream! { - let mut _last_activity = Instant::now(); - let mut buffer = String::new(); - let mut utf8_remainder: Vec = Vec::new(); - - tokio::pin!(stream); - - loop { - let chunk_result = timeout(idle_timeout, stream.next()).await; - - match chunk_result { - Ok(Some(Ok(bytes))) => { - _last_activity = Instant::now(); - - // 解析 SSE 事件 - crate::proxy::sse::append_utf8_safe(&mut buffer, &mut utf8_remainder, &bytes); - - // 提取完整事件 - while let Some(event_text) = take_sse_block(&mut buffer) { - for line in event_text.lines() { - if let Some(data) = strip_sse_field(line, "data") { - if data.trim() != "[DONE]" { - if let Ok(json) = serde_json::from_str::(data) { - let mut guard = events.lock().await; - guard.push(json); - } - } - } - } - } - - yield Ok(bytes); - } - Ok(Some(Err(e))) => { - log::error!("流错误: {e}"); - yield Err(std::io::Error::other(e.to_string())); - break; - } - Ok(None) => { - // 流结束 - break; - } - Err(_) => { - // 空闲超时 - log::warn!("流式响应空闲超时: {idle_timeout:?} 无数据"); - yield Err(std::io::Error::other("Stream idle timeout")); - break; - } - } - } - } - } - - /// 获取收集的事件 - pub async fn get_events(&self) -> Vec { - let guard = self.events.lock().await; - guard.clone() - } - - /// 从收集的事件中提取 Token 使用量 - pub async fn extract_usage(&self, session: &ProxySession) -> Option { - let events = self.get_events().await; - - match session.client_format { - super::session::ClientFormat::Claude => TokenUsage::from_claude_stream_events(&events), - super::session::ClientFormat::Codex => TokenUsage::from_codex_stream_events(&events), - super::session::ClientFormat::Gemini | super::session::ClientFormat::GeminiCli => { - TokenUsage::from_gemini_stream_chunks(&events) - } - _ => None, - } - } -} - -/// 非流式响应处理器 -#[allow(dead_code)] -pub struct NonStreamHandler; - -#[allow(dead_code)] -impl NonStreamHandler { - /// 处理非流式响应 - /// - /// 克隆响应体用于后台解析,原始响应立即返回 - pub async fn handle_response( - body: &[u8], - session: &ProxySession, - ) -> Result, ProxyError> { - let json: Value = serde_json::from_slice(body) - .map_err(|e| ProxyError::TransformError(format!("Failed to parse response: {e}")))?; - - let usage = match session.client_format { - super::session::ClientFormat::Claude => TokenUsage::from_claude_response(&json), - super::session::ClientFormat::Codex => TokenUsage::from_codex_response_adjusted(&json), - super::session::ClientFormat::Gemini | super::session::ClientFormat::GeminiCli => { - TokenUsage::from_gemini_response(&json) - } - super::session::ClientFormat::OpenAI => TokenUsage::from_openrouter_response(&json), - _ => None, - }; - - Ok(usage) - } -} - -/// 统一响应分发器 -#[allow(dead_code)] -pub struct ResponseDispatcher; - -#[allow(dead_code)] -impl ResponseDispatcher { - /// 判断响应类型 - pub fn detect_type(content_type: &str) -> ResponseType { - ResponseType::from_content_type(content_type) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_response_type_detection() { - assert_eq!( - ResponseType::from_content_type("text/event-stream"), - ResponseType::Stream - ); - assert_eq!( - ResponseType::from_content_type("text/event-stream; charset=utf-8"), - ResponseType::Stream - ); - assert_eq!( - ResponseType::from_content_type("application/json"), - ResponseType::NonStream - ); - } - - #[test] - fn test_stream_handler_creation() { - let handler = StreamHandler::new(30); - assert_eq!(handler.idle_timeout, Duration::from_secs(30)); - } - - #[test] - fn test_strip_sse_field_accepts_optional_space() { - assert_eq!( - super::strip_sse_field("data: {\"ok\":true}", "data"), - Some("{\"ok\":true}") - ); - assert_eq!( - super::strip_sse_field("data:{\"ok\":true}", "data"), - Some("{\"ok\":true}") - ); - assert_eq!( - super::strip_sse_field("event: message_start", "event"), - Some("message_start") - ); - assert_eq!( - super::strip_sse_field("event:message_start", "event"), - Some("message_start") - ); - } -} diff --git a/src-tauri/src/proxy/session.rs b/src-tauri/src/proxy/session.rs index 7f3ce06b7..95b726189 100644 --- a/src-tauri/src/proxy/session.rs +++ b/src-tauri/src/proxy/session.rs @@ -10,180 +10,8 @@ //! - 其他: 生成新的 UUID use axum::http::HeaderMap; -use std::time::Instant; use uuid::Uuid; -/// 客户端请求格式 -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[allow(dead_code)] -pub enum ClientFormat { - /// Claude Messages API (/v1/messages) - Claude, - /// Codex Response API (/v1/responses) - Codex, - /// OpenAI Chat Completions API (/v1/chat/completions) - OpenAI, - /// Gemini API (/v1beta/models/*/generateContent) - Gemini, - /// Gemini CLI API (/v1internal/models/*/generateContent) - GeminiCli, - /// 未知格式 - Unknown, -} - -#[allow(dead_code)] -impl ClientFormat { - /// 从请求路径检测格式 - pub fn from_path(path: &str) -> Self { - if path.contains("/v1/messages") { - ClientFormat::Claude - } else if path.contains("/v1/responses") { - ClientFormat::Codex - } else if path.contains("/v1/chat/completions") { - ClientFormat::OpenAI - } else if path.contains("/v1internal/") && path.contains("generateContent") { - // Gemini CLI 使用 /v1internal/ 路径 - ClientFormat::GeminiCli - } else if (path.contains("/v1beta/") || path.contains("/v1/")) - && path.contains("generateContent") - { - // Gemini API 使用 /v1beta/ 或 /v1/ 路径 - ClientFormat::Gemini - } else if path.contains("generateContent") { - // 通用 Gemini 端点 - ClientFormat::Gemini - } else { - ClientFormat::Unknown - } - } - - /// 从请求体内容检测格式(回退方案) - pub fn from_body(body: &serde_json::Value) -> Self { - // Claude 格式特征: messages 数组 + model 字段 + 无 response_format - if body.get("messages").is_some() - && body.get("model").is_some() - && body.get("response_format").is_none() - && body.get("contents").is_none() - { - // 区分 Claude 和 OpenAI - if body.get("max_tokens").is_some() { - return ClientFormat::Claude; - } - return ClientFormat::OpenAI; - } - - // Codex 格式特征: input 字段 - if body.get("input").is_some() { - return ClientFormat::Codex; - } - - // Gemini 格式特征: contents 数组 - if body.get("contents").is_some() { - return ClientFormat::Gemini; - } - - ClientFormat::Unknown - } - - /// 转换为字符串 - pub fn as_str(&self) -> &'static str { - match self { - ClientFormat::Claude => "claude", - ClientFormat::Codex => "codex", - ClientFormat::OpenAI => "openai", - ClientFormat::Gemini => "gemini", - ClientFormat::GeminiCli => "gemini_cli", - ClientFormat::Unknown => "unknown", - } - } -} - -impl std::fmt::Display for ClientFormat { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.as_str()) - } -} - -/// 代理会话 -/// -/// 包含请求全生命周期的上下文数据 -#[derive(Debug, Clone)] -#[allow(dead_code)] -pub struct ProxySession { - /// 唯一会话 ID - pub session_id: String, - /// 请求开始时间 - pub start_time: Instant, - /// HTTP 方法 - pub method: String, - /// 请求 URL - pub request_url: String, - /// User-Agent - pub user_agent: Option, - /// 客户端请求格式 - pub client_format: ClientFormat, - /// 选定的供应商 ID - pub provider_id: Option, - /// 模型名称 - pub model: Option, - /// 是否为流式请求 - pub is_streaming: bool, -} - -#[allow(dead_code)] -impl ProxySession { - /// 从请求创建会话 - pub fn from_request( - method: &str, - request_url: &str, - user_agent: Option<&str>, - body: Option<&serde_json::Value>, - ) -> Self { - // 检测客户端格式 - let mut client_format = ClientFormat::from_path(request_url); - if client_format == ClientFormat::Unknown { - if let Some(body) = body { - client_format = ClientFormat::from_body(body); - } - } - - // 检测是否为流式请求 - let is_streaming = body - .and_then(|b| b.get("stream")) - .and_then(|v| v.as_bool()) - .unwrap_or(false); - - // 提取模型名称 - let model = body - .and_then(|b| b.get("model")) - .and_then(|v| v.as_str()) - .map(|s| s.to_string()); - - Self { - session_id: Uuid::new_v4().to_string(), - start_time: Instant::now(), - method: method.to_string(), - request_url: request_url.to_string(), - user_agent: user_agent.map(|s| s.to_string()), - client_format, - provider_id: None, - model, - is_streaming, - } - } - - /// 设置供应商 ID - pub fn with_provider(mut self, provider_id: &str) -> Self { - self.provider_id = Some(provider_id.to_string()); - self - } - - /// 获取请求延迟(毫秒) - pub fn latency_ms(&self) -> u64 { - self.start_time.elapsed().as_millis() as u64 - } -} - // ============================================================================ // Session ID 提取器 // ============================================================================ @@ -380,121 +208,6 @@ mod tests { use super::*; use serde_json::json; - #[test] - fn test_client_format_from_path_claude() { - assert_eq!( - ClientFormat::from_path("/v1/messages"), - ClientFormat::Claude - ); - assert_eq!( - ClientFormat::from_path("/api/v1/messages"), - ClientFormat::Claude - ); - } - - #[test] - fn test_client_format_from_path_codex() { - assert_eq!( - ClientFormat::from_path("/v1/responses"), - ClientFormat::Codex - ); - } - - #[test] - fn test_client_format_from_path_openai() { - assert_eq!( - ClientFormat::from_path("/v1/chat/completions"), - ClientFormat::OpenAI - ); - } - - #[test] - fn test_client_format_from_path_gemini() { - assert_eq!( - ClientFormat::from_path("/v1beta/models/gemini-pro:generateContent"), - ClientFormat::Gemini - ); - } - - #[test] - fn test_client_format_from_path_gemini_cli() { - assert_eq!( - ClientFormat::from_path("/v1internal/models/gemini-pro:generateContent"), - ClientFormat::GeminiCli - ); - } - - #[test] - fn test_client_format_from_body_claude() { - let body = json!({ - "model": "claude-3-5-sonnet", - "messages": [{"role": "user", "content": "Hello"}], - "max_tokens": 1024 - }); - assert_eq!(ClientFormat::from_body(&body), ClientFormat::Claude); - } - - #[test] - fn test_client_format_from_body_codex() { - let body = json!({ - "input": "Write a function" - }); - assert_eq!(ClientFormat::from_body(&body), ClientFormat::Codex); - } - - #[test] - fn test_client_format_from_body_gemini() { - let body = json!({ - "contents": [{"parts": [{"text": "Hello"}]}] - }); - assert_eq!(ClientFormat::from_body(&body), ClientFormat::Gemini); - } - - #[test] - fn test_session_id_uniqueness() { - let session1 = ProxySession::from_request("POST", "/v1/messages", None, None); - let session2 = ProxySession::from_request("POST", "/v1/messages", None, None); - assert_ne!(session1.session_id, session2.session_id); - } - - #[test] - fn test_session_from_request() { - let body = json!({ - "model": "claude-3-5-sonnet", - "messages": [{"role": "user", "content": "Hello"}], - "max_tokens": 1024, - "stream": true - }); - - let session = - ProxySession::from_request("POST", "/v1/messages", Some("Mozilla/5.0"), Some(&body)); - - assert_eq!(session.method, "POST"); - assert_eq!(session.request_url, "/v1/messages"); - assert_eq!(session.user_agent, Some("Mozilla/5.0".to_string())); - assert_eq!(session.client_format, ClientFormat::Claude); - assert_eq!(session.model, Some("claude-3-5-sonnet".to_string())); - assert!(session.is_streaming); - } - - #[test] - fn test_session_with_provider() { - let session = ProxySession::from_request("POST", "/v1/messages", None, None) - .with_provider("provider-123"); - - assert_eq!(session.provider_id, Some("provider-123".to_string())); - } - - #[test] - fn test_client_format_as_str() { - assert_eq!(ClientFormat::Claude.as_str(), "claude"); - assert_eq!(ClientFormat::Codex.as_str(), "codex"); - assert_eq!(ClientFormat::OpenAI.as_str(), "openai"); - assert_eq!(ClientFormat::Gemini.as_str(), "gemini"); - assert_eq!(ClientFormat::GeminiCli.as_str(), "gemini_cli"); - assert_eq!(ClientFormat::Unknown.as_str(), "unknown"); - } - // ========== Session ID 提取测试 ========== #[test] diff --git a/src-tauri/src/proxy/types.rs b/src-tauri/src/proxy/types.rs index 933db3c53..358e68822 100644 --- a/src-tauri/src/proxy/types.rs +++ b/src-tauri/src/proxy/types.rs @@ -118,15 +118,6 @@ pub struct ProxyTakeoverStatus { pub openclaw: bool, } -/// API 格式类型(预留,当前不需要格式转换) -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[allow(dead_code)] -pub enum ApiFormat { - Claude, - OpenAI, - Gemini, -} - /// Provider健康状态 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ProviderHealth { diff --git a/src-tauri/src/proxy/usage/calculator.rs b/src-tauri/src/proxy/usage/calculator.rs index 089c8478e..0defaeda1 100644 --- a/src-tauri/src/proxy/usage/calculator.rs +++ b/src-tauri/src/proxy/usage/calculator.rs @@ -112,16 +112,6 @@ impl CostCalculator { } } - /// 尝试计算成本,如果模型未知则返回 None - #[allow(dead_code)] - pub fn try_calculate( - usage: &TokenUsage, - pricing: Option<&ModelPricing>, - cost_multiplier: Decimal, - ) -> Option { - pricing.map(|p| Self::calculate(usage, p, cost_multiplier)) - } - pub fn try_calculate_for_app( app_type: &str, usage: &TokenUsage, @@ -253,23 +243,6 @@ mod tests { assert_eq!(cost.total_cost, Decimal::from_str("0.0045").unwrap()); } - #[test] - fn test_unknown_model_handling() { - let usage = TokenUsage { - input_tokens: 1000, - output_tokens: 500, - cache_read_tokens: 0, - cache_creation_tokens: 0, - model: None, - message_id: None, - }; - - let multiplier = Decimal::from_str("1.0").unwrap(); - let cost = CostCalculator::try_calculate(&usage, None, multiplier); - - assert!(cost.is_none()); - } - #[test] fn test_decimal_precision() { let usage = TokenUsage { diff --git a/src-tauri/src/proxy/usage/mod.rs b/src-tauri/src/proxy/usage/mod.rs index 052ec8e08..08ef51018 100644 --- a/src-tauri/src/proxy/usage/mod.rs +++ b/src-tauri/src/proxy/usage/mod.rs @@ -12,4 +12,4 @@ pub use calculator::{CostBreakdown, CostCalculator, ModelPricing}; #[allow(unused_imports)] pub use logger::{RequestLog, UsageLogger}; #[allow(unused_imports)] -pub use parser::{ApiType, TokenUsage}; +pub use parser::TokenUsage; diff --git a/src-tauri/src/proxy/usage/parser.rs b/src-tauri/src/proxy/usage/parser.rs index 6f438595f..44fc64585 100644 --- a/src-tauri/src/proxy/usage/parser.rs +++ b/src-tauri/src/proxy/usage/parser.rs @@ -82,16 +82,6 @@ impl TokenUsage { } } -/// API 类型 -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[allow(dead_code)] -pub enum ApiType { - Claude, - OpenRouter, - Codex, - Gemini, -} - impl TokenUsage { /// 从 Claude API 非流式响应解析 pub fn from_claude_response(body: &Value) -> Option { @@ -238,20 +228,6 @@ impl TokenUsage { } } - /// 从 OpenRouter 响应解析 (OpenAI 格式) - #[allow(dead_code)] - pub fn from_openrouter_response(body: &Value) -> Option { - let usage = body.get("usage")?; - Some(Self { - input_tokens: usage.get("prompt_tokens")?.as_u64()? as u32, - output_tokens: usage.get("completion_tokens")?.as_u64()? as u32, - cache_read_tokens: 0, - cache_creation_tokens: 0, - model: None, - message_id: response_id(body, "id"), - }) - } - /// 从 Codex API 非流式响应解析 pub fn from_codex_response(body: &Value) -> Option { let usage = body.get("usage"); @@ -291,60 +267,6 @@ impl TokenUsage { }) } - /// 从 Codex API 响应解析并调整 input_tokens - /// - /// Codex 的 input_tokens 需要减去 cached_tokens 以获得实际计费的 token 数 - /// 公式: adjusted_input = max(input_tokens - cached_tokens, 0) - #[allow(dead_code)] - pub fn from_codex_response_adjusted(body: &Value) -> Option { - let usage = body.get("usage")?; - let input_tokens = usage.get("input_tokens")?.as_u64()? as u32; - let output_tokens = usage.get("output_tokens")?.as_u64()? as u32; - - // 获取 cached_tokens (可能在 cache_read_input_tokens 或 input_tokens_details 中) - let cached_tokens = openai_cache_read_tokens(usage); - let cache_write_tokens = openai_cache_write_tokens(usage); - - // 调整 input_tokens: OpenAI total input 同时包含 cache read/write 两桶。 - let adjusted_input = input_tokens - .saturating_sub(cached_tokens) - .saturating_sub(cache_write_tokens); - - // 提取响应中的模型名称 - let model = body - .get("model") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()); - - Some(Self { - input_tokens: adjusted_input, - output_tokens, - cache_read_tokens: cached_tokens, - cache_creation_tokens: cache_write_tokens, - model, - message_id: response_id(body, "id"), - }) - } - - /// 从 Codex API 流式响应解析 - #[allow(dead_code)] - pub fn from_codex_stream_events(events: &[Value]) -> Option { - log::debug!("[Codex] 解析流式事件,共 {} 个事件", events.len()); - for event in events { - if let Some(event_type) = event.get("type").and_then(|v| v.as_str()) { - log::debug!("[Codex] 事件类型: {event_type}"); - if event_type == "response.completed" { - if let Some(response) = event.get("response") { - log::debug!("[Codex] 找到 response.completed 事件,解析 usage"); - return Self::from_codex_response_adjusted(response); - } - } - } - } - log::debug!("[Codex] 未找到 response.completed 事件"); - None - } - /// 智能 Codex 响应解析 - 自动检测 OpenAI 或 Codex 格式 /// /// Codex 支持两种 API 格式: @@ -750,22 +672,6 @@ mod tests { assert_eq!(usage.model, None); } - #[test] - fn test_openrouter_response_parsing() { - let response = json!({ - "usage": { - "prompt_tokens": 100, - "completion_tokens": 50 - } - }); - - let usage = TokenUsage::from_openrouter_response(&response).unwrap(); - assert_eq!(usage.input_tokens, 100); - assert_eq!(usage.output_tokens, 50); - assert_eq!(usage.cache_read_tokens, 0); - assert_eq!(usage.cache_creation_tokens, 0); - } - #[test] fn test_gemini_response_parsing() { let response = json!({ @@ -883,81 +789,6 @@ mod tests { assert_eq!(usage.input_tokens, 1000); assert_eq!(usage.cache_read_tokens, 300); assert_eq!(usage.cache_creation_tokens, 200); - - let adjusted = TokenUsage::from_codex_response_adjusted(&response).unwrap(); - assert_eq!(adjusted.input_tokens, 500); - assert_eq!(adjusted.cache_read_tokens, 300); - assert_eq!(adjusted.cache_creation_tokens, 200); - } - - #[test] - fn test_codex_response_adjusted() { - let response = json!({ - "usage": { - "input_tokens": 1000, - "output_tokens": 500, - "input_tokens_details": { - "cached_tokens": 300 - } - } - }); - - let usage = TokenUsage::from_codex_response_adjusted(&response).unwrap(); - // input_tokens 应该被调整: 1000 - 300 = 700 - assert_eq!(usage.input_tokens, 700); - assert_eq!(usage.output_tokens, 500); - assert_eq!(usage.cache_read_tokens, 300); - } - - #[test] - fn test_codex_response_adjusted_no_cache() { - let response = json!({ - "usage": { - "input_tokens": 1000, - "output_tokens": 500 - } - }); - - let usage = TokenUsage::from_codex_response_adjusted(&response).unwrap(); - // 没有 cached_tokens,input_tokens 保持不变 - assert_eq!(usage.input_tokens, 1000); - assert_eq!(usage.output_tokens, 500); - assert_eq!(usage.cache_read_tokens, 0); - } - - #[test] - fn test_codex_response_adjusted_cache_read_input_tokens() { - let response = json!({ - "usage": { - "input_tokens": 1000, - "output_tokens": 500, - "cache_read_input_tokens": 200 - } - }); - - let usage = TokenUsage::from_codex_response_adjusted(&response).unwrap(); - assert_eq!(usage.input_tokens, 800); - assert_eq!(usage.output_tokens, 500); - assert_eq!(usage.cache_read_tokens, 200); - } - - #[test] - fn test_codex_response_adjusted_saturating_sub() { - // 测试 cached_tokens > input_tokens 的边界情况 - let response = json!({ - "usage": { - "input_tokens": 100, - "output_tokens": 50, - "input_tokens_details": { - "cached_tokens": 200 - } - } - }); - - let usage = TokenUsage::from_codex_response_adjusted(&response).unwrap(); - // saturating_sub 确保不会下溢 - assert_eq!(usage.input_tokens, 0); - assert_eq!(usage.cache_read_tokens, 200); } #[test] diff --git a/src/components/ColorPicker.tsx b/src/components/ColorPicker.tsx deleted file mode 100644 index 2a5c0b074..000000000 --- a/src/components/ColorPicker.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import React from "react"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { cn } from "@/lib/utils"; -import { useTranslation } from "react-i18next"; - -interface ColorPickerProps { - value?: string; - onValueChange: (color: string) => void; - label?: string; - presets?: string[]; -} - -const DEFAULT_PRESETS = [ - "#00A67E", - "#D4915D", - "#4285F4", - "#FF6A00", - "#00A4FF", - "#FF9900", - "#0078D4", - "#FF0000", - "#1E88E5", - "#6366F1", - "#0F62FE", - "#2932E1", -]; - -export const ColorPicker: React.FC = ({ - value = "#4285F4", - onValueChange, - label, - presets = DEFAULT_PRESETS, -}) => { - const { t } = useTranslation(); - const displayLabel = label ?? t("providerIcon.color", "图标颜色"); - return ( -
- - - {/* 颜色预设 */} -
- {presets.map((color) => ( -
- - {/* 自定义颜色输入 */} -
- onValueChange(e.target.value)} - className="w-16 h-10 p-1 cursor-pointer" - /> - onValueChange(e.target.value)} - placeholder="#4285F4" - className="flex-1 font-mono" - /> -
-
- ); -}; diff --git a/src/components/icons/TerminalIcons.tsx b/src/components/icons/TerminalIcons.tsx deleted file mode 100644 index a7eeecc13..000000000 --- a/src/components/icons/TerminalIcons.tsx +++ /dev/null @@ -1,190 +0,0 @@ -import { SVGProps } from "react"; - -export function ITermIcon(props: SVGProps) { - return ( - - iTerm2 - - - ); -} - -export function AlacrittyIcon(props: SVGProps) { - return ( - - Alacritty - - - ); -} - -export function WezTermIcon(props: SVGProps) { - return ( - - WezTerm - - - ); -} - -export function GhosttyIcon(props: SVGProps) { - return ( - - Ghostty - - - ); -} - -export function KittyIcon(props: SVGProps) { - // Official icon is complex and has fixed width/height/viewBox in original. - // Simplifying viewBox to 0 0 256 256 effectively as original was 240x240 but translated. - // Original viewBox="0 0 240 240" with g transform="translate(0 -812.362)" and elements around y=850. - // 850 - 812 = 38. So it's confusing. - // Let's copy the raw SVG content but adapt it to be a component. - // To make it behave like an icon, we should probably set viewBox="0 0 240 240" and keep the transform. - // It relies on fill colors. If we want it to be monochrome (currentColor), we should remove fills or set them to currentColor. - // However, official icons often have brand colors. The user said "official icon", which implies color. - // But usually in a dropdown we might want monochrome or original color. - // simple-icons are usually monochrome. - // Let's keep Kitty as original color since it's complex, OR mono if it works? - // The kitty icon has multiple paths with different colors. I'll preserve them for now as it's "official". - // If it looks weird in dark mode/light mode, we might need to adjust. - return ( - - - - - - - - - - - - - - - - - - - ); -} diff --git a/src/components/mode-toggle.tsx b/src/components/mode-toggle.tsx deleted file mode 100644 index 9f3d45965..000000000 --- a/src/components/mode-toggle.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Moon, Sun } from "lucide-react"; -import { useTranslation } from "react-i18next"; -import { Button } from "@/components/ui/button"; -import { useTheme } from "@/components/theme-provider"; - -export function ModeToggle() { - const { theme, setTheme } = useTheme(); - const { t } = useTranslation(); - - const toggleTheme = () => { - if (theme === "dark") { - setTheme("light"); - } else { - setTheme("dark"); - } - }; - - return ( - - ); -} diff --git a/src/components/prompts/PromptFormModal.tsx b/src/components/prompts/PromptFormModal.tsx deleted file mode 100644 index dd09fdb30..000000000 --- a/src/components/prompts/PromptFormModal.tsx +++ /dev/null @@ -1,164 +0,0 @@ -import React, { useState, useEffect } from "react"; -import { useTranslation } from "react-i18next"; -import { Button } from "@/components/ui/button"; -import { - Dialog, - DialogContent, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import MarkdownEditor from "@/components/MarkdownEditor"; -import type { Prompt, AppId } from "@/lib/api"; - -interface PromptFormModalProps { - appId: AppId; - editingId?: string; - initialData?: Prompt; - onSave: (id: string, prompt: Prompt) => Promise; - onClose: () => void; -} - -const PromptFormModal: React.FC = ({ - appId, - editingId, - initialData, - onSave, - onClose, -}) => { - const { t } = useTranslation(); - const appName = t(`apps.${appId}`); - const filenameMap: Record, string> = { - claude: "CLAUDE.md", - "claude-desktop": "CLAUDE.md", - codex: "AGENTS.md", - gemini: "GEMINI.md", - grokbuild: "AGENTS.md", - opencode: "AGENTS.md", - hermes: "AGENTS.md", - }; - const filename = filenameMap[appId as Exclude]; - const [name, setName] = useState(""); - const [description, setDescription] = useState(""); - const [content, setContent] = useState(""); - const [saving, setSaving] = useState(false); - const [isDarkMode, setIsDarkMode] = useState(false); - - useEffect(() => { - // 检测初始暗色模式状态 - setIsDarkMode(document.documentElement.classList.contains("dark")); - - // 监听 html 元素的 class 变化以实时响应主题切换 - const observer = new MutationObserver(() => { - setIsDarkMode(document.documentElement.classList.contains("dark")); - }); - - observer.observe(document.documentElement, { - attributes: true, - attributeFilter: ["class"], - }); - - return () => observer.disconnect(); - }, []); - - useEffect(() => { - if (initialData) { - setName(initialData.name); - setDescription(initialData.description || ""); - setContent(initialData.content); - } - }, [initialData]); - - const handleSave = async () => { - if (!name.trim()) { - return; - } - - setSaving(true); - try { - const id = editingId || `prompt-${Date.now()}`; - const timestamp = Math.floor(Date.now() / 1000); - const prompt: Prompt = { - id, - name: name.trim(), - description: description.trim() || undefined, - content: content.trim(), - enabled: initialData?.enabled || false, - createdAt: initialData?.createdAt || timestamp, - updatedAt: timestamp, - }; - await onSave(id, prompt); - onClose(); - } catch (error) { - // Error handled by hook - } finally { - setSaving(false); - } - }; - - return ( - - - - - {editingId - ? t("prompts.editTitle", { appName }) - : t("prompts.addTitle", { appName })} - - - -
-
- - setName(e.target.value)} - placeholder={t("prompts.namePlaceholder")} - /> -
- -
- - setDescription(e.target.value)} - placeholder={t("prompts.descriptionPlaceholder")} - /> -
- -
- - -
-
- - - - - -
-
- ); -}; - -export default PromptFormModal; diff --git a/src/components/providers/HealthStatusIndicator.tsx b/src/components/providers/HealthStatusIndicator.tsx deleted file mode 100644 index 6612daa71..000000000 --- a/src/components/providers/HealthStatusIndicator.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import React from "react"; -import { cn } from "@/lib/utils"; -import type { HealthStatus } from "@/lib/api/connectivity-check"; -import { useTranslation } from "react-i18next"; - -interface HealthStatusIndicatorProps { - status: HealthStatus; - responseTimeMs?: number; - className?: string; -} - -const statusConfig = { - operational: { - color: "bg-emerald-500", - labelKey: "health.operational", - labelFallback: "正常", - textColor: "text-emerald-600 dark:text-emerald-400", - }, - degraded: { - color: "bg-yellow-500", - labelKey: "health.degraded", - labelFallback: "降级", - textColor: "text-yellow-600 dark:text-yellow-400", - }, - failed: { - color: "bg-red-500", - labelKey: "health.failed", - labelFallback: "失败", - textColor: "text-red-600 dark:text-red-400", - }, -}; - -export const HealthStatusIndicator: React.FC = ({ - status, - responseTimeMs, - className, -}) => { - const { t } = useTranslation(); - const config = statusConfig[status]; - const label = t(config.labelKey, { defaultValue: config.labelFallback }); - - return ( -
-
- - {label} - {responseTimeMs !== undefined && ` (${responseTimeMs}ms)`} - -
- ); -}; diff --git a/src/components/providers/forms/hooks/index.ts b/src/components/providers/forms/hooks/index.ts index 3c98c2645..4e61b84d2 100644 --- a/src/components/providers/forms/hooks/index.ts +++ b/src/components/providers/forms/hooks/index.ts @@ -4,7 +4,6 @@ export { useBaseUrlState } from "./useBaseUrlState"; export { useModelState } from "./useModelState"; export { useCodexConfigState } from "./useCodexConfigState"; export { useApiKeyLink } from "./useApiKeyLink"; -export { useCustomEndpoints } from "./useCustomEndpoints"; export { useTemplateValues } from "./useTemplateValues"; export { useCommonConfigSnippet } from "./useCommonConfigSnippet"; export { useCodexCommonConfig } from "./useCodexCommonConfig"; diff --git a/src/components/providers/forms/hooks/useCustomEndpoints.ts b/src/components/providers/forms/hooks/useCustomEndpoints.ts deleted file mode 100644 index 77ff88d21..000000000 --- a/src/components/providers/forms/hooks/useCustomEndpoints.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { useMemo } from "react"; -import type { AppId } from "@/lib/api"; -import type { CustomEndpoint } from "@/types"; -import type { ProviderPreset } from "@/config/claudeProviderPresets"; -import type { CodexProviderPreset } from "@/config/codexProviderPresets"; - -type PresetEntry = { - id: string; - preset: ProviderPreset | CodexProviderPreset; -}; - -interface UseCustomEndpointsProps { - appId: AppId; - selectedPresetId: string | null; - presetEntries: PresetEntry[]; - draftCustomEndpoints: string[]; - baseUrl: string; - codexBaseUrl: string; -} - -/** - * 收集和管理自定义端点 - * - * 收集来源: - * 1. 用户在测速弹窗中新增的自定义端点 - * 2. 预设中的 endpointCandidates - * 3. 当前选中的 Base URL - */ -export function useCustomEndpoints({ - appId, - selectedPresetId, - presetEntries, - draftCustomEndpoints, - baseUrl, - codexBaseUrl, -}: UseCustomEndpointsProps) { - const customEndpointsMap = useMemo(() => { - const urlSet = new Set(); - - // 辅助函数:标准化并添加 URL - const push = (raw?: string) => { - const url = (raw || "").trim().replace(/\/+$/, ""); - if (url) urlSet.add(url); - }; - - // 1. 自定义端点(来自用户新增) - for (const u of draftCustomEndpoints) push(u); - - // 2. 预设端点候选 - if (selectedPresetId && selectedPresetId !== "custom") { - const entry = presetEntries.find((item) => item.id === selectedPresetId); - if (entry) { - const preset = entry.preset as any; - if (Array.isArray(preset?.endpointCandidates)) { - for (const u of preset.endpointCandidates as string[]) push(u); - } - } - } - - // 3. 当前 Base URL - if (appId === "codex") { - push(codexBaseUrl); - } else { - push(baseUrl); - } - - // 构建 CustomEndpoint map - const urls = Array.from(urlSet.values()); - if (urls.length === 0) { - return null; - } - - const now = Date.now(); - const customMap: Record = {}; - for (const url of urls) { - if (!customMap[url]) { - customMap[url] = { url, addedAt: now, lastUsed: undefined }; - } - } - - return customMap; - }, [ - appId, - selectedPresetId, - presetEntries, - draftCustomEndpoints, - baseUrl, - codexBaseUrl, - ]); - - return customEndpointsMap; -} diff --git a/src/components/proxy/CircuitBreakerConfigPanel.tsx b/src/components/proxy/CircuitBreakerConfigPanel.tsx deleted file mode 100644 index 31c72b756..000000000 --- a/src/components/proxy/CircuitBreakerConfigPanel.tsx +++ /dev/null @@ -1,356 +0,0 @@ -import { - useCircuitBreakerConfig, - useUpdateCircuitBreakerConfig, -} from "@/lib/query/failover"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Button } from "@/components/ui/button"; -import { useState, useEffect } from "react"; -import { toast } from "sonner"; -import { useTranslation } from "react-i18next"; - -/** - * 熔断器配置面板 - * 允许用户调整熔断器参数 - */ -export function CircuitBreakerConfigPanel() { - const { t } = useTranslation(); - const { data: config, isLoading } = useCircuitBreakerConfig(); - const updateConfig = useUpdateCircuitBreakerConfig(); - - // 使用字符串状态以支持完全清空输入框 - const [formData, setFormData] = useState({ - failureThreshold: "5", - successThreshold: "2", - timeoutSeconds: "60", - errorRateThreshold: "50", // 存储百分比值 - minRequests: "10", - }); - - // 当配置加载完成时更新表单数据 - useEffect(() => { - if (config) { - setFormData({ - failureThreshold: String(config.failureThreshold), - successThreshold: String(config.successThreshold), - timeoutSeconds: String(config.timeoutSeconds), - errorRateThreshold: String(Math.round(config.errorRateThreshold * 100)), - minRequests: String(config.minRequests), - }); - } - }, [config]); - - const handleSave = async () => { - // 解析数字,返回 NaN 表示无效输入 - const parseNum = (val: string) => { - const trimmed = val.trim(); - // 必须是纯数字 - if (!/^-?\d+$/.test(trimmed)) return NaN; - return parseInt(trimmed); - }; - - // 定义各字段的有效范围 - const ranges = { - failureThreshold: { min: 1, max: 20 }, - successThreshold: { min: 1, max: 10 }, - timeoutSeconds: { min: 0, max: 300 }, - errorRateThreshold: { min: 0, max: 100 }, - minRequests: { min: 5, max: 100 }, - }; - - // 解析原始值 - const raw = { - failureThreshold: parseNum(formData.failureThreshold), - successThreshold: parseNum(formData.successThreshold), - timeoutSeconds: parseNum(formData.timeoutSeconds), - errorRateThreshold: parseNum(formData.errorRateThreshold), - minRequests: parseNum(formData.minRequests), - }; - - // 校验是否超出范围(NaN 也视为无效) - const errors: string[] = []; - const checkRange = ( - value: number, - range: { min: number; max: number }, - label: string, - ) => { - if (isNaN(value) || value < range.min || value > range.max) { - errors.push(`${label}: ${range.min}-${range.max}`); - } - }; - - checkRange( - raw.failureThreshold, - ranges.failureThreshold, - t("circuitBreaker.failureThreshold", "失败阈值"), - ); - checkRange( - raw.successThreshold, - ranges.successThreshold, - t("circuitBreaker.successThreshold", "成功阈值"), - ); - checkRange( - raw.timeoutSeconds, - ranges.timeoutSeconds, - t("circuitBreaker.timeoutSeconds", "超时时间"), - ); - checkRange( - raw.errorRateThreshold, - ranges.errorRateThreshold, - t("circuitBreaker.errorRateThreshold", "错误率阈值"), - ); - checkRange( - raw.minRequests, - ranges.minRequests, - t("circuitBreaker.minRequests", "最小请求数"), - ); - - if (errors.length > 0) { - toast.error( - t("circuitBreaker.validationFailed", { - fields: errors.join("; "), - defaultValue: `以下字段超出有效范围: ${errors.join("; ")}`, - }), - ); - return; - } - - try { - await updateConfig.mutateAsync({ - failureThreshold: raw.failureThreshold, - successThreshold: raw.successThreshold, - timeoutSeconds: raw.timeoutSeconds, - errorRateThreshold: raw.errorRateThreshold / 100, - minRequests: raw.minRequests, - }); - toast.success(t("circuitBreaker.configSaved", "熔断器配置已保存"), { - closeButton: true, - }); - } catch (error) { - toast.error( - t("circuitBreaker.saveFailed", "保存失败") + ": " + String(error), - ); - } - }; - - const handleReset = () => { - if (config) { - setFormData({ - failureThreshold: String(config.failureThreshold), - successThreshold: String(config.successThreshold), - timeoutSeconds: String(config.timeoutSeconds), - errorRateThreshold: String(Math.round(config.errorRateThreshold * 100)), - minRequests: String(config.minRequests), - }); - } - }; - - if (isLoading) { - return ( -
- {t("circuitBreaker.loading", "加载中...")} -
- ); - } - - return ( -
-
-

- {t("circuitBreaker.title", "熔断器配置")} -

-

- {t( - "circuitBreaker.description", - "调整熔断器参数以控制故障检测和恢复行为", - )} -

-
- -
- -
- {/* 失败阈值 */} -
- - - setFormData({ ...formData, failureThreshold: e.target.value }) - } - /> -

- {t( - "circuitBreaker.failureThresholdHint", - "连续失败多少次后打开熔断器", - )} -

-
- - {/* 超时时间 */} -
- - - setFormData({ ...formData, timeoutSeconds: e.target.value }) - } - /> -

- {t( - "circuitBreaker.timeoutSecondsHint", - "熔断器打开后多久尝试恢复(半开状态)", - )} -

-
- - {/* 成功阈值 */} -
- - - setFormData({ ...formData, successThreshold: e.target.value }) - } - /> -

- {t( - "circuitBreaker.successThresholdHint", - "半开状态下成功多少次后关闭熔断器", - )} -

-
- - {/* 错误率阈值 */} -
- - - setFormData({ ...formData, errorRateThreshold: e.target.value }) - } - /> -

- {t( - "circuitBreaker.errorRateThresholdHint", - "错误率超过此值时打开熔断器", - )} -

-
- - {/* 最小请求数 */} -
- - - setFormData({ ...formData, minRequests: e.target.value }) - } - /> -

- {t("circuitBreaker.minRequestsHint", "计算错误率前的最小请求数")} -

-
-
- -
- - -
- - {/* 说明信息 */} -
-

- {t("circuitBreaker.instructionsTitle", "配置说明")} -

-
    -
  • - •{" "} - {t("circuitBreaker.failureThreshold", "失败阈值")} - : - {t( - "circuitBreaker.instructions.failureThreshold", - "连续失败达到此次数时,熔断器打开", - )} -
  • -
  • - • {t("circuitBreaker.timeoutSeconds", "超时时间")} - : - {t( - "circuitBreaker.instructions.timeout", - "熔断器打开后,等待此时间后尝试半开", - )} -
  • -
  • - •{" "} - {t("circuitBreaker.successThreshold", "成功阈值")} - : - {t( - "circuitBreaker.instructions.successThreshold", - "半开状态下,成功达到此次数时关闭熔断器", - )} -
  • -
  • - •{" "} - - {t("circuitBreaker.errorRateThreshold", "错误率阈值")} - - : - {t( - "circuitBreaker.instructions.errorRate", - "错误率超过此值时,熔断器打开", - )} -
  • -
  • - • {t("circuitBreaker.minRequests", "最小请求数")}: - {t( - "circuitBreaker.instructions.minRequests", - "只有请求数达到此值后才计算错误率", - )} -
  • -
-
-
- ); -} diff --git a/src/components/skills/RepoManager.tsx b/src/components/skills/RepoManager.tsx deleted file mode 100644 index 69fee8d9f..000000000 --- a/src/components/skills/RepoManager.tsx +++ /dev/null @@ -1,202 +0,0 @@ -import { useState } from "react"; -import { useTranslation } from "react-i18next"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Trash2, ExternalLink, Plus } from "lucide-react"; -import { settingsApi } from "@/lib/api"; -import type { DiscoverableSkill, SkillRepo } from "@/lib/api/skills"; - -interface RepoManagerProps { - open: boolean; - onOpenChange: (open: boolean) => void; - repos: SkillRepo[]; - skills: DiscoverableSkill[]; - onAdd: (repo: SkillRepo) => Promise; - onRemove: (owner: string, name: string) => Promise; -} - -export function RepoManager({ - open: isOpen, - onOpenChange, - repos, - skills, - onAdd, - onRemove, -}: RepoManagerProps) { - const { t } = useTranslation(); - const [repoUrl, setRepoUrl] = useState(""); - const [branch, setBranch] = useState(""); - const [error, setError] = useState(""); - - const getSkillCount = (repo: SkillRepo) => - skills.filter( - (skill) => - skill.repoOwner === repo.owner && - skill.repoName === repo.name && - (skill.repoBranch || "main") === (repo.branch || "main"), - ).length; - - const parseRepoUrl = ( - url: string, - ): { owner: string; name: string } | null => { - // 支持格式: - // - https://github.com/owner/name - // - owner/name - // - https://github.com/owner/name.git - - let cleaned = url.trim(); - cleaned = cleaned.replace(/^https?:\/\/github\.com\//, ""); - cleaned = cleaned.replace(/\.git$/, ""); - - const parts = cleaned.split("/"); - if (parts.length === 2 && parts[0] && parts[1]) { - return { owner: parts[0], name: parts[1] }; - } - - return null; - }; - - const handleAdd = async () => { - setError(""); - - const parsed = parseRepoUrl(repoUrl); - if (!parsed) { - setError(t("skills.repo.invalidUrl")); - return; - } - - try { - await onAdd({ - owner: parsed.owner, - name: parsed.name, - branch: branch || "main", - enabled: true, - }); - - setRepoUrl(""); - setBranch(""); - } catch (e) { - setError(e instanceof Error ? e.message : t("skills.repo.addFailed")); - } - }; - - const handleOpenRepo = async (owner: string, name: string) => { - try { - await settingsApi.openExternal(`https://github.com/${owner}/${name}`); - } catch (error) { - console.error("Failed to open URL:", error); - } - }; - - return ( - - - {/* 固定头部 */} - - {t("skills.repo.title")} - {t("skills.repo.description")} - - - {/* 可滚动内容区域 */} -
- {/* 添加仓库表单 */} -
-
- -
- setRepoUrl(e.target.value)} - className="flex-1" - /> -
- setBranch(e.target.value)} - className="flex-1" - /> - -
-
- {error &&

{error}

} -
- - {/* 仓库列表 */} -
-

{t("skills.repo.list")}

- {repos.length === 0 ? ( -

- {t("skills.repo.empty")} -

- ) : ( -
- {repos.map((repo) => ( -
-
-
- {repo.owner}/{repo.name} -
-
- {t("skills.repo.branch")}: {repo.branch || "main"} - - {t("skills.repo.skillCount", { - count: getSkillCount(repo), - })} - -
-
-
- - -
-
- ))} -
- )} -
-
-
-
-
- ); -} diff --git a/src/components/usage/DataSourceBar.tsx b/src/components/usage/DataSourceBar.tsx deleted file mode 100644 index cad6b0e70..000000000 --- a/src/components/usage/DataSourceBar.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { useQuery, useQueryClient } from "@tanstack/react-query"; -import { usageApi } from "@/lib/api/usage"; -import { usageKeys } from "@/lib/query/usage"; -import { Database, FileText, RefreshCw, Loader2 } from "lucide-react"; -import { Button } from "@/components/ui/button"; -import { useState } from "react"; -import { toast } from "sonner"; - -interface DataSourceBarProps { - refreshIntervalMs: number; -} - -const DATA_SOURCE_ICONS: Record = { - proxy: , - session_log: , - codex_db: , - codex_session: , - gemini_session: , - opencode_session: , - grok_session: , -}; - -export function DataSourceBar({ refreshIntervalMs }: DataSourceBarProps) { - const { t } = useTranslation(); - const queryClient = useQueryClient(); - const [syncing, setSyncing] = useState(false); - - const { data: sources } = useQuery({ - queryKey: [...usageKeys.all, "data-sources"], - queryFn: usageApi.getDataSourceBreakdown, - refetchInterval: refreshIntervalMs > 0 ? refreshIntervalMs : false, - refetchIntervalInBackground: false, - }); - - const handleSync = async () => { - setSyncing(true); - try { - const result = await usageApi.syncSessionUsage(); - if (result.imported > 0) { - toast.success( - t("usage.sessionSync.imported", { - count: result.imported, - defaultValue: "Imported {{count}} records from session logs", - }), - ); - // Refresh all usage data - queryClient.invalidateQueries({ queryKey: usageKeys.all }); - } else { - toast.info( - t("usage.sessionSync.upToDate", { - defaultValue: "Session logs are up to date", - }), - ); - } - } catch { - toast.error( - t("usage.sessionSync.failed", { - defaultValue: "Session sync failed", - }), - ); - } finally { - setSyncing(false); - } - }; - - if (!sources || sources.length === 0) { - return null; - } - - const hasNonProxy = sources.some((s) => s.dataSource !== "proxy"); - - return ( -
- - {t("usage.dataSources", { defaultValue: "Data Sources" })}: - -
- {sources.map((source) => ( -
- {DATA_SOURCE_ICONS[source.dataSource] ?? ( - - )} - - {t(`usage.dataSource.${source.dataSource}`, { - defaultValue: source.dataSource, - })} - - - {source.requestCount.toLocaleString()} - -
- ))} -
- -
- -
-
- ); -} diff --git a/src/config/iconInference.ts b/src/config/iconInference.ts deleted file mode 100644 index 562610146..000000000 --- a/src/config/iconInference.ts +++ /dev/null @@ -1,77 +0,0 @@ -/** - * 根据供应商名称智能推断图标配置 - */ - -const iconMappings = { - // AI 服务商 - claude: { icon: "claude", iconColor: "#D4915D" }, - anthropic: { icon: "anthropic", iconColor: "#D4915D" }, - deepseek: { icon: "deepseek", iconColor: "#1E88E5" }, - zhipu: { icon: "zhipu", iconColor: "#0F62FE" }, - glm: { icon: "zhipu", iconColor: "#0F62FE" }, - qwen: { icon: "qwen", iconColor: "#FF6A00" }, - bailian: { icon: "bailian", iconColor: "#624AFF" }, - alibaba: { icon: "alibaba", iconColor: "#FF6A00" }, - aliyun: { icon: "alibaba", iconColor: "#FF6A00" }, - kimi: { icon: "kimi", iconColor: "#6366F1" }, - moonshot: { icon: "moonshot", iconColor: "#6366F1" }, - stepfun: { icon: "stepfun", iconColor: "#005AFF" }, - step: { icon: "stepfun", iconColor: "#005AFF" }, - baidu: { icon: "baidu", iconColor: "#2932E1" }, - tencent: { icon: "tencent", iconColor: "#00A4FF" }, - hunyuan: { icon: "hunyuan", iconColor: "#00A4FF" }, - minimax: { icon: "minimax", iconColor: "#FF6B6B" }, - google: { icon: "google", iconColor: "#4285F4" }, - meta: { icon: "meta", iconColor: "#0081FB" }, - mistral: { icon: "mistral", iconColor: "#FF7000" }, - cohere: { icon: "cohere", iconColor: "#39594D" }, - perplexity: { icon: "perplexity", iconColor: "#20808D" }, - huggingface: { icon: "huggingface", iconColor: "#FFD21E" }, - novita: { icon: "novita", iconColor: "#000000" }, - - // 云平台 - aws: { icon: "aws", iconColor: "#FF9900" }, - azure: { icon: "azure", iconColor: "#0078D4" }, - huawei: { icon: "huawei", iconColor: "#FF0000" }, - cloudflare: { icon: "cloudflare", iconColor: "#F38020" }, -}; - -/** - * 根据预设名称推断图标 - */ -export function inferIconForPreset(presetName: string): { - icon?: string; - iconColor?: string; -} { - const nameLower = presetName.toLowerCase(); - - // 精确匹配或模糊匹配 - for (const [key, config] of Object.entries(iconMappings)) { - if (nameLower.includes(key)) { - return config; - } - } - - return {}; -} - -/** - * 批量为预设添加图标配置 - */ -export function addIconsToPresets< - T extends { name: string; icon?: string; iconColor?: string }, ->(presets: T[]): T[] { - return presets.map((preset) => { - // 如果已经配置了图标,则保留原配置 - if (preset.icon) { - return preset; - } - - // 否则根据名称推断 - const inferred = inferIconForPreset(preset.name); - return { - ...preset, - ...inferred, - }; - }); -} diff --git a/src/hooks/useProxyConfig.ts b/src/hooks/useProxyConfig.ts deleted file mode 100644 index 92ba1d3c5..000000000 --- a/src/hooks/useProxyConfig.ts +++ /dev/null @@ -1,48 +0,0 @@ -/** - * 代理配置管理 Hook - */ - -import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; -import { invoke } from "@tauri-apps/api/core"; -import { toast } from "sonner"; -import { useTranslation } from "react-i18next"; -import type { ProxyConfig } from "@/types/proxy"; - -/** - * 代理配置管理 - */ -export function useProxyConfig() { - const queryClient = useQueryClient(); - const { t } = useTranslation(); - - // 查询配置 - const { data: config, isLoading } = useQuery({ - queryKey: ["proxyConfig"], - queryFn: () => invoke("get_proxy_config"), - }); - - // 更新配置 - const updateMutation = useMutation({ - mutationFn: (newConfig: ProxyConfig) => - invoke("update_proxy_config", { config: newConfig }), - onSuccess: () => { - toast.success(t("proxy.settings.toast.saved"), { closeButton: true }); - queryClient.invalidateQueries({ queryKey: ["proxyConfig"] }); - queryClient.invalidateQueries({ queryKey: ["proxyStatus"] }); - }, - onError: (error: Error) => { - toast.error( - t("proxy.settings.toast.saveFailed", { - error: error.message, - }), - ); - }, - }); - - return { - config, - isLoading, - updateConfig: updateMutation.mutateAsync, - isUpdating: updateMutation.isPending, - }; -} diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index f9214cfa5..499299981 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -23,7 +23,6 @@ "unknown": "Unknown", "enterValidValue": "Please enter a valid value", "clear": "Clear", - "toggleTheme": "Toggle theme", "format": "Format", "formatSuccess": "Formatted successfully", "formatError": "Format failed: {{error}}", @@ -1507,24 +1506,6 @@ "grokbuild": "Grok Build" }, "rawInputLabel": "Raw", - "dataSources": "Data Sources", - "dataSource": { - "proxy": "Routing", - "session_log": "Session Log", - "codex_db": "Codex DB", - "codex_session": "Codex Session", - "gemini_session": "Gemini Session", - "opencode_session": "OpenCode Session", - "grok_session": "Grok Build Session" - }, - "sessionSync": { - "trigger": "Sync session logs", - "import": "Import Sessions", - "resync": "Sync", - "imported": "Imported {{count}} records from session logs", - "upToDate": "Session logs are up to date", - "failed": "Session sync failed" - }, "rebuildCodex": { "title": "Codex Usage Maintenance", "description": "Rebuild Codex session usage from local rollout logs", @@ -2539,8 +2520,7 @@ "selectIcon": "Select Icon", "preview": "Preview", "clickToChange": "Click to change icon", - "clickToSelect": "Click to select icon", - "color": "Icon Color" + "clickToSelect": "Click to select icon" }, "migration": { "success": "Configuration migrated successfully", @@ -2782,33 +2762,6 @@ "streamingIdle": "Streaming Idle Timeout", "nonStreaming": "Non-Streaming Timeout" }, - "circuitBreaker": { - "failureThreshold": "Failure Threshold", - "successThreshold": "Success Threshold", - "timeoutSeconds": "Timeout (seconds)", - "errorRateThreshold": "Error Rate Threshold (%)", - "minRequests": "Minimum Requests", - "validationFailed": "The following fields are out of valid range: {{fields}}", - "configSaved": "Circuit breaker configuration saved", - "saveFailed": "Save failed", - "loading": "Loading...", - "title": "Circuit Breaker Configuration", - "description": "Adjust circuit breaker parameters to control fault detection and recovery behavior", - "failureThresholdHint": "How many consecutive failures trigger the circuit breaker", - "timeoutSecondsHint": "How long to wait before attempting recovery (half-open state)", - "successThresholdHint": "How many successes in half-open state to close the circuit breaker", - "errorRateThresholdHint": "Open circuit breaker when error rate exceeds this value", - "minRequestsHint": "Minimum requests before calculating error rate", - "saveConfig": "Save Configuration", - "instructionsTitle": "Configuration Instructions", - "instructions": { - "failureThreshold": "Circuit breaker opens when consecutive failures reach this count", - "timeout": "After circuit breaker opens, wait this time before attempting half-open", - "successThreshold": "In half-open state, close circuit breaker when successes reach this count", - "errorRate": "Circuit breaker opens when error rate exceeds this value", - "minRequests": "Error rate is only calculated after request count reaches this value" - } - }, "universalProvider": { "duplicate": "Duplicate", "duplicatedAndSynced": "Universal provider duplicated and synced", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 547884426..8651cd053 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -23,7 +23,6 @@ "unknown": "不明", "enterValidValue": "有効な値を入力してください", "clear": "クリア", - "toggleTheme": "テーマを切り替え", "format": "フォーマット", "formatSuccess": "整形しました", "formatError": "整形に失敗しました: {{error}}", @@ -1507,24 +1506,6 @@ "grokbuild": "Grok Build" }, "rawInputLabel": "原始", - "dataSources": "データソース", - "dataSource": { - "proxy": "ルーティング", - "session_log": "セッションログ", - "codex_db": "Codex DB", - "codex_session": "Codex セッション", - "gemini_session": "Gemini セッション", - "opencode_session": "OpenCode セッション", - "grok_session": "Grok Build セッション" - }, - "sessionSync": { - "trigger": "セッションログを同期", - "import": "セッションをインポート", - "resync": "同期", - "imported": "セッションログから {{count}} 件のレコードをインポートしました", - "upToDate": "セッションログは最新です", - "failed": "セッション同期に失敗しました" - }, "rebuildCodex": { "title": "Codex 使用量メンテナンス", "description": "ローカルの rollout ログから Codex セッション使用量を再構築します", @@ -2539,8 +2520,7 @@ "selectIcon": "アイコンを選択", "preview": "プレビュー", "clickToChange": "クリックでアイコンを変更", - "clickToSelect": "クリックでアイコンを選択", - "color": "アイコンカラー" + "clickToSelect": "クリックでアイコンを選択" }, "migration": { "success": "設定の移行が完了しました", @@ -2782,33 +2762,6 @@ "streamingIdle": "ストリーミングアイドルタイムアウト", "nonStreaming": "非ストリーミングタイムアウト" }, - "circuitBreaker": { - "failureThreshold": "失敗閾値", - "successThreshold": "成功閾値", - "timeoutSeconds": "タイムアウト(秒)", - "errorRateThreshold": "エラー率閾値 (%)", - "minRequests": "最小リクエスト数", - "validationFailed": "以下のフィールドが有効範囲外です: {{fields}}", - "configSaved": "サーキットブレーカー設定が保存されました", - "saveFailed": "保存に失敗しました", - "loading": "読み込み中...", - "title": "サーキットブレーカー設定", - "description": "サーキットブレーカーパラメータを調整して、障害検出と復旧動作を制御します", - "failureThresholdHint": "連続失敗後にサーキットブレーカーを開く回数", - "timeoutSecondsHint": "サーキットブレーカーを開いた後、復旧を試みるまでの時間(半開状態)", - "successThresholdHint": "半開状態で成功してサーキットブレーカーを閉じる回数", - "errorRateThresholdHint": "エラー率がこの値を超えるとサーキットブレーカーを開く", - "minRequestsHint": "エラー率を計算する前の最小リクエスト数", - "saveConfig": "設定を保存", - "instructionsTitle": "設定説明", - "instructions": { - "failureThreshold": "連続失敗がこの回数に達するとサーキットブレーカーが開く", - "timeout": "サーキットブレーカーを開いた後、この時間待機してから半開を試みる", - "successThreshold": "半開状態で成功がこの回数に達するとサーキットブレーカーを閉じる", - "errorRate": "エラー率がこの値を超えるとサーキットブレーカーが開く", - "minRequests": "リクエスト数がこの値に達した後にのみエラー率が計算される" - } - }, "universalProvider": { "duplicate": "複製", "duplicatedAndSynced": "統合プロバイダーを複製して同期しました", diff --git a/src/i18n/locales/zh-TW.json b/src/i18n/locales/zh-TW.json index eb1d5ecd6..a1e647c1d 100644 --- a/src/i18n/locales/zh-TW.json +++ b/src/i18n/locales/zh-TW.json @@ -23,7 +23,6 @@ "unknown": "未知", "enterValidValue": "請輸入有效的內容", "clear": "清除", - "toggleTheme": "切換主題", "format": "格式化", "formatSuccess": "格式化成功", "formatError": "格式化失敗:{{error}}", @@ -1478,24 +1477,6 @@ "grokbuild": "Grok Build" }, "rawInputLabel": "原始", - "dataSources": "資料來源", - "dataSource": { - "proxy": "路由", - "session_log": "工作階段日誌", - "codex_db": "Codex 資料庫", - "codex_session": "Codex 工作階段日誌", - "gemini_session": "Gemini 工作階段日誌", - "opencode_session": "OpenCode 工作階段日誌", - "grok_session": "Grok Build 工作階段日誌" - }, - "sessionSync": { - "trigger": "同步工作階段日誌", - "import": "匯入工作階段", - "resync": "同步", - "imported": "從工作階段日誌匯入了 {{count}} 筆紀錄", - "upToDate": "工作階段日誌已是最新", - "failed": "工作階段同步失敗" - }, "rebuildCodex": { "title": "Codex 用量維護", "description": "從本機 rollout 日誌重新建構 Codex 工作階段用量", @@ -2510,8 +2491,7 @@ "selectIcon": "選擇圖示", "preview": "預覽", "clickToChange": "點擊更換圖示", - "clickToSelect": "點擊選擇圖示", - "color": "圖示顏色" + "clickToSelect": "點擊選擇圖示" }, "migration": { "success": "設定遷移成功", @@ -2753,33 +2733,6 @@ "streamingIdle": "串流閒置逾時", "nonStreaming": "非串流逾時" }, - "circuitBreaker": { - "failureThreshold": "失敗閾值", - "successThreshold": "成功閾值", - "timeoutSeconds": "逾時時間(秒)", - "errorRateThreshold": "錯誤率閾值 (%)", - "minRequests": "最小請求數", - "validationFailed": "以下欄位超出有效範圍:{{fields}}", - "configSaved": "斷路器設定已儲存", - "saveFailed": "儲存失敗", - "loading": "載入中...", - "title": "斷路器設定", - "description": "調整斷路器參數以控制故障檢測和恢復行為", - "failureThresholdHint": "連續失敗多少次後打開斷路器", - "timeoutSecondsHint": "斷路器打開後多久嘗試恢復(半開狀態)", - "successThresholdHint": "半開狀態下成功多少次後關閉斷路器", - "errorRateThresholdHint": "錯誤率超過此值時打開斷路器", - "minRequestsHint": "計算錯誤率前的最小請求數", - "saveConfig": "儲存設定", - "instructionsTitle": "設定說明", - "instructions": { - "failureThreshold": "連續失敗達到此次數時,斷路器打開", - "timeout": "斷路器打開後,等待此時間後嘗試半開", - "successThreshold": "半開狀態下,成功達到此次數時關閉斷路器", - "errorRate": "錯誤率超過此值時,斷路器打開", - "minRequests": "只有請求數達到此值後才計算錯誤率" - } - }, "universalProvider": { "duplicate": "複製", "duplicatedAndSynced": "通用供應商已複製並同步", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index 12f8686ac..5af4d6a15 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -23,7 +23,6 @@ "unknown": "未知", "enterValidValue": "请输入有效的内容", "clear": "清除", - "toggleTheme": "切换主题", "format": "格式化", "formatSuccess": "格式化成功", "formatError": "格式化失败:{{error}}", @@ -1507,24 +1506,6 @@ "grokbuild": "Grok Build" }, "rawInputLabel": "原始", - "dataSources": "数据来源", - "dataSource": { - "proxy": "路由", - "session_log": "会话日志", - "codex_db": "Codex 数据库", - "codex_session": "Codex 会话日志", - "gemini_session": "Gemini 会话日志", - "opencode_session": "OpenCode 会话日志", - "grok_session": "Grok Build 会话日志" - }, - "sessionSync": { - "trigger": "同步会话日志", - "import": "导入会话", - "resync": "同步", - "imported": "从会话日志导入了 {{count}} 条记录", - "upToDate": "会话日志已是最新", - "failed": "会话同步失败" - }, "rebuildCodex": { "title": "Codex 用量维护", "description": "从本地 rollout 日志重新构建 Codex 会话用量", @@ -2539,8 +2520,7 @@ "selectIcon": "选择图标", "preview": "预览", "clickToChange": "点击更换图标", - "clickToSelect": "点击选择图标", - "color": "图标颜色" + "clickToSelect": "点击选择图标" }, "migration": { "success": "配置迁移成功", @@ -2782,33 +2762,6 @@ "streamingIdle": "流式静默超时", "nonStreaming": "非流式超时" }, - "circuitBreaker": { - "failureThreshold": "失败阈值", - "successThreshold": "成功阈值", - "timeoutSeconds": "超时时间(秒)", - "errorRateThreshold": "错误率阈值 (%)", - "minRequests": "最小请求数", - "validationFailed": "以下字段超出有效范围: {{fields}}", - "configSaved": "熔断器配置已保存", - "saveFailed": "保存失败", - "loading": "加载中...", - "title": "熔断器配置", - "description": "调整熔断器参数以控制故障检测和恢复行为", - "failureThresholdHint": "连续失败多少次后打开熔断器", - "timeoutSecondsHint": "熔断器打开后多久尝试恢复(半开状态)", - "successThresholdHint": "半开状态下成功多少次后关闭熔断器", - "errorRateThresholdHint": "错误率超过此值时打开熔断器", - "minRequestsHint": "计算错误率前的最小请求数", - "saveConfig": "保存配置", - "instructionsTitle": "配置说明", - "instructions": { - "failureThreshold": "连续失败达到此次数时,熔断器打开", - "timeout": "熔断器打开后,等待此时间后尝试半开", - "successThreshold": "半开状态下,成功达到此次数时关闭熔断器", - "errorRate": "错误率超过此值时,熔断器打开", - "minRequests": "只有请求数达到此值后才计算错误率" - } - }, "universalProvider": { "duplicate": "复制", "duplicatedAndSynced": "统一供应商已复制并同步", diff --git a/src/icons/extracted/index.ts b/src/icons/extracted/index.ts index c988f2c8f..61e665732 100644 --- a/src/icons/extracted/index.ts +++ b/src/icons/extracted/index.ts @@ -1,5 +1,5 @@ -// Auto-generated icon index -// Do not edit manually +// Hand-curated icon index with optimized SVG content and custom name mappings. +// Update entries deliberately; automatic regeneration is intentionally unsupported. import _a6api from "./a6-icon.png"; import _apikeyfun from "./apikeyfun.png"; diff --git a/src/lib/schemas/common.ts b/src/lib/schemas/common.ts deleted file mode 100644 index a042dab22..000000000 --- a/src/lib/schemas/common.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { z } from "zod"; -import { validateToml, tomlToMcpServer } from "@/utils/tomlUtils"; - -/** - * 解析 JSON 语法错误,返回更友好的位置信息。 - */ -function parseJsonError(error: unknown): string { - if (!(error instanceof SyntaxError)) { - return "JSON 格式错误"; - } - - const message = error.message || "JSON 解析失败"; - - // Chrome/V8: "Unexpected token ... in JSON at position 123" - const positionMatch = message.match(/at position (\d+)/i); - if (positionMatch) { - const position = parseInt(positionMatch[1], 10); - return `JSON 格式错误(位置:${position})`; - } - - // Firefox: "JSON.parse: unexpected character at line 1 column 23" - const lineColumnMatch = message.match(/line (\d+) column (\d+)/i); - if (lineColumnMatch) { - const line = lineColumnMatch[1]; - const column = lineColumnMatch[2]; - return `JSON 格式错误:第 ${line} 行,第 ${column} 列`; - } - - return `JSON 格式错误:${message}`; -} - -/** - * 通用的 JSON 配置文本校验: - * - 非空 - * - 可解析且为对象(非数组) - */ -export const jsonConfigSchema = z - .string() - .min(1, "配置不能为空") - .superRefine((value, ctx) => { - try { - const obj = JSON.parse(value); - if (!obj || typeof obj !== "object" || Array.isArray(obj)) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "需为单个对象配置", - }); - } - } catch (e) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: parseJsonError(e), - }); - } - }); - -/** - * 通用的 TOML 配置文本校验: - * - 允许为空(由上层业务决定是否必填) - * - 语法与结构有效 - * - 针对 stdio/http/sse 的必填字段(command/url)进行提示 - */ -export const tomlConfigSchema = z.string().superRefine((value, ctx) => { - const err = validateToml(value); - if (err) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `TOML 无效:${err}`, - }); - return; - } - - if (!value.trim()) return; - - try { - const server = tomlToMcpServer(value); - if (server.type === "stdio" && !server.command?.trim()) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "stdio 类型需填写 command", - }); - } - if ( - (server.type === "http" || server.type === "sse") && - !server.url?.trim() - ) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `${server.type} 类型需填写 url`, - }); - } - } catch (e: any) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: e?.message || "TOML 解析失败", - }); - } -}); diff --git a/src/lib/schemas/mcp.ts b/src/lib/schemas/mcp.ts deleted file mode 100644 index 65aeab0ab..000000000 --- a/src/lib/schemas/mcp.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { z } from "zod"; - -const mcpServerSpecSchema = z - .object({ - type: z.enum(["stdio", "http", "sse"]).optional(), - command: z.string().trim().optional(), - args: z.array(z.string()).optional(), - env: z.record(z.string(), z.string()).optional(), - cwd: z.string().optional(), - url: z.string().trim().url("请输入有效的 URL").optional(), - headers: z.record(z.string(), z.string()).optional(), - }) - .superRefine((server, ctx) => { - const type = server.type ?? "stdio"; - if (type === "stdio" && !server.command?.trim()) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "stdio 类型需填写 command", - path: ["command"], - }); - } - if ((type === "http" || type === "sse") && !server.url?.trim()) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `${type} 类型需填写 url`, - path: ["url"], - }); - } - }); - -export const mcpServerSchema = z.object({ - id: z.string().min(1, "请输入服务器 ID"), - name: z.string().optional(), - description: z.string().optional(), - tags: z.array(z.string()).optional(), - homepage: z.string().url().optional(), - docs: z.string().url().optional(), - enabled: z.boolean().optional(), - server: mcpServerSpecSchema, -}); - -export type McpServerFormData = z.infer; diff --git a/src/lib/schemas/settings.ts b/src/lib/schemas/settings.ts deleted file mode 100644 index 61f17166d..000000000 --- a/src/lib/schemas/settings.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { z } from "zod"; - -const directorySchema = z - .string() - .trim() - .min(1, "路径不能为空") - .optional() - .or(z.literal("")); - -export const settingsSchema = z.object({ - // 设备级 UI 设置 - showInTray: z.boolean(), - minimizeToTrayOnClose: z.boolean(), - enableClaudePluginIntegration: z.boolean().optional(), - skipClaudeOnboarding: z.boolean().optional(), - launchOnStartup: z.boolean().optional(), - enableLocalProxy: z.boolean().optional(), - usageDashboardRefreshIntervalMs: z.number().optional(), - preserveCodexOfficialAuthOnSwitch: z.boolean().optional(), - unifyCodexSessionHistory: z.boolean().optional(), - language: z.enum(["en", "zh", "zh-TW", "ja"]).optional(), - - // 设备级目录覆盖 - claudeConfigDir: directorySchema.nullable().optional(), - codexConfigDir: directorySchema.nullable().optional(), - geminiConfigDir: directorySchema.nullable().optional(), - grokConfigDir: directorySchema.nullable().optional(), - opencodeConfigDir: directorySchema.nullable().optional(), - openclawConfigDir: directorySchema.nullable().optional(), - - // 当前供应商 ID(设备级) - currentProviderClaude: z.string().optional(), - currentProviderClaudeDesktop: z.string().optional(), - currentProviderCodex: z.string().optional(), - currentProviderGemini: z.string().optional(), - - // Skill 同步设置 - skillSyncMethod: z.enum(["auto", "symlink", "copy"]).optional(), - skillStorageLocation: z.enum(["cc_switch", "unified"]).optional(), - - // WebDAV v2 同步设置(通过专用命令保存,schema 仅用于读取) - webdavSync: z - .object({ - enabled: z.boolean().optional(), - autoSync: z.boolean().optional(), - baseUrl: z.string().trim().optional().or(z.literal("")), - username: z.string().trim().optional().or(z.literal("")), - password: z.string().optional(), - remoteRoot: z.string().trim().optional().or(z.literal("")), - profile: z.string().trim().optional().or(z.literal("")), - status: z - .object({ - lastSyncAt: z.number().nullable().optional(), - lastError: z.string().nullable().optional(), - lastErrorSource: z.string().nullable().optional(), - lastRemoteEtag: z.string().nullable().optional(), - lastLocalManifestHash: z.string().nullable().optional(), - lastRemoteManifestHash: z.string().nullable().optional(), - }) - .optional(), - }) - .optional(), - - // 本机自动迁移状态(后端维护且保存时后端忽略前端值,仅供读取展示) - localMigrations: z - .object({ - codexThirdPartyHistoryProviderBucketV1: z - .object({ - completedAt: z.string(), - targetProviderId: z.string(), - sourceProviderIds: z.array(z.string()).optional(), - migratedJsonlFiles: z.number().optional(), - migratedStateRows: z.number().optional(), - }) - .optional(), - }) - .optional(), -}); - -export type SettingsFormData = z.infer;