diff --git a/.yarn/patches/ollama-ai-provider-v2-npm-1.5.5-8bef249af9.patch b/.yarn/patches/ollama-ai-provider-v2-npm-1.5.5-8bef249af9.patch index ea14381539..c306bef6e5 100644 --- a/.yarn/patches/ollama-ai-provider-v2-npm-1.5.5-8bef249af9.patch +++ b/.yarn/patches/ollama-ai-provider-v2-npm-1.5.5-8bef249af9.patch @@ -7,7 +7,7 @@ index 8dd9b498050dbecd8dd6b901acf1aa8ca38a49af..ed644349c9d38fe2a66b2fb44214f7c1 type OllamaChatModelId = "athene-v2" | "athene-v2:72b" | "aya-expanse" | "aya-expanse:8b" | "aya-expanse:32b" | "codegemma" | "codegemma:2b" | "codegemma:7b" | "codellama" | "codellama:7b" | "codellama:13b" | "codellama:34b" | "codellama:70b" | "codellama:code" | "codellama:python" | "command-r" | "command-r:35b" | "command-r-plus" | "command-r-plus:104b" | "command-r7b" | "command-r7b:7b" | "deepseek-r1" | "deepseek-r1:1.5b" | "deepseek-r1:7b" | "deepseek-r1:8b" | "deepseek-r1:14b" | "deepseek-r1:32b" | "deepseek-r1:70b" | "deepseek-r1:671b" | "deepseek-coder-v2" | "deepseek-coder-v2:16b" | "deepseek-coder-v2:236b" | "deepseek-v3" | "deepseek-v3:671b" | "devstral" | "devstral:24b" | "dolphin3" | "dolphin3:8b" | "exaone3.5" | "exaone3.5:2.4b" | "exaone3.5:7.8b" | "exaone3.5:32b" | "falcon2" | "falcon2:11b" | "falcon3" | "falcon3:1b" | "falcon3:3b" | "falcon3:7b" | "falcon3:10b" | "firefunction-v2" | "firefunction-v2:70b" | "gemma" | "gemma:2b" | "gemma:7b" | "gemma2" | "gemma2:2b" | "gemma2:9b" | "gemma2:27b" | "gemma3" | "gemma3:1b" | "gemma3:4b" | "gemma3:12b" | "gemma3:27b" | "granite3-dense" | "granite3-dense:2b" | "granite3-dense:8b" | "granite3-guardian" | "granite3-guardian:2b" | "granite3-guardian:8b" | "granite3-moe" | "granite3-moe:1b" | "granite3-moe:3b" | "granite3.1-dense" | "granite3.1-dense:2b" | "granite3.1-dense:8b" | "granite3.1-moe" | "granite3.1-moe:1b" | "granite3.1-moe:3b" | "llama2" | "llama2:7b" | "llama2:13b" | "llama2:70b" | "llama3" | "llama3:8b" | "llama3:70b" | "llama3-chatqa" | "llama3-chatqa:8b" | "llama3-chatqa:70b" | "llama3-gradient" | "llama3-gradient:8b" | "llama3-gradient:70b" | "llama3.1" | "llama3.1:8b" | "llama3.1:70b" | "llama3.1:405b" | "llama3.2" | "llama3.2:1b" | "llama3.2:3b" | "llama3.2-vision" | "llama3.2-vision:11b" | "llama3.2-vision:90b" | "llama3.3" | "llama3.3:70b" | "llama4" | "llama4:16x17b" | "llama4:128x17b" | "llama-guard3" | "llama-guard3:1b" | "llama-guard3:8b" | "llava" | "llava:7b" | "llava:13b" | "llava:34b" | "llava-llama3" | "llava-llama3:8b" | "llava-phi3" | "llava-phi3:3.8b" | "marco-o1" | "marco-o1:7b" | "mistral" | "mistral:7b" | "mistral-large" | "mistral-large:123b" | "mistral-nemo" | "mistral-nemo:12b" | "mistral-small" | "mistral-small:22b" | "mixtral" | "mixtral:8x7b" | "mixtral:8x22b" | "moondream" | "moondream:1.8b" | "openhermes" | "openhermes:v2.5" | "nemotron" | "nemotron:70b" | "nemotron-mini" | "nemotron-mini:4b" | "olmo" | "olmo:7b" | "olmo:13b" | "opencoder" | "opencoder:1.5b" | "opencoder:8b" | "phi3" | "phi3:3.8b" | "phi3:14b" | "phi3.5" | "phi3.5:3.8b" | "phi4" | "phi4:14b" | "qwen" | "qwen:7b" | "qwen:14b" | "qwen:32b" | "qwen:72b" | "qwen:110b" | "qwen2" | "qwen2:0.5b" | "qwen2:1.5b" | "qwen2:7b" | "qwen2:72b" | "qwen2.5" | "qwen2.5:0.5b" | "qwen2.5:1.5b" | "qwen2.5:3b" | "qwen2.5:7b" | "qwen2.5:14b" | "qwen2.5:32b" | "qwen2.5:72b" | "qwen2.5-coder" | "qwen2.5-coder:0.5b" | "qwen2.5-coder:1.5b" | "qwen2.5-coder:3b" | "qwen2.5-coder:7b" | "qwen2.5-coder:14b" | "qwen2.5-coder:32b" | "qwen3" | "qwen3:0.6b" | "qwen3:1.7b" | "qwen3:4b" | "qwen3:8b" | "qwen3:14b" | "qwen3:30b" | "qwen3:32b" | "qwen3:235b" | "qwq" | "qwq:32b" | "sailor2" | "sailor2:1b" | "sailor2:8b" | "sailor2:20b" | "shieldgemma" | "shieldgemma:2b" | "shieldgemma:9b" | "shieldgemma:27b" | "smallthinker" | "smallthinker:3b" | "smollm" | "smollm:135m" | "smollm:360m" | "smollm:1.7b" | "tinyllama" | "tinyllama:1.1b" | "tulu3" | "tulu3:8b" | "tulu3:70b" | (string & {}); declare const ollamaProviderOptions: z.ZodObject<{ - think: z.ZodOptional; -+ think: z.ZodOptional]>>; ++ think: z.ZodOptional, z.ZodLiteral<"medium">, z.ZodLiteral<"high">]>>; options: z.ZodOptional; repeat_last_n: z.ZodOptional; @@ -29,7 +29,7 @@ index 8dd9b498050dbecd8dd6b901acf1aa8ca38a49af..ed644349c9d38fe2a66b2fb44214f7c1 declare const ollamaCompletionProviderOptions: z.ZodObject<{ - think: z.ZodOptional; -+ think: z.ZodOptional]>>; ++ think: z.ZodOptional, z.ZodLiteral<"medium">, z.ZodLiteral<"high">]>>; user: z.ZodOptional; suffix: z.ZodOptional; echo: z.ZodOptional; @@ -42,7 +42,7 @@ index 35b5142ce8476ce2549ed7c2ec48e7d8c46c90d9..2ef64dc9a4c2be043e6af608241a6a83 // src/completion/ollama-completion-language-model.ts var ollamaCompletionProviderOptions = import_v42.z.object({ - think: import_v42.z.boolean().optional(), -+ think: import_v42.z.union([import_v42.z.boolean(), import_v42.z.enum(['low', 'medium', 'high'])]).optional(), ++ think: import_v42.z.union([import_v42.z.boolean(), import_v42.z.literal('low'), import_v42.z.literal('medium'), import_v42.z.literal('high')]).optional(), user: import_v42.z.string().optional(), suffix: import_v42.z.string().optional(), echo: import_v42.z.boolean().optional() @@ -64,7 +64,7 @@ index 35b5142ce8476ce2549ed7c2ec48e7d8c46c90d9..2ef64dc9a4c2be043e6af608241a6a83 * Only supported by certain models like DeepSeek R1 and Qwen 3. */ - think: import_v44.z.boolean().optional(), -+ think: import_v44.z.union([import_v44.z.boolean(), import_v44.z.enum(['low', 'medium', 'high'])]).optional(), ++ think: import_v44.z.union([import_v44.z.boolean(), import_v44.z.literal('low'), import_v44.z.literal('medium'), import_v44.z.literal('high')]).optional(), options: import_v44.z.object({ num_ctx: import_v44.z.number().optional(), repeat_last_n: import_v44.z.number().optional(), @@ -97,7 +97,7 @@ index e2a634a78d80ac9542f2cc4f96cf2291094b10cf..67b23efce3c1cf4f026693d3ff924698 // src/completion/ollama-completion-language-model.ts var ollamaCompletionProviderOptions = z2.object({ - think: z2.boolean().optional(), -+ think: z2.union([z2.boolean(), z2.enum(['low', 'medium', 'high'])]).optional(), ++ think: z2.union([z2.boolean(), z2.literal('low'), z2.literal('medium'), z2.literal('high')]).optional(), user: z2.string().optional(), suffix: z2.string().optional(), echo: z2.boolean().optional() @@ -119,7 +119,7 @@ index e2a634a78d80ac9542f2cc4f96cf2291094b10cf..67b23efce3c1cf4f026693d3ff924698 * Only supported by certain models like DeepSeek R1 and Qwen 3. */ - think: z4.boolean().optional(), -+ think: z4.union([z4.boolean(), z4.enum(['low', 'medium', 'high'])]).optional(), ++ think: z4.union([z4.boolean(), z4.literal('low'), z4.literal('medium'), z4.literal('high')]).optional(), options: z4.object({ num_ctx: z4.number().optional(), repeat_last_n: z4.number().optional(), diff --git a/docs/en/references/fuzzy-search.md b/docs/en/references/fuzzy-search.md new file mode 100644 index 0000000000..11c2002cb9 --- /dev/null +++ b/docs/en/references/fuzzy-search.md @@ -0,0 +1,129 @@ +# Fuzzy Search for File List + +This document describes the fuzzy search implementation for file listing in Cherry Studio. + +## Overview + +The fuzzy search feature allows users to find files by typing partial or approximate file names/paths. It uses a two-tier file filtering strategy (ripgrep glob pre-filtering with greedy substring fallback) combined with subsequence-based scoring for optimal performance and flexibility. + +## Features + +- **Ripgrep Glob Pre-filtering**: Primary filtering using glob patterns for fast native-level filtering +- **Greedy Substring Matching**: Fallback file filtering strategy when ripgrep glob pre-filtering returns no results +- **Subsequence-based Segment Scoring**: During scoring, path segments gain additional weight when query characters appear in order +- **Relevance Scoring**: Results are sorted by a relevance score derived from multiple factors + +## Matching Strategies + +### 1. Ripgrep Glob Pre-filtering (Primary) + +The query is converted to a glob pattern for ripgrep to do initial filtering: + +``` +Query: "updater" +Glob: "*u*p*d*a*t*e*r*" +``` + +This leverages ripgrep's native performance for the initial file filtering. + +### 2. Greedy Substring Matching (Fallback) + +When the glob pre-filter returns no results, the system falls back to greedy substring matching. This allows more flexible matching: + +``` +Query: "updatercontroller" +File: "packages/update/src/node/updateController.ts" + +Matching process: +1. Find "update" (longest match from start) +2. Remaining "rcontroller" → find "r" then "controller" +3. All parts matched → Success +``` + +## Scoring Algorithm + +Results are ranked by a relevance score based on named constants defined in `FileStorage.ts`: + +| Constant | Value | Description | +|----------|-------|-------------| +| `SCORE_FILENAME_STARTS` | 100 | Filename starts with query (highest priority) | +| `SCORE_FILENAME_CONTAINS` | 80 | Filename contains exact query substring | +| `SCORE_SEGMENT_MATCH` | 60 | Per path segment that matches query | +| `SCORE_WORD_BOUNDARY` | 20 | Query matches start of a word | +| `SCORE_CONSECUTIVE_CHAR` | 15 | Per consecutive character match | +| `PATH_LENGTH_PENALTY_FACTOR` | 4 | Logarithmic penalty for longer paths | + +### Scoring Strategy + +The scoring prioritizes: +1. **Filename matches** (highest): Files where the query appears in the filename are most relevant +2. **Path segment matches**: Multiple matching segments indicate stronger relevance +3. **Word boundaries**: Matching at word starts (e.g., "upd" matching "update") is preferred +4. **Consecutive matches**: Longer consecutive character sequences score higher +5. **Path length**: Shorter paths are preferred (logarithmic penalty prevents long paths from dominating) + +### Example Scoring + +For query `updater`: + +| File | Score Factors | +|------|---------------| +| `RCUpdater.js` | Short path + filename contains "updater" | +| `updateController.ts` | Multiple segment matches | +| `UpdaterHelper.plist` | Long path penalty | + +## Configuration + +### DirectoryListOptions + +```typescript +interface DirectoryListOptions { + recursive?: boolean // Default: true + maxDepth?: number // Default: 10 + includeHidden?: boolean // Default: false + includeFiles?: boolean // Default: true + includeDirectories?: boolean // Default: true + maxEntries?: number // Default: 20 + searchPattern?: string // Default: '.' + fuzzy?: boolean // Default: true +} +``` + +## Usage + +```typescript +// Basic fuzzy search +const files = await window.api.file.listDirectory(dirPath, { + searchPattern: 'updater', + fuzzy: true, + maxEntries: 20 +}) + +// Disable fuzzy search (exact glob matching) +const files = await window.api.file.listDirectory(dirPath, { + searchPattern: 'update', + fuzzy: false +}) +``` + +## Performance Considerations + +1. **Ripgrep Pre-filtering**: Most queries are handled by ripgrep's native glob matching, which is extremely fast +2. **Fallback Only When Needed**: Greedy substring matching (which loads all files) only runs when glob matching returns empty results +3. **Result Limiting**: Only top 20 results are returned by default +4. **Excluded Directories**: Common large directories are automatically excluded: + - `node_modules` + - `.git` + - `dist`, `build` + - `.next`, `.nuxt` + - `coverage`, `.cache` + +## Implementation Details + +The implementation is located in `src/main/services/FileStorage.ts`: + +- `queryToGlobPattern()`: Converts query to ripgrep glob pattern +- `isFuzzyMatch()`: Subsequence matching algorithm +- `isGreedySubstringMatch()`: Greedy substring matching fallback +- `getFuzzyMatchScore()`: Calculates relevance score +- `listDirectoryWithRipgrep()`: Main search orchestration diff --git a/docs/zh/references/fuzzy-search.md b/docs/zh/references/fuzzy-search.md new file mode 100644 index 0000000000..d28d189928 --- /dev/null +++ b/docs/zh/references/fuzzy-search.md @@ -0,0 +1,129 @@ +# 文件列表模糊搜索 + +本文档描述了 Cherry Studio 中文件列表的模糊搜索实现。 + +## 概述 + +模糊搜索功能允许用户通过输入部分或近似的文件名/路径来查找文件。它使用两层文件过滤策略(ripgrep glob 预过滤 + 贪婪子串匹配回退),结合基于子序列的评分,以获得最佳性能和灵活性。 + +## 功能特性 + +- **Ripgrep Glob 预过滤**:使用 glob 模式进行快速原生级过滤的主要过滤策略 +- **贪婪子串匹配**:当 ripgrep glob 预过滤无结果时的回退文件过滤策略 +- **基于子序列的段评分**:评分时,当查询字符按顺序出现时,路径段获得额外权重 +- **相关性评分**:结果按多因素相关性分数排序 + +## 匹配策略 + +### 1. Ripgrep Glob 预过滤(主要) + +查询被转换为 glob 模式供 ripgrep 进行初始过滤: + +``` +查询: "updater" +Glob: "*u*p*d*a*t*e*r*" +``` + +这利用了 ripgrep 的原生性能进行初始文件过滤。 + +### 2. 贪婪子串匹配(回退) + +当 glob 预过滤无结果时,系统回退到贪婪子串匹配。这允许更灵活的匹配: + +``` +查询: "updatercontroller" +文件: "packages/update/src/node/updateController.ts" + +匹配过程: +1. 找到 "update"(从开头的最长匹配) +2. 剩余 "rcontroller" → 找到 "r" 然后 "controller" +3. 所有部分都匹配 → 成功 +``` + +## 评分算法 + +结果根据 `FileStorage.ts` 中定义的命名常量进行相关性分数排名: + +| 常量 | 值 | 描述 | +|------|-----|------| +| `SCORE_FILENAME_STARTS` | 100 | 文件名以查询开头(最高优先级)| +| `SCORE_FILENAME_CONTAINS` | 80 | 文件名包含精确查询子串 | +| `SCORE_SEGMENT_MATCH` | 60 | 每个匹配查询的路径段 | +| `SCORE_WORD_BOUNDARY` | 20 | 查询匹配单词开头 | +| `SCORE_CONSECUTIVE_CHAR` | 15 | 每个连续字符匹配 | +| `PATH_LENGTH_PENALTY_FACTOR` | 4 | 较长路径的对数惩罚 | + +### 评分策略 + +评分优先级: +1. **文件名匹配**(最高):查询出现在文件名中的文件最相关 +2. **路径段匹配**:多个匹配段表示更强的相关性 +3. **词边界**:在单词开头匹配(如 "upd" 匹配 "update")更优先 +4. **连续匹配**:更长的连续字符序列得分更高 +5. **路径长度**:较短路径更优先(对数惩罚防止长路径主导评分) + +### 评分示例 + +对于查询 `updater`: + +| 文件 | 评分因素 | +|------|----------| +| `RCUpdater.js` | 短路径 + 文件名包含 "updater" | +| `updateController.ts` | 多个路径段匹配 | +| `UpdaterHelper.plist` | 长路径惩罚 | + +## 配置 + +### DirectoryListOptions + +```typescript +interface DirectoryListOptions { + recursive?: boolean // 默认: true + maxDepth?: number // 默认: 10 + includeHidden?: boolean // 默认: false + includeFiles?: boolean // 默认: true + includeDirectories?: boolean // 默认: true + maxEntries?: number // 默认: 20 + searchPattern?: string // 默认: '.' + fuzzy?: boolean // 默认: true +} +``` + +## 使用方法 + +```typescript +// 基本模糊搜索 +const files = await window.api.file.listDirectory(dirPath, { + searchPattern: 'updater', + fuzzy: true, + maxEntries: 20 +}) + +// 禁用模糊搜索(精确 glob 匹配) +const files = await window.api.file.listDirectory(dirPath, { + searchPattern: 'update', + fuzzy: false +}) +``` + +## 性能考虑 + +1. **Ripgrep 预过滤**:大多数查询由 ripgrep 的原生 glob 匹配处理,速度极快 +2. **仅在需要时回退**:贪婪子串匹配(加载所有文件)仅在 glob 匹配返回空结果时运行 +3. **结果限制**:默认只返回前 20 个结果 +4. **排除目录**:自动排除常见的大型目录: + - `node_modules` + - `.git` + - `dist`、`build` + - `.next`、`.nuxt` + - `coverage`、`.cache` + +## 实现细节 + +实现位于 `src/main/services/FileStorage.ts`: + +- `queryToGlobPattern()`:将查询转换为 ripgrep glob 模式 +- `isFuzzyMatch()`:子序列匹配算法 +- `isGreedySubstringMatch()`:贪婪子串匹配回退 +- `getFuzzyMatchScore()`:计算相关性分数 +- `listDirectoryWithRipgrep()`:主搜索协调 diff --git a/electron-builder.yml b/electron-builder.yml index 11dce735c5..8af4642f05 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -134,68 +134,38 @@ artifactBuildCompleted: scripts/artifact-build-completed.js releaseInfo: releaseNotes: | - Cherry Studio 1.7.7 - New Models & UI Improvements + Cherry Studio 1.7.8 - Bug Fixes & Performance Improvements - This release adds new AI model support, OpenRouter integration, and UI redesigns. + This release focuses on bug fixes and performance optimizations. - ✨ New Features - - [Models] Add GLM-4.7 and MiniMax-M2.1 model support - - [Provider] Add OpenRouter provider support - - [OVMS] Upgrade to 2025.4 with Qwen3-4B-int4-ov preset model - - [OVMS] Close OVMS process when app quits - - [Search] Show keyword-adjacent snippets in history search - - [Painting] Add extend_params support for DMX painting - - [UI] Add MCP logo and replace Hammer icon - - 🎨 UI Improvements - - [Notes] Move notes settings to popup in NotesPage for quick access - - [WebSearch] Redesign settings with two-column layout and "Set as Default" button - - [Display] Improve font selector for long font names - - [Transfer] Rename LanDrop to LanTransfer + ⚡ Performance + - [ModelList] Improve model list loading performance 🐛 Bug Fixes - - [API] Correct aihubmix Anthropic API path - - [OpenRouter] Support GPT-5.1/5.2 reasoning effort 'none' and improve error handling - - [Thinking] Fix interleaved thinking support - - [Memory] Fix retrieval issues and enable database backup - - [Settings] Update default assistant settings to disable temperature - - [OpenAI] Add persistent server configuration support - - [Azure] Normalize Azure endpoint - - [MCP] Check system npx/uvx before falling back to bundled binaries - - [Prompt] Improve language instruction clarity - - [Models] Include GPT5.2 series in verbosity check - - [URL] Enhance urlContext validation for supported providers and models + - [Ollama] Fix new users unable to use Ollama models + - [Ollama] Improve reasoningEffort handling + - [Assistants] Prevent deleting last assistant and add error message + - [Shortcut] Fix shortcut icons sorting disorder + - [Memory] Fix global memory settings submit failure + - [Windows] Fix remember size not working for SelectionAction window + - [Anthropic] Fix API base URL handling + - [Files] Allow more file extensions - Cherry Studio 1.7.7 - 新模型与界面改进 + Cherry Studio 1.7.8 - 问题修复与性能优化 - 本次更新添加了新 AI 模型支持、OpenRouter 集成以及界面重新设计。 + 本次更新专注于问题修复和性能优化。 - ✨ 新功能 - - [模型] 添加 GLM-4.7 和 MiniMax-M2.1 模型支持 - - [服务商] 添加 OpenRouter 服务商支持 - - [OVMS] 升级至 2025.4,新增 Qwen3-4B-int4-ov 预设模型 - - [OVMS] 应用退出时关闭 OVMS 进程 - - [搜索] 历史搜索显示关键词上下文片段 - - [绘图] DMX 绘图添加扩展参数支持 - - [界面] 添加 MCP 图标并替换锤子图标 - - 🎨 界面改进 - - [笔记] 将笔记设置移至笔记页弹窗,快速访问无需离开当前页面 - - [网页搜索] 采用两栏布局重新设计设置界面,添加"设为默认"按钮 - - [显示] 改进长字体名称的字体选择器 - - [传输] LanDrop 重命名为 LanTransfer + ⚡ 性能优化 + - [模型列表] 提升模型列表加载性能 🐛 问题修复 - - [API] 修复 aihubmix Anthropic API 路径 - - [OpenRouter] 支持 GPT-5.1/5.2 reasoning effort 'none' 并改进错误处理 - - [思考] 修复交错思考支持 - - [记忆] 修复检索问题并启用数据库备份 - - [设置] 更新默认助手设置禁用温度 - - [OpenAI] 添加持久化服务器配置支持 - - [Azure] 规范化 Azure 端点 - - [MCP] 优先检查系统 npx/uvx 再回退到内置二进制文件 - - [提示词] 改进语言指令清晰度 - - [模型] GPT5.2 系列添加到 verbosity 检查 - - [URL] 增强 urlContext 对支持的服务商和模型的验证 + - [Ollama] 修复新用户无法使用 Ollama 模型的问题 + - [Ollama] 改进推理参数处理 + - [助手] 防止删除最后一个助手并添加错误提示 + - [快捷方式] 修复快捷方式图标排序混乱 + - [记忆] 修复全局记忆设置提交失败 + - [窗口] 修复 SelectionAction 窗口记住尺寸不生效 + - [Anthropic] 修复 API 地址处理 + - [文件] 允许更多文件扩展名 diff --git a/package.json b/package.json index b5745dae39..dc5f1307de 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "CherryStudio", - "version": "1.7.7", + "version": "1.7.8", "private": true, "description": "A powerful AI assistant for producer.", "main": "./out/main/index.js", diff --git a/packages/aiCore/AI_SDK_ARCHITECTURE.md b/packages/aiCore/AI_SDK_ARCHITECTURE.md deleted file mode 100644 index 67af20c0a6..0000000000 --- a/packages/aiCore/AI_SDK_ARCHITECTURE.md +++ /dev/null @@ -1,514 +0,0 @@ -# AI Core 基于 Vercel AI SDK 的技术架构 - -## 1. 架构设计理念 - -### 1.1 设计目标 - -- **简化分层**:`models`(模型层)→ `runtime`(运行时层),清晰的职责分离 -- **统一接口**:使用 Vercel AI SDK 统一不同 AI Provider 的接口差异 -- **动态导入**:通过动态导入实现按需加载,减少打包体积 -- **最小包装**:直接使用 AI SDK 的类型和接口,避免重复定义 -- **插件系统**:基于钩子的通用插件架构,支持请求全生命周期扩展 -- **类型安全**:利用 TypeScript 和 AI SDK 的类型系统确保类型安全 -- **轻量级**:专注核心功能,保持包的轻量和高效 -- **包级独立**:作为独立包管理,便于复用和维护 -- **Agent就绪**:为将来集成 OpenAI Agents SDK 预留扩展空间 - -### 1.2 核心优势 - -- **标准化**:AI SDK 提供统一的模型接口,减少适配工作 -- **简化设计**:函数式API,避免过度抽象 -- **更好的开发体验**:完整的 TypeScript 支持和丰富的生态系统 -- **性能优化**:AI SDK 内置优化和最佳实践 -- **模块化设计**:独立包结构,支持跨项目复用 -- **可扩展插件**:通用的流转换和参数处理插件系统 -- **面向未来**:为 OpenAI Agents SDK 集成做好准备 - -## 2. 整体架构图 - -```mermaid -graph TD - subgraph "用户应用 (如 Cherry Studio)" - UI["用户界面"] - Components["应用组件"] - end - - subgraph "packages/aiCore (AI Core 包)" - subgraph "Runtime Layer (运行时层)" - RuntimeExecutor["RuntimeExecutor (运行时执行器)"] - PluginEngine["PluginEngine (插件引擎)"] - RuntimeAPI["Runtime API (便捷函数)"] - end - - subgraph "Models Layer (模型层)" - ModelFactory["createModel() (模型工厂)"] - ProviderCreator["ProviderCreator (提供商创建器)"] - end - - subgraph "Core Systems (核心系统)" - subgraph "Plugins (插件)" - PluginManager["PluginManager (插件管理)"] - BuiltInPlugins["Built-in Plugins (内置插件)"] - StreamTransforms["Stream Transforms (流转换)"] - end - - subgraph "Middleware (中间件)" - MiddlewareWrapper["wrapModelWithMiddlewares() (中间件包装)"] - end - - subgraph "Providers (提供商)" - Registry["Provider Registry (注册表)"] - Factory["Provider Factory (工厂)"] - end - end - end - - subgraph "Vercel AI SDK" - AICore["ai (核心库)"] - OpenAI["@ai-sdk/openai"] - Anthropic["@ai-sdk/anthropic"] - Google["@ai-sdk/google"] - XAI["@ai-sdk/xai"] - Others["其他 19+ Providers"] - end - - subgraph "Future: OpenAI Agents SDK" - AgentSDK["@openai/agents (未来集成)"] - AgentExtensions["Agent Extensions (预留)"] - end - - UI --> RuntimeAPI - Components --> RuntimeExecutor - RuntimeAPI --> RuntimeExecutor - RuntimeExecutor --> PluginEngine - RuntimeExecutor --> ModelFactory - PluginEngine --> PluginManager - ModelFactory --> ProviderCreator - ModelFactory --> MiddlewareWrapper - ProviderCreator --> Registry - Registry --> Factory - Factory --> OpenAI - Factory --> Anthropic - Factory --> Google - Factory --> XAI - Factory --> Others - - RuntimeExecutor --> AICore - AICore --> streamText - AICore --> generateText - AICore --> streamObject - AICore --> generateObject - - PluginManager --> StreamTransforms - PluginManager --> BuiltInPlugins - - %% 未来集成路径 - RuntimeExecutor -.-> AgentSDK - AgentSDK -.-> AgentExtensions -``` - -## 3. 包结构设计 - -### 3.1 新架构文件结构 - -``` -packages/aiCore/ -├── src/ -│ ├── core/ # 核心层 - 内部实现 -│ │ ├── models/ # 模型层 - 模型创建和配置 -│ │ │ ├── factory.ts # 模型工厂函数 ✅ -│ │ │ ├── ModelCreator.ts # 模型创建器 ✅ -│ │ │ ├── ConfigManager.ts # 配置管理器 ✅ -│ │ │ ├── types.ts # 模型类型定义 ✅ -│ │ │ └── index.ts # 模型层导出 ✅ -│ │ ├── runtime/ # 运行时层 - 执行和用户API -│ │ │ ├── executor.ts # 运行时执行器 ✅ -│ │ │ ├── pluginEngine.ts # 插件引擎 ✅ -│ │ │ ├── types.ts # 运行时类型定义 ✅ -│ │ │ └── index.ts # 运行时导出 ✅ -│ │ ├── middleware/ # 中间件系统 -│ │ │ ├── wrapper.ts # 模型包装器 ✅ -│ │ │ ├── manager.ts # 中间件管理器 ✅ -│ │ │ ├── types.ts # 中间件类型 ✅ -│ │ │ └── index.ts # 中间件导出 ✅ -│ │ ├── plugins/ # 插件系统 -│ │ │ ├── types.ts # 插件类型定义 ✅ -│ │ │ ├── manager.ts # 插件管理器 ✅ -│ │ │ ├── built-in/ # 内置插件 ✅ -│ │ │ │ ├── logging.ts # 日志插件 ✅ -│ │ │ │ ├── webSearchPlugin/ # 网络搜索插件 ✅ -│ │ │ │ ├── toolUsePlugin/ # 工具使用插件 ✅ -│ │ │ │ └── index.ts # 内置插件导出 ✅ -│ │ │ ├── README.md # 插件文档 ✅ -│ │ │ └── index.ts # 插件导出 ✅ -│ │ ├── providers/ # 提供商管理 -│ │ │ ├── registry.ts # 提供商注册表 ✅ -│ │ │ ├── factory.ts # 提供商工厂 ✅ -│ │ │ ├── creator.ts # 提供商创建器 ✅ -│ │ │ ├── types.ts # 提供商类型 ✅ -│ │ │ ├── utils.ts # 工具函数 ✅ -│ │ │ └── index.ts # 提供商导出 ✅ -│ │ ├── options/ # 配置选项 -│ │ │ ├── factory.ts # 选项工厂 ✅ -│ │ │ ├── types.ts # 选项类型 ✅ -│ │ │ ├── xai.ts # xAI 选项 ✅ -│ │ │ ├── openrouter.ts # OpenRouter 选项 ✅ -│ │ │ ├── examples.ts # 示例配置 ✅ -│ │ │ └── index.ts # 选项导出 ✅ -│ │ └── index.ts # 核心层导出 ✅ -│ ├── types.ts # 全局类型定义 ✅ -│ └── index.ts # 包主入口文件 ✅ -├── package.json # 包配置文件 ✅ -├── tsconfig.json # TypeScript 配置 ✅ -├── README.md # 包说明文档 ✅ -└── AI_SDK_ARCHITECTURE.md # 本文档 ✅ -``` - -## 4. 架构分层详解 - -### 4.1 Models Layer (模型层) - -**职责**:统一的模型创建和配置管理 - -**核心文件**: - -- `factory.ts`: 模型工厂函数 (`createModel`, `createModels`) -- `ProviderCreator.ts`: 底层提供商创建和模型实例化 -- `types.ts`: 模型配置类型定义 - -**设计特点**: - -- 函数式设计,避免不必要的类抽象 -- 统一的模型配置接口 -- 自动处理中间件应用 -- 支持批量模型创建 - -**核心API**: - -```typescript -// 模型配置接口 -export interface ModelConfig { - providerId: ProviderId - modelId: string - options: ProviderSettingsMap[ProviderId] - middlewares?: LanguageModelV1Middleware[] -} - -// 核心模型创建函数 -export async function createModel(config: ModelConfig): Promise -export async function createModels(configs: ModelConfig[]): Promise -``` - -### 4.2 Runtime Layer (运行时层) - -**职责**:运行时执行器和用户面向的API接口 - -**核心组件**: - -- `executor.ts`: 运行时执行器类 -- `plugin-engine.ts`: 插件引擎(原PluginEnabledAiClient) -- `index.ts`: 便捷函数和工厂方法 - -**设计特点**: - -- 提供三种使用方式:类实例、静态工厂、函数式调用 -- 自动集成模型创建和插件处理 -- 完整的类型安全支持 -- 为 OpenAI Agents SDK 预留扩展接口 - -**核心API**: - -```typescript -// 运行时执行器 -export class RuntimeExecutor { - static create( - providerId: T, - options: ProviderSettingsMap[T], - plugins?: AiPlugin[] - ): RuntimeExecutor - - async streamText(modelId: string, params: StreamTextParams): Promise - async generateText(modelId: string, params: GenerateTextParams): Promise - async streamObject(modelId: string, params: StreamObjectParams): Promise - async generateObject(modelId: string, params: GenerateObjectParams): Promise -} - -// 便捷函数式API -export async function streamText( - providerId: T, - options: ProviderSettingsMap[T], - modelId: string, - params: StreamTextParams, - plugins?: AiPlugin[] -): Promise -``` - -### 4.3 Plugin System (插件系统) - -**职责**:可扩展的插件架构 - -**核心组件**: - -- `PluginManager`: 插件生命周期管理 -- `built-in/`: 内置插件集合 -- 流转换收集和应用 - -**设计特点**: - -- 借鉴 Rollup 的钩子分类设计 -- 支持流转换 (`experimental_transform`) -- 内置常用插件(日志、计数等) -- 完整的生命周期钩子 - -**插件接口**: - -```typescript -export interface AiPlugin { - name: string - enforce?: 'pre' | 'post' - - // 【First】首个钩子 - 只执行第一个返回值的插件 - resolveModel?: (modelId: string, context: AiRequestContext) => string | null | Promise - loadTemplate?: (templateName: string, context: AiRequestContext) => any | null | Promise - - // 【Sequential】串行钩子 - 链式执行,支持数据转换 - transformParams?: (params: any, context: AiRequestContext) => any | Promise - transformResult?: (result: any, context: AiRequestContext) => any | Promise - - // 【Parallel】并行钩子 - 不依赖顺序,用于副作用 - onRequestStart?: (context: AiRequestContext) => void | Promise - onRequestEnd?: (context: AiRequestContext, result: any) => void | Promise - onError?: (error: Error, context: AiRequestContext) => void | Promise - - // 【Stream】流处理 - transformStream?: () => TransformStream -} -``` - -### 4.4 Middleware System (中间件系统) - -**职责**:AI SDK原生中间件支持 - -**核心组件**: - -- `ModelWrapper.ts`: 模型包装函数 - -**设计哲学**: - -- 直接使用AI SDK的 `wrapLanguageModel` -- 与插件系统分离,职责明确 -- 函数式设计,简化使用 - -```typescript -export function wrapModelWithMiddlewares(model: LanguageModel, middlewares: LanguageModelV1Middleware[]): LanguageModel -``` - -### 4.5 Provider System (提供商系统) - -**职责**:AI Provider注册表和动态导入 - -**核心组件**: - -- `registry.ts`: 19+ Provider配置和类型 -- `factory.ts`: Provider配置工厂 - -**支持的Providers**: - -- OpenAI, Anthropic, Google, XAI -- Azure OpenAI, Amazon Bedrock, Google Vertex -- Groq, Together.ai, Fireworks, DeepSeek -- 等19+ AI SDK官方支持的providers - -## 5. 使用方式 - -### 5.1 函数式调用 (推荐 - 简单场景) - -```typescript -import { streamText, generateText } from '@cherrystudio/ai-core/runtime' - -// 直接函数调用 -const stream = await streamText( - 'anthropic', - { apiKey: 'your-api-key' }, - 'claude-3', - { messages: [{ role: 'user', content: 'Hello!' }] }, - [loggingPlugin] -) -``` - -### 5.2 执行器实例 (推荐 - 复杂场景) - -```typescript -import { createExecutor } from '@cherrystudio/ai-core/runtime' - -// 创建可复用的执行器 -const executor = createExecutor('openai', { apiKey: 'your-api-key' }, [plugin1, plugin2]) - -// 多次使用 -const stream = await executor.streamText('gpt-4', { - messages: [{ role: 'user', content: 'Hello!' }] -}) - -const result = await executor.generateText('gpt-4', { - messages: [{ role: 'user', content: 'How are you?' }] -}) -``` - -### 5.3 静态工厂方法 - -```typescript -import { RuntimeExecutor } from '@cherrystudio/ai-core/runtime' - -// 静态创建 -const executor = RuntimeExecutor.create('anthropic', { apiKey: 'your-api-key' }) -await executor.streamText('claude-3', { messages: [...] }) -``` - -### 5.4 直接模型创建 (高级用法) - -```typescript -import { createModel } from '@cherrystudio/ai-core/models' -import { streamText } from 'ai' - -// 直接创建模型使用 -const model = await createModel({ - providerId: 'openai', - modelId: 'gpt-4', - options: { apiKey: 'your-api-key' }, - middlewares: [middleware1, middleware2] -}) - -// 直接使用 AI SDK -const result = await streamText({ model, messages: [...] }) -``` - -## 6. 为 OpenAI Agents SDK 预留的设计 - -### 6.1 架构兼容性 - -当前架构完全兼容 OpenAI Agents SDK 的集成需求: - -```typescript -// 当前的模型创建 -const model = await createModel({ - providerId: 'anthropic', - modelId: 'claude-3', - options: { apiKey: 'xxx' } -}) - -// 将来可以直接用于 OpenAI Agents SDK -import { Agent, run } from '@openai/agents' - -const agent = new Agent({ - model, // ✅ 直接兼容 LanguageModel 接口 - name: 'Assistant', - instructions: '...', - tools: [tool1, tool2] -}) - -const result = await run(agent, 'user input') -``` - -### 6.2 预留的扩展点 - -1. **runtime/agents/** 目录预留 -2. **AgentExecutor** 类预留 -3. **Agent工具转换插件** 预留 -4. **多Agent编排** 预留 - -### 6.3 未来架构扩展 - -``` -packages/aiCore/src/core/ -├── runtime/ -│ ├── agents/ # 🚀 未来添加 -│ │ ├── AgentExecutor.ts -│ │ ├── WorkflowManager.ts -│ │ └── ConversationManager.ts -│ ├── executor.ts -│ └── index.ts -``` - -## 7. 架构优势 - -### 7.1 简化设计 - -- **移除过度抽象**:删除了orchestration层和creation层的复杂包装 -- **函数式优先**:models层使用函数而非类 -- **直接明了**:runtime层直接提供用户API - -### 7.2 职责清晰 - -- **Models**: 专注模型创建和配置 -- **Runtime**: 专注执行和用户API -- **Plugins**: 专注扩展功能 -- **Providers**: 专注AI Provider管理 - -### 7.3 类型安全 - -- 完整的 TypeScript 支持 -- AI SDK 类型的直接复用 -- 避免类型重复定义 - -### 7.4 灵活使用 - -- 三种使用模式满足不同需求 -- 从简单函数调用到复杂执行器 -- 支持直接AI SDK使用 - -### 7.5 面向未来 - -- 为 OpenAI Agents SDK 集成做好准备 -- 清晰的扩展点和架构边界 -- 模块化设计便于功能添加 - -## 8. 技术决策记录 - -### 8.1 为什么选择简化的两层架构? - -- **职责分离**:models专注创建,runtime专注执行 -- **模块化**:每层都有清晰的边界和职责 -- **扩展性**:为Agent功能预留了清晰的扩展空间 - -### 8.2 为什么选择函数式设计? - -- **简洁性**:避免不必要的类设计 -- **性能**:减少对象创建开销 -- **易用性**:函数调用更直观 - -### 8.3 为什么分离插件和中间件? - -- **职责明确**: 插件处理应用特定需求 -- **原生支持**: 中间件使用AI SDK原生功能 -- **灵活性**: 两套系统可以独立演进 - -## 9. 总结 - -AI Core架构实现了: - -### 9.1 核心特点 - -- ✅ **简化架构**: 2层核心架构,职责清晰 -- ✅ **函数式设计**: models层完全函数化 -- ✅ **类型安全**: 统一的类型定义和AI SDK类型复用 -- ✅ **插件扩展**: 强大的插件系统 -- ✅ **多种使用方式**: 满足不同复杂度需求 -- ✅ **Agent就绪**: 为OpenAI Agents SDK集成做好准备 - -### 9.2 核心价值 - -- **统一接口**: 一套API支持19+ AI providers -- **灵活使用**: 函数式、实例式、静态工厂式 -- **强类型**: 完整的TypeScript支持 -- **可扩展**: 插件和中间件双重扩展能力 -- **高性能**: 最小化包装,直接使用AI SDK -- **面向未来**: Agent SDK集成架构就绪 - -### 9.3 未来发展 - -这个架构提供了: - -- **优秀的开发体验**: 简洁的API和清晰的使用模式 -- **强大的扩展能力**: 为Agent功能预留了完整的架构空间 -- **良好的维护性**: 职责分离明确,代码易于维护 -- **广泛的适用性**: 既适合简单调用也适合复杂应用 diff --git a/packages/aiCore/src/core/providers/core/initialization.ts b/packages/aiCore/src/core/providers/core/initialization.ts index c24493ca70..cce551ad5e 100644 --- a/packages/aiCore/src/core/providers/core/initialization.ts +++ b/packages/aiCore/src/core/providers/core/initialization.ts @@ -162,6 +162,8 @@ const OpenAIExtension = ProviderExtension.create({ const OpenRouterExtension = ProviderExtension.create({ name: 'openrouter', + // TODO: 实现注册后修改拓展配置 + aliases: ['tokenflux'] as const, supportsImageGeneration: true, create: (options?: OpenRouterProviderSettings) => { const provider = createOpenRouter(options) diff --git a/packages/shared/IpcChannel.ts b/packages/shared/IpcChannel.ts index 2ba327db93..8361a917e5 100644 --- a/packages/shared/IpcChannel.ts +++ b/packages/shared/IpcChannel.ts @@ -364,6 +364,7 @@ export enum IpcChannel { OCR_ListProviders = 'ocr:list-providers', // OVMS + Ovms_IsSupported = 'ovms:is-supported', Ovms_AddModel = 'ovms:add-model', Ovms_StopAddModel = 'ovms:stop-addmodel', Ovms_GetModels = 'ovms:get-models', diff --git a/src/main/index.ts b/src/main/index.ts index ec16475d3f..536485a490 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -37,7 +37,7 @@ import { versionService } from './services/VersionService' import { windowService } from './services/WindowService' import { initWebviewHotkeys } from './services/WebviewService' import { runAsyncFunction } from './utils' -import { ovmsManager } from './services/OvmsManager' +import { isOvmsSupported } from './services/OvmsManager' const logger = loggerService.withContext('MainEntry') @@ -158,7 +158,7 @@ if (!app.requestSingleInstanceLock()) { registerShortcuts(mainWindow) - registerIpc(mainWindow, app) + await registerIpc(mainWindow, app) localTransferService.startDiscovery({ resetList: true }) replaceDevtoolsFont(mainWindow) @@ -248,7 +248,14 @@ if (!app.requestSingleInstanceLock()) { app.on('will-quit', async () => { // 简单的资源清理,不阻塞退出流程 - await ovmsManager.stopOvms() + if (isOvmsSupported) { + const { ovmsManager } = await import('./services/OvmsManager') + if (ovmsManager) { + await ovmsManager.stopOvms() + } else { + logger.warn('Unexpected behavior: undefined ovmsManager, but OVMS should be supported.') + } + } try { await mcpService.cleanup() diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 8f86a93075..56932a51d6 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -59,7 +59,7 @@ import NotificationService from './services/NotificationService' import * as NutstoreService from './services/NutstoreService' import ObsidianVaultService from './services/ObsidianVaultService' import { ocrService } from './services/ocr/OcrService' -import { ovmsManager } from './services/OvmsManager' +import { isOvmsSupported } from './services/OvmsManager' import powerMonitorService from './services/PowerMonitorService' import { proxyManager } from './services/ProxyManager' import { pythonService } from './services/PythonService' @@ -97,6 +97,7 @@ import { untildify } from './utils/file' import { updateAppDataConfig } from './utils/init' +import { getCpuName, getDeviceType, getHostname } from './utils/system' import { compress, decompress } from './utils/zip' const logger = loggerService.withContext('IPC') @@ -120,7 +121,7 @@ function extractPluginError(error: unknown): PluginError | null { return null } -export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { +export async function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { const appUpdater = new AppUpdater() const notificationService = new NotificationService() @@ -498,9 +499,9 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { ipcMain.handle(IpcChannel.Zip_Decompress, (_, text: Buffer) => decompress(text)) // system - ipcMain.handle(IpcChannel.System_GetDeviceType, () => (isMac ? 'mac' : isWin ? 'windows' : 'linux')) - ipcMain.handle(IpcChannel.System_GetHostname, () => require('os').hostname()) - ipcMain.handle(IpcChannel.System_GetCpuName, () => require('os').cpus()[0].model) + ipcMain.handle(IpcChannel.System_GetDeviceType, getDeviceType) + ipcMain.handle(IpcChannel.System_GetHostname, getHostname) + ipcMain.handle(IpcChannel.System_GetCpuName, getCpuName) ipcMain.handle(IpcChannel.System_CheckGitBash, () => { if (!isWin) { return true // Non-Windows systems don't need Git Bash @@ -974,15 +975,36 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { ipcMain.handle(IpcChannel.OCR_ListProviders, () => ocrService.listProviderIds()) // OVMS - ipcMain.handle(IpcChannel.Ovms_AddModel, (_, modelName: string, modelId: string, modelSource: string, task: string) => - ovmsManager.addModel(modelName, modelId, modelSource, task) - ) - ipcMain.handle(IpcChannel.Ovms_StopAddModel, () => ovmsManager.stopAddModel()) - ipcMain.handle(IpcChannel.Ovms_GetModels, () => ovmsManager.getModels()) - ipcMain.handle(IpcChannel.Ovms_IsRunning, () => ovmsManager.initializeOvms()) - ipcMain.handle(IpcChannel.Ovms_GetStatus, () => ovmsManager.getOvmsStatus()) - ipcMain.handle(IpcChannel.Ovms_RunOVMS, () => ovmsManager.runOvms()) - ipcMain.handle(IpcChannel.Ovms_StopOVMS, () => ovmsManager.stopOvms()) + ipcMain.handle(IpcChannel.Ovms_IsSupported, () => isOvmsSupported) + if (isOvmsSupported) { + const { ovmsManager } = await import('./services/OvmsManager') + if (ovmsManager) { + ipcMain.handle( + IpcChannel.Ovms_AddModel, + (_, modelName: string, modelId: string, modelSource: string, task: string) => + ovmsManager.addModel(modelName, modelId, modelSource, task) + ) + ipcMain.handle(IpcChannel.Ovms_StopAddModel, () => ovmsManager.stopAddModel()) + ipcMain.handle(IpcChannel.Ovms_GetModels, () => ovmsManager.getModels()) + ipcMain.handle(IpcChannel.Ovms_IsRunning, () => ovmsManager.initializeOvms()) + ipcMain.handle(IpcChannel.Ovms_GetStatus, () => ovmsManager.getOvmsStatus()) + ipcMain.handle(IpcChannel.Ovms_RunOVMS, () => ovmsManager.runOvms()) + ipcMain.handle(IpcChannel.Ovms_StopOVMS, () => ovmsManager.stopOvms()) + } else { + logger.error('Unexpected behavior: undefined ovmsManager, but OVMS should be supported.') + } + } else { + const fallback = () => { + throw new Error('OVMS is only supported on Windows with intel CPU.') + } + ipcMain.handle(IpcChannel.Ovms_AddModel, fallback) + ipcMain.handle(IpcChannel.Ovms_StopAddModel, fallback) + ipcMain.handle(IpcChannel.Ovms_GetModels, fallback) + ipcMain.handle(IpcChannel.Ovms_IsRunning, fallback) + ipcMain.handle(IpcChannel.Ovms_GetStatus, fallback) + ipcMain.handle(IpcChannel.Ovms_RunOVMS, fallback) + ipcMain.handle(IpcChannel.Ovms_StopOVMS, fallback) + } // CherryAI ipcMain.handle(IpcChannel.Cherryai_GetSignature, (_, params) => generateSignature(params)) diff --git a/src/main/services/BackupManager.ts b/src/main/services/BackupManager.ts index 46b78ed5a9..e08bbd4d7b 100644 --- a/src/main/services/BackupManager.ts +++ b/src/main/services/BackupManager.ts @@ -1,3 +1,19 @@ +/** + * @deprecated Scheduled for removal in v2.0.0 + * -------------------------------------------------------------------------- + * ⚠️ NOTICE: V2 DATA&UI REFACTORING (by 0xfullex) + * -------------------------------------------------------------------------- + * STOP: Feature PRs affecting this file are currently BLOCKED. + * Only critical bug fixes are accepted during this migration phase. + * + * This file is being refactored to v2 standards. + * Any non-critical changes will conflict with the ongoing work. + * + * 🔗 Context & Status: + * - Contribution Hold: https://github.com/CherryHQ/cherry-studio/issues/10954 + * - v2 Refactor PR : https://github.com/CherryHQ/cherry-studio/pull/10162 + * -------------------------------------------------------------------------- + */ import { loggerService } from '@logger' import { IpcChannel } from '@shared/IpcChannel' import type { WebDavConfig } from '@types' diff --git a/src/main/services/CacheService.ts b/src/main/services/CacheService.ts index d2984a9984..4f2e2f8b20 100644 --- a/src/main/services/CacheService.ts +++ b/src/main/services/CacheService.ts @@ -1,3 +1,19 @@ +/** + * @deprecated Scheduled for removal in v2.0.0 + * -------------------------------------------------------------------------- + * ⚠️ NOTICE: V2 DATA&UI REFACTORING (by 0xfullex) + * -------------------------------------------------------------------------- + * STOP: Feature PRs affecting this file are currently BLOCKED. + * Only critical bug fixes are accepted during this migration phase. + * + * This file is being refactored to v2 standards. + * Any non-critical changes will conflict with the ongoing work. + * + * 🔗 Context & Status: + * - Contribution Hold: https://github.com/CherryHQ/cherry-studio/issues/10954 + * - v2 Refactor PR : https://github.com/CherryHQ/cherry-studio/pull/10162 + * -------------------------------------------------------------------------- + */ interface CacheItem { data: T timestamp: number diff --git a/src/main/services/ConfigManager.ts b/src/main/services/ConfigManager.ts index 6f2bbd44a4..98537c85a1 100644 --- a/src/main/services/ConfigManager.ts +++ b/src/main/services/ConfigManager.ts @@ -1,3 +1,19 @@ +/** + * @deprecated Scheduled for removal in v2.0.0 + * -------------------------------------------------------------------------- + * ⚠️ NOTICE: V2 DATA&UI REFACTORING (by 0xfullex) + * -------------------------------------------------------------------------- + * STOP: Feature PRs affecting this file are currently BLOCKED. + * Only critical bug fixes are accepted during this migration phase. + * + * This file is being refactored to v2 standards. + * Any non-critical changes will conflict with the ongoing work. + * + * 🔗 Context & Status: + * - Contribution Hold: https://github.com/CherryHQ/cherry-studio/issues/10954 + * - v2 Refactor PR : https://github.com/CherryHQ/cherry-studio/pull/10162 + * -------------------------------------------------------------------------- + */ import type { UpgradeChannel } from '@shared/config/constant' import { defaultLanguage, ZOOM_SHORTCUTS } from '@shared/config/constant' import type { LanguageVarious, Shortcut } from '@types' diff --git a/src/main/services/FileStorage.ts b/src/main/services/FileStorage.ts index f0b7ce32b0..2d7520ca67 100644 --- a/src/main/services/FileStorage.ts +++ b/src/main/services/FileStorage.ts @@ -130,16 +130,18 @@ interface DirectoryListOptions { includeDirectories?: boolean maxEntries?: number searchPattern?: string + fuzzy?: boolean } const DEFAULT_DIRECTORY_LIST_OPTIONS: Required = { recursive: true, - maxDepth: 3, + maxDepth: 10, includeHidden: false, includeFiles: true, includeDirectories: true, - maxEntries: 10, - searchPattern: '.' + maxEntries: 20, + searchPattern: '.', + fuzzy: true } class FileStorage { @@ -1046,10 +1048,226 @@ class FileStorage { } /** - * Search files by content pattern + * Fuzzy match: checks if all characters in query appear in text in order (case-insensitive) + * Example: "updater" matches "packages/update/src/node/updateController.ts" */ - private async searchByContent(resolvedPath: string, options: Required): Promise { - const args: string[] = ['-l'] + private isFuzzyMatch(text: string, query: string): boolean { + let i = 0 // text index + let j = 0 // query index + const textLower = text.toLowerCase() + const queryLower = query.toLowerCase() + + while (i < textLower.length && j < queryLower.length) { + if (textLower[i] === queryLower[j]) { + j++ + } + i++ + } + return j === queryLower.length + } + + /** + * Scoring constants for fuzzy match relevance ranking + * Higher values = higher priority in search results + */ + private static readonly SCORE_SEGMENT_MATCH = 60 // Per path segment that matches query + private static readonly SCORE_FILENAME_CONTAINS = 80 // Filename contains exact query substring + private static readonly SCORE_FILENAME_STARTS = 100 // Filename starts with query (highest priority) + private static readonly SCORE_CONSECUTIVE_CHAR = 15 // Per consecutive character match + private static readonly SCORE_WORD_BOUNDARY = 20 // Query matches start of a word + private static readonly PATH_LENGTH_PENALTY_FACTOR = 4 // Logarithmic penalty multiplier for longer paths + + /** + * Calculate fuzzy match score (higher is better) + * Scoring factors: + * - Consecutive character matches (bonus) + * - Match at word boundaries (bonus) + * - Shorter path length (bonus) + * - Match in filename vs directory (bonus) + */ + private getFuzzyMatchScore(filePath: string, query: string): number { + const pathLower = filePath.toLowerCase() + const queryLower = query.toLowerCase() + const fileName = filePath.split('/').pop() || '' + const fileNameLower = fileName.toLowerCase() + + let score = 0 + + // Count how many times query-related words appear in path segments + const pathSegments = pathLower.split(/[/\\]/) + let segmentMatchCount = 0 + for (const segment of pathSegments) { + if (this.isFuzzyMatch(segment, queryLower)) { + segmentMatchCount++ + } + } + score += segmentMatchCount * FileStorage.SCORE_SEGMENT_MATCH + + // Bonus for filename starting with query (stronger than generic "contains") + if (fileNameLower.startsWith(queryLower)) { + score += FileStorage.SCORE_FILENAME_STARTS + } else if (fileNameLower.includes(queryLower)) { + // Bonus for exact substring match in filename (e.g., "updater" in "RCUpdater.js") + score += FileStorage.SCORE_FILENAME_CONTAINS + } + + // Calculate consecutive match bonus + let i = 0 + let j = 0 + let consecutiveCount = 0 + let maxConsecutive = 0 + + while (i < pathLower.length && j < queryLower.length) { + if (pathLower[i] === queryLower[j]) { + consecutiveCount++ + maxConsecutive = Math.max(maxConsecutive, consecutiveCount) + j++ + } else { + consecutiveCount = 0 + } + i++ + } + score += maxConsecutive * FileStorage.SCORE_CONSECUTIVE_CHAR + + // Bonus for word boundary matches (e.g., "upd" matches start of "update") + // Only count once to avoid inflating scores for paths with repeated patterns + const boundaryPrefix = queryLower.slice(0, Math.min(3, queryLower.length)) + const words = pathLower.split(/[/\\._-]/) + for (const word of words) { + if (word.startsWith(boundaryPrefix)) { + score += FileStorage.SCORE_WORD_BOUNDARY + break + } + } + + // Penalty for longer paths (prefer shorter, more specific matches) + // Use logarithmic scaling to prevent long paths from dominating the score + // A 50-char path gets ~-16 penalty, 100-char gets ~-18, 200-char gets ~-21 + score -= Math.log(filePath.length + 1) * FileStorage.PATH_LENGTH_PENALTY_FACTOR + + return score + } + + /** + * Convert query to glob pattern for ripgrep pre-filtering + * e.g., "updater" -> "*u*p*d*a*t*e*r*" + */ + private queryToGlobPattern(query: string): string { + // Escape special glob characters (including ! for negation) + const escaped = query.replace(/[[\]{}()*+?.,\\^$|#!]/g, '\\$&') + // Convert to fuzzy glob: each char separated by * + return '*' + escaped.split('').join('*') + '*' + } + + /** + * Greedy substring match: check if all characters in query can be matched + * by finding consecutive substrings in text (not necessarily single chars) + * e.g., "updatercontroller" matches "updateController" by: + * "update" + "r" (from Controller) + "controller" + */ + private isGreedySubstringMatch(text: string, query: string): boolean { + const textLower = text.toLowerCase() + const queryLower = query.toLowerCase() + + let queryIndex = 0 + let searchStart = 0 + + while (queryIndex < queryLower.length) { + // Try to find the longest matching substring starting at queryIndex + let bestMatchLen = 0 + let bestMatchPos = -1 + + for (let len = queryLower.length - queryIndex; len >= 1; len--) { + const substr = queryLower.slice(queryIndex, queryIndex + len) + const foundAt = textLower.indexOf(substr, searchStart) + if (foundAt !== -1) { + bestMatchLen = len + bestMatchPos = foundAt + break // Found longest possible match + } + } + + if (bestMatchLen === 0) { + // No substring match found, query cannot be matched + return false + } + + queryIndex += bestMatchLen + searchStart = bestMatchPos + bestMatchLen + } + + return true + } + + /** + * Calculate greedy substring match score (higher is better) + * Rewards: fewer match fragments, shorter match span, matches in filename + */ + private getGreedyMatchScore(filePath: string, query: string): number { + const textLower = filePath.toLowerCase() + const queryLower = query.toLowerCase() + const fileName = filePath.split('/').pop() || '' + const fileNameLower = fileName.toLowerCase() + + let queryIndex = 0 + let searchStart = 0 + let fragmentCount = 0 + let firstMatchPos = -1 + let lastMatchEnd = 0 + + while (queryIndex < queryLower.length) { + let bestMatchLen = 0 + let bestMatchPos = -1 + + for (let len = queryLower.length - queryIndex; len >= 1; len--) { + const substr = queryLower.slice(queryIndex, queryIndex + len) + const foundAt = textLower.indexOf(substr, searchStart) + if (foundAt !== -1) { + bestMatchLen = len + bestMatchPos = foundAt + break + } + } + + if (bestMatchLen === 0) { + return -Infinity // No match + } + + fragmentCount++ + if (firstMatchPos === -1) firstMatchPos = bestMatchPos + lastMatchEnd = bestMatchPos + bestMatchLen + queryIndex += bestMatchLen + searchStart = lastMatchEnd + } + + const matchSpan = lastMatchEnd - firstMatchPos + let score = 0 + + // Fewer fragments = better (single continuous match is best) + // Max bonus when fragmentCount=1, decreases as fragments increase + score += Math.max(0, 100 - (fragmentCount - 1) * 30) + + // Shorter span relative to query length = better (tighter match) + // Perfect match: span equals query length + const spanRatio = queryLower.length / matchSpan + score += spanRatio * 50 + + // Bonus for match in filename + if (this.isGreedySubstringMatch(fileNameLower, queryLower)) { + score += 80 + } + + // Penalty for longer paths + score -= Math.log(filePath.length + 1) * 4 + + return score + } + + /** + * Build common ripgrep arguments for file listing + */ + private buildRipgrepBaseArgs(options: Required, resolvedPath: string): string[] { + const args: string[] = ['--files'] // Handle hidden files if (!options.includeHidden) { @@ -1076,82 +1294,74 @@ class FileStorage { args.push('--max-depth', options.maxDepth.toString()) } - // Handle max count - if (options.maxEntries > 0) { - args.push('--max-count', options.maxEntries.toString()) - } - - // Add search pattern (search in content) - args.push(options.searchPattern) - - // Add the directory path args.push(resolvedPath) - const { exitCode, output } = await executeRipgrep(args) - - // Exit code 0 means files found, 1 means no files found (still success), 2+ means error - if (exitCode >= 2) { - throw new Error(`Ripgrep failed with exit code ${exitCode}: ${output}`) - } - - // Parse ripgrep output (already sorted by relevance) - const results = output - .split('\n') - .filter((line) => line.trim()) - .map((line) => line.replace(/\\/g, '/')) - .slice(0, options.maxEntries) - - return results + return args } private async listDirectoryWithRipgrep( resolvedPath: string, options: Required ): Promise { - const maxEntries = options.maxEntries + // Fuzzy search mode: use ripgrep glob for pre-filtering, then score in JS + if (options.fuzzy && options.searchPattern && options.searchPattern !== '.') { + const args = this.buildRipgrepBaseArgs(options, resolvedPath) - // Step 1: Search by filename first + // Insert glob pattern before the path (last element) + const globPattern = this.queryToGlobPattern(options.searchPattern) + args.splice(args.length - 1, 0, '--iglob', globPattern) + + const { exitCode, output } = await executeRipgrep(args) + + if (exitCode >= 2) { + throw new Error(`Ripgrep failed with exit code ${exitCode}: ${output}`) + } + + const filteredFiles = output + .split('\n') + .filter((line) => line.trim()) + .map((line) => line.replace(/\\/g, '/')) + + // If fuzzy glob found results, validate fuzzy match, sort and return + if (filteredFiles.length > 0) { + return filteredFiles + .filter((file) => this.isFuzzyMatch(file, options.searchPattern)) + .map((file) => ({ file, score: this.getFuzzyMatchScore(file, options.searchPattern) })) + .sort((a, b) => b.score - a.score) + .slice(0, options.maxEntries) + .map((item) => item.file) + } + + // Fallback: if no results, try greedy substring match on all files + logger.debug('Fuzzy glob returned no results, falling back to greedy substring match') + const fallbackArgs = this.buildRipgrepBaseArgs(options, resolvedPath) + + const fallbackResult = await executeRipgrep(fallbackArgs) + + if (fallbackResult.exitCode >= 2) { + return [] + } + + const allFiles = fallbackResult.output + .split('\n') + .filter((line) => line.trim()) + .map((line) => line.replace(/\\/g, '/')) + + const greedyMatched = allFiles.filter((file) => this.isGreedySubstringMatch(file, options.searchPattern)) + + return greedyMatched + .map((file) => ({ file, score: this.getGreedyMatchScore(file, options.searchPattern) })) + .sort((a, b) => b.score - a.score) + .slice(0, options.maxEntries) + .map((item) => item.file) + } + + // Fallback: search by filename only (non-fuzzy mode) logger.debug('Searching by filename pattern', { pattern: options.searchPattern, path: resolvedPath }) const filenameResults = await this.searchByFilename(resolvedPath, options) logger.debug('Found matches by filename', { count: filenameResults.length }) - - // If we have enough filename matches, return them - if (filenameResults.length >= maxEntries) { - return filenameResults.slice(0, maxEntries) - } - - // Step 2: If filename matches are less than maxEntries, search by content to fill up - logger.debug('Filename matches insufficient, searching by content to fill up', { - filenameCount: filenameResults.length, - needed: maxEntries - filenameResults.length - }) - - // Adjust maxEntries for content search to get enough results - const contentOptions = { - ...options, - maxEntries: maxEntries - filenameResults.length + 20 // Request extra to account for duplicates - } - - const contentResults = await this.searchByContent(resolvedPath, contentOptions) - - logger.debug('Found matches by content', { count: contentResults.length }) - - // Combine results: filename matches first, then content matches (deduplicated) - const combined = [...filenameResults] - const filenameSet = new Set(filenameResults) - - for (const filePath of contentResults) { - if (!filenameSet.has(filePath)) { - combined.push(filePath) - if (combined.length >= maxEntries) { - break - } - } - } - - logger.debug('Combined results', { total: combined.length, filenameCount: filenameResults.length }) - return combined.slice(0, maxEntries) + return filenameResults.slice(0, options.maxEntries) } public validateNotesDirectory = async (_: Electron.IpcMainInvokeEvent, dirPath: string): Promise => { diff --git a/src/main/services/MCPService.ts b/src/main/services/MCPService.ts index 5fc5cf8682..7d36e6d7e3 100644 --- a/src/main/services/MCPService.ts +++ b/src/main/services/MCPService.ts @@ -785,7 +785,7 @@ class McpService { ...tool, inputSchema: z.parse(MCPToolInputSchema, tool.inputSchema), outputSchema: tool.outputSchema ? z.parse(MCPToolOutputSchema, tool.outputSchema) : undefined, - id: buildFunctionCallToolName(server.name, tool.name, server.id), + id: buildFunctionCallToolName(server.name, tool.name), serverId: server.id, serverName: server.name, type: 'mcp' diff --git a/src/main/services/OvmsManager.ts b/src/main/services/OvmsManager.ts index 54e0a1bb8b..67d6d9a9df 100644 --- a/src/main/services/OvmsManager.ts +++ b/src/main/services/OvmsManager.ts @@ -3,6 +3,8 @@ import { homedir } from 'node:os' import { promisify } from 'node:util' import { loggerService } from '@logger' +import { isWin } from '@main/constant' +import { getCpuName } from '@main/utils/system' import { HOME_CHERRY_DIR } from '@shared/config/constant' import * as fs from 'fs-extra' import * as path from 'path' @@ -11,6 +13,8 @@ const logger = loggerService.withContext('OvmsManager') const execAsync = promisify(exec) +export const isOvmsSupported = isWin && getCpuName().toLowerCase().includes('intel') + interface OvmsProcess { pid: number path: string @@ -29,6 +33,12 @@ interface OvmsConfig { class OvmsManager { private ovms: OvmsProcess | null = null + constructor() { + if (!isOvmsSupported) { + throw new Error('OVMS Manager is only supported on Windows platform with Intel CPU.') + } + } + /** * Recursively terminate a process and all its child processes * @param pid Process ID to terminate @@ -563,4 +573,4 @@ class OvmsManager { } // Export singleton instance -export const ovmsManager = new OvmsManager() +export const ovmsManager = isOvmsSupported ? new OvmsManager() : undefined diff --git a/src/main/services/ReduxService.ts b/src/main/services/ReduxService.ts index cdbaff42bf..8880691a24 100644 --- a/src/main/services/ReduxService.ts +++ b/src/main/services/ReduxService.ts @@ -1,3 +1,19 @@ +/** + * @deprecated Scheduled for removal in v2.0.0 + * -------------------------------------------------------------------------- + * ⚠️ NOTICE: V2 DATA&UI REFACTORING (by 0xfullex) + * -------------------------------------------------------------------------- + * STOP: Feature PRs affecting this file are currently BLOCKED. + * Only critical bug fixes are accepted during this migration phase. + * + * This file is being refactored to v2 standards. + * Any non-critical changes will conflict with the ongoing work. + * + * 🔗 Context & Status: + * - Contribution Hold: https://github.com/CherryHQ/cherry-studio/issues/10954 + * - v2 Refactor PR : https://github.com/CherryHQ/cherry-studio/pull/10162 + * -------------------------------------------------------------------------- + */ import { loggerService } from '@logger' import { IpcChannel } from '@shared/IpcChannel' import { ipcMain } from 'electron' diff --git a/src/main/services/ShortcutService.ts b/src/main/services/ShortcutService.ts index a84d8ac248..c99e0b5dc0 100644 --- a/src/main/services/ShortcutService.ts +++ b/src/main/services/ShortcutService.ts @@ -1,3 +1,19 @@ +/** + * @deprecated Scheduled for removal in v2.0.0 + * -------------------------------------------------------------------------- + * ⚠️ NOTICE: V2 DATA&UI REFACTORING (by 0xfullex) + * -------------------------------------------------------------------------- + * STOP: Feature PRs affecting this file are currently BLOCKED. + * Only critical bug fixes are accepted during this migration phase. + * + * This file is being refactored to v2 standards. + * Any non-critical changes will conflict with the ongoing work. + * + * 🔗 Context & Status: + * - Contribution Hold: https://github.com/CherryHQ/cherry-studio/issues/10954 + * - v2 Refactor PR : https://github.com/CherryHQ/cherry-studio/pull/10162 + * -------------------------------------------------------------------------- + */ import { loggerService } from '@logger' import { handleZoomFactor } from '@main/utils/zoom' import type { Shortcut } from '@types' diff --git a/src/main/services/StoreSyncService.ts b/src/main/services/StoreSyncService.ts index 57f07195b6..6013afdd57 100644 --- a/src/main/services/StoreSyncService.ts +++ b/src/main/services/StoreSyncService.ts @@ -1,3 +1,19 @@ +/** + * @deprecated Scheduled for removal in v2.0.0 + * -------------------------------------------------------------------------- + * ⚠️ NOTICE: V2 DATA&UI REFACTORING (by 0xfullex) + * -------------------------------------------------------------------------- + * STOP: Feature PRs affecting this file are currently BLOCKED. + * Only critical bug fixes are accepted during this migration phase. + * + * This file is being refactored to v2 standards. + * Any non-critical changes will conflict with the ongoing work. + * + * 🔗 Context & Status: + * - Contribution Hold: https://github.com/CherryHQ/cherry-studio/issues/10954 + * - v2 Refactor PR : https://github.com/CherryHQ/cherry-studio/pull/10162 + * -------------------------------------------------------------------------- + */ import { IpcChannel } from '@shared/IpcChannel' import type { StoreSyncAction } from '@types' import { BrowserWindow, ipcMain } from 'electron' diff --git a/src/main/services/agents/BaseService.ts b/src/main/services/agents/BaseService.ts index 461fdab96d..e30814bb6f 100644 --- a/src/main/services/agents/BaseService.ts +++ b/src/main/services/agents/BaseService.ts @@ -2,6 +2,7 @@ import { loggerService } from '@logger' import { mcpApiService } from '@main/apiServer/services/mcp' import type { ModelValidationError } from '@main/apiServer/utils' import { validateModelId } from '@main/apiServer/utils' +import { buildFunctionCallToolName } from '@main/utils/mcp' import type { AgentType, MCPTool, SlashCommand, Tool } from '@types' import { objectKeys } from '@types' import fs from 'fs' @@ -14,6 +15,17 @@ import { builtinSlashCommands } from './services/claudecode/commands' import { builtinTools } from './services/claudecode/tools' const logger = loggerService.withContext('BaseService') +const MCP_TOOL_ID_PREFIX = 'mcp__' +const MCP_TOOL_LEGACY_PREFIX = 'mcp_' + +const buildMcpToolId = (serverId: string, toolName: string) => `${MCP_TOOL_ID_PREFIX}${serverId}__${toolName}` +const toLegacyMcpToolId = (toolId: string) => { + if (!toolId.startsWith(MCP_TOOL_ID_PREFIX)) { + return null + } + const rawId = toolId.slice(MCP_TOOL_ID_PREFIX.length) + return `${MCP_TOOL_LEGACY_PREFIX}${rawId.replace(/__/g, '_')}` +} /** * Base service class providing shared utilities for all agent-related services. @@ -35,8 +47,12 @@ export abstract class BaseService { 'slash_commands' ] - public async listMcpTools(agentType: AgentType, ids?: string[]): Promise { + public async listMcpTools( + agentType: AgentType, + ids?: string[] + ): Promise<{ tools: Tool[]; legacyIdMap: Map }> { const tools: Tool[] = [] + const legacyIdMap = new Map() if (agentType === 'claude-code') { tools.push(...builtinTools) } @@ -46,13 +62,21 @@ export abstract class BaseService { const server = await mcpApiService.getServerInfo(id) if (server) { server.tools.forEach((tool: MCPTool) => { + const canonicalId = buildFunctionCallToolName(server.name, tool.name) + const serverIdBasedId = buildMcpToolId(id, tool.name) + const legacyId = toLegacyMcpToolId(serverIdBasedId) + tools.push({ - id: `mcp_${id}_${tool.name}`, + id: canonicalId, name: tool.name, type: 'mcp', description: tool.description || '', requirePermissions: true }) + legacyIdMap.set(serverIdBasedId, canonicalId) + if (legacyId) { + legacyIdMap.set(legacyId, canonicalId) + } }) } } catch (error) { @@ -64,7 +88,53 @@ export abstract class BaseService { } } - return tools + return { tools, legacyIdMap } + } + + /** + * Normalize MCP tool IDs in allowed_tools to the current format. + * + * Legacy formats: + * - "mcp____" (double underscore separators, server ID based) + * - "mcp__" (single underscore separators) + * Current format: "mcp____" (double underscore separators). + * + * This keeps persisted data compatible without requiring a database migration. + */ + protected normalizeAllowedTools( + allowedTools: string[] | undefined, + tools: Tool[], + legacyIdMap?: Map + ): string[] | undefined { + if (!allowedTools || allowedTools.length === 0) { + return allowedTools + } + + const resolvedLegacyIdMap = new Map() + + if (legacyIdMap) { + for (const [legacyId, canonicalId] of legacyIdMap) { + resolvedLegacyIdMap.set(legacyId, canonicalId) + } + } + + for (const tool of tools) { + if (tool.type !== 'mcp') { + continue + } + const legacyId = toLegacyMcpToolId(tool.id) + if (!legacyId) { + continue + } + resolvedLegacyIdMap.set(legacyId, tool.id) + } + + if (resolvedLegacyIdMap.size === 0) { + return allowedTools + } + + const normalized = allowedTools.map((toolId) => resolvedLegacyIdMap.get(toolId) ?? toolId) + return Array.from(new Set(normalized)) } public async listSlashCommands(agentType: AgentType): Promise { diff --git a/src/main/services/agents/database/DatabaseManager.ts b/src/main/services/agents/database/DatabaseManager.ts index f4b13971c7..913f9e4a66 100644 --- a/src/main/services/agents/database/DatabaseManager.ts +++ b/src/main/services/agents/database/DatabaseManager.ts @@ -1,3 +1,19 @@ +/** + * @deprecated Scheduled for removal in v2.0.0 + * -------------------------------------------------------------------------- + * ⚠️ NOTICE: V2 DATA&UI REFACTORING (by 0xfullex) + * -------------------------------------------------------------------------- + * STOP: Feature PRs affecting this file are currently BLOCKED. + * Only critical bug fixes are accepted during this migration phase. + * + * This file is being refactored to v2 standards. + * Any non-critical changes will conflict with the ongoing work. + * + * 🔗 Context & Status: + * - Contribution Hold: https://github.com/CherryHQ/cherry-studio/issues/10954 + * - v2 Refactor PR : https://github.com/CherryHQ/cherry-studio/pull/10162 + * -------------------------------------------------------------------------- + */ import { type Client, createClient } from '@libsql/client' import { loggerService } from '@logger' import type { LibSQLDatabase } from 'drizzle-orm/libsql' diff --git a/src/main/services/agents/drizzle.config.ts b/src/main/services/agents/drizzle.config.ts index e12518c069..7278883c11 100644 --- a/src/main/services/agents/drizzle.config.ts +++ b/src/main/services/agents/drizzle.config.ts @@ -1,3 +1,19 @@ +/** + * @deprecated Scheduled for removal in v2.0.0 + * -------------------------------------------------------------------------- + * ⚠️ NOTICE: V2 DATA&UI REFACTORING (by 0xfullex) + * -------------------------------------------------------------------------- + * STOP: Feature PRs affecting this file are currently BLOCKED. + * Only critical bug fixes are accepted during this migration phase. + * + * This file is being refactored to v2 standards. + * Any non-critical changes will conflict with the ongoing work. + * + * 🔗 Context & Status: + * - Contribution Hold: https://github.com/CherryHQ/cherry-studio/issues/10954 + * - v2 Refactor PR : https://github.com/CherryHQ/cherry-studio/pull/10162 + * -------------------------------------------------------------------------- + */ /** * Drizzle Kit configuration for agents database */ diff --git a/src/main/services/agents/services/AgentService.ts b/src/main/services/agents/services/AgentService.ts index 2faa87bb45..7542c1935b 100644 --- a/src/main/services/agents/services/AgentService.ts +++ b/src/main/services/agents/services/AgentService.ts @@ -89,7 +89,9 @@ export class AgentService extends BaseService { } const agent = this.deserializeJsonFields(result[0]) as GetAgentResponse - agent.tools = await this.listMcpTools(agent.type, agent.mcps) + const { tools, legacyIdMap } = await this.listMcpTools(agent.type, agent.mcps) + agent.tools = tools + agent.allowed_tools = this.normalizeAllowedTools(agent.allowed_tools, agent.tools, legacyIdMap) // Load installed_plugins from cache file instead of database const workdir = agent.accessible_paths?.[0] @@ -134,7 +136,9 @@ export class AgentService extends BaseService { const agents = result.map((row) => this.deserializeJsonFields(row)) as GetAgentResponse[] for (const agent of agents) { - agent.tools = await this.listMcpTools(agent.type, agent.mcps) + const { tools, legacyIdMap } = await this.listMcpTools(agent.type, agent.mcps) + agent.tools = tools + agent.allowed_tools = this.normalizeAllowedTools(agent.allowed_tools, agent.tools, legacyIdMap) } return { agents, total: totalResult[0].count } diff --git a/src/main/services/agents/services/SessionService.ts b/src/main/services/agents/services/SessionService.ts index d933ef8dd9..90b32bb31c 100644 --- a/src/main/services/agents/services/SessionService.ts +++ b/src/main/services/agents/services/SessionService.ts @@ -156,7 +156,9 @@ export class SessionService extends BaseService { } const session = this.deserializeJsonFields(result[0]) as GetAgentSessionResponse - session.tools = await this.listMcpTools(session.agent_type, session.mcps) + const { tools, legacyIdMap } = await this.listMcpTools(session.agent_type, session.mcps) + session.tools = tools + session.allowed_tools = this.normalizeAllowedTools(session.allowed_tools, session.tools, legacyIdMap) // If slash_commands is not in database yet (e.g., first invoke before init message), // fall back to builtin + local commands. Otherwise, use the merged commands from database. @@ -202,6 +204,12 @@ export class SessionService extends BaseService { const sessions = result.map((row) => this.deserializeJsonFields(row)) as GetAgentSessionResponse[] + for (const session of sessions) { + const { tools, legacyIdMap } = await this.listMcpTools(session.agent_type, session.mcps) + session.tools = tools + session.allowed_tools = this.normalizeAllowedTools(session.allowed_tools, session.tools, legacyIdMap) + } + return { sessions, total } } diff --git a/src/main/services/agents/tests/BaseService.test.ts b/src/main/services/agents/tests/BaseService.test.ts new file mode 100644 index 0000000000..fe2f4e103a --- /dev/null +++ b/src/main/services/agents/tests/BaseService.test.ts @@ -0,0 +1,91 @@ +import type { Tool } from '@types' +import { describe, expect, it, vi } from 'vitest' + +vi.mock('@main/apiServer/services/mcp', () => ({ + mcpApiService: { + getServerInfo: vi.fn() + } +})) + +vi.mock('@main/apiServer/utils', () => ({ + validateModelId: vi.fn() +})) + +import { BaseService } from '../BaseService' + +class TestBaseService extends BaseService { + public normalize( + allowedTools: string[] | undefined, + tools: Tool[], + legacyIdMap?: Map + ): string[] | undefined { + return this.normalizeAllowedTools(allowedTools, tools, legacyIdMap) + } +} + +const buildMcpTool = (id: string): Tool => ({ + id, + name: id, + type: 'mcp', + description: 'test tool', + requirePermissions: true +}) + +describe('BaseService.normalizeAllowedTools', () => { + const service = new TestBaseService() + + it('returns undefined or empty inputs unchanged', () => { + expect(service.normalize(undefined, [])).toBeUndefined() + expect(service.normalize([], [])).toEqual([]) + }) + + it('normalizes legacy MCP tool IDs and deduplicates entries', () => { + const tools: Tool[] = [ + buildMcpTool('mcp__server_one__tool_one'), + buildMcpTool('mcp__server_two__tool_two'), + { id: 'custom_tool', name: 'custom_tool', type: 'custom' } + ] + + const legacyIdMap = new Map([ + ['mcp__server-1__tool-one', 'mcp__server_one__tool_one'], + ['mcp_server-1_tool-one', 'mcp__server_one__tool_one'], + ['mcp__server-2__tool-two', 'mcp__server_two__tool_two'] + ]) + + const allowedTools = [ + 'mcp__server-1__tool-one', + 'mcp_server-1_tool-one', + 'mcp_server_one_tool_one', + 'mcp__server_one__tool_one', + 'custom_tool', + 'mcp__server_two__tool_two', + 'mcp_server_two_tool_two', + 'mcp__server-2__tool-two' + ] + + expect(service.normalize(allowedTools, tools, legacyIdMap)).toEqual([ + 'mcp__server_one__tool_one', + 'custom_tool', + 'mcp__server_two__tool_two' + ]) + }) + + it('keeps legacy IDs when no matching MCP tool exists', () => { + const tools: Tool[] = [buildMcpTool('mcp__server_one__tool_one')] + const legacyIdMap = new Map([['mcp__server-1__tool-one', 'mcp__server_one__tool_one']]) + + const allowedTools = ['mcp__unknown__tool', 'mcp__server_one__tool_one'] + + expect(service.normalize(allowedTools, tools, legacyIdMap)).toEqual([ + 'mcp__unknown__tool', + 'mcp__server_one__tool_one' + ]) + }) + + it('returns allowed tools unchanged when no MCP tools are available', () => { + const allowedTools = ['custom_tool', 'builtin_tool'] + const tools: Tool[] = [{ id: 'custom_tool', name: 'custom_tool', type: 'custom' }] + + expect(service.normalize(allowedTools, tools)).toEqual(allowedTools) + }) +}) diff --git a/src/main/utils/__tests__/mcp.test.ts b/src/main/utils/__tests__/mcp.test.ts index b1a35f925e..706a44bc84 100644 --- a/src/main/utils/__tests__/mcp.test.ts +++ b/src/main/utils/__tests__/mcp.test.ts @@ -3,194 +3,223 @@ import { describe, expect, it } from 'vitest' import { buildFunctionCallToolName } from '../mcp' describe('buildFunctionCallToolName', () => { - describe('basic functionality', () => { - it('should combine server name and tool name', () => { + describe('basic format', () => { + it('should return format mcp__{server}__{tool}', () => { const result = buildFunctionCallToolName('github', 'search_issues') - expect(result).toContain('github') - expect(result).toContain('search') + expect(result).toBe('mcp__github__search_issues') }) - it('should sanitize names by replacing dashes with underscores', () => { - const result = buildFunctionCallToolName('my-server', 'my-tool') - // Input dashes are replaced, but the separator between server and tool is a dash - expect(result).toBe('my_serv-my_tool') - expect(result).toContain('_') - }) - - it('should handle empty server names gracefully', () => { - const result = buildFunctionCallToolName('', 'tool') - expect(result).toBeTruthy() + it('should handle simple server and tool names', () => { + expect(buildFunctionCallToolName('fetch', 'get_page')).toBe('mcp__fetch__get_page') + expect(buildFunctionCallToolName('database', 'query')).toBe('mcp__database__query') + expect(buildFunctionCallToolName('cherry_studio', 'search')).toBe('mcp__cherry_studio__search') }) }) - describe('uniqueness with serverId', () => { - it('should generate different IDs for same server name but different serverIds', () => { - const serverId1 = 'server-id-123456' - const serverId2 = 'server-id-789012' - const serverName = 'github' - const toolName = 'search_repos' - - const result1 = buildFunctionCallToolName(serverName, toolName, serverId1) - const result2 = buildFunctionCallToolName(serverName, toolName, serverId2) - - expect(result1).not.toBe(result2) - expect(result1).toContain('123456') - expect(result2).toContain('789012') + describe('valid JavaScript identifier', () => { + it('should always start with mcp__ prefix (valid JS identifier start)', () => { + const result = buildFunctionCallToolName('123server', '456tool') + expect(result).toMatch(/^mcp__/) + expect(result).toBe('mcp__123server__456tool') }) - it('should generate same ID when serverId is not provided', () => { + it('should only contain alphanumeric chars and underscores', () => { + const result = buildFunctionCallToolName('my-server', 'my-tool') + expect(result).toBe('mcp__my_server__my_tool') + expect(result).toMatch(/^[a-zA-Z][a-zA-Z0-9_]*$/) + }) + + it('should be a valid JavaScript identifier', () => { + const testCases = [ + ['github', 'create_issue'], + ['my-server', 'fetch-data'], + ['test@server', 'tool#name'], + ['server.name', 'tool.action'], + ['123abc', 'def456'] + ] + + for (const [server, tool] of testCases) { + const result = buildFunctionCallToolName(server, tool) + // Valid JS identifiers match this pattern + expect(result).toMatch(/^[a-zA-Z_][a-zA-Z0-9_]*$/) + } + }) + }) + + describe('character sanitization', () => { + it('should replace dashes with underscores', () => { + const result = buildFunctionCallToolName('my-server', 'my-tool-name') + expect(result).toBe('mcp__my_server__my_tool_name') + }) + + it('should replace special characters with underscores', () => { + const result = buildFunctionCallToolName('test@server!', 'tool#name$') + expect(result).toBe('mcp__test_server__tool_name') + }) + + it('should replace dots with underscores', () => { + const result = buildFunctionCallToolName('server.name', 'tool.action') + expect(result).toBe('mcp__server_name__tool_action') + }) + + it('should replace spaces with underscores', () => { + const result = buildFunctionCallToolName('my server', 'my tool') + expect(result).toBe('mcp__my_server__my_tool') + }) + + it('should collapse consecutive underscores', () => { + const result = buildFunctionCallToolName('my--server', 'my___tool') + expect(result).toBe('mcp__my_server__my_tool') + expect(result).not.toMatch(/_{3,}/) + }) + + it('should trim leading and trailing underscores from parts', () => { + const result = buildFunctionCallToolName('_server_', '_tool_') + expect(result).toBe('mcp__server__tool') + }) + + it('should handle names with only special characters', () => { + const result = buildFunctionCallToolName('---', '###') + expect(result).toBe('mcp____') + }) + }) + + describe('length constraints', () => { + it('should not exceed 63 characters', () => { + const longServerName = 'a'.repeat(50) + const longToolName = 'b'.repeat(50) + const result = buildFunctionCallToolName(longServerName, longToolName) + + expect(result.length).toBeLessThanOrEqual(63) + }) + + it('should truncate server name to max 20 chars', () => { + const longServerName = 'abcdefghijklmnopqrstuvwxyz' // 26 chars + const result = buildFunctionCallToolName(longServerName, 'tool') + + expect(result).toBe('mcp__abcdefghijklmnopqrst__tool') + expect(result).toContain('abcdefghijklmnopqrst') // First 20 chars + expect(result).not.toContain('uvwxyz') // Truncated + }) + + it('should truncate tool name to max 35 chars', () => { + const longToolName = 'a'.repeat(40) + const result = buildFunctionCallToolName('server', longToolName) + + const expectedTool = 'a'.repeat(35) + expect(result).toBe(`mcp__server__${expectedTool}`) + }) + + it('should not end with underscores after truncation', () => { + // Create a name that would end with underscores after truncation + const longServerName = 'a'.repeat(20) + const longToolName = 'b'.repeat(35) + '___extra' + const result = buildFunctionCallToolName(longServerName, longToolName) + + expect(result).not.toMatch(/_+$/) + expect(result.length).toBeLessThanOrEqual(63) + }) + + it('should handle max length edge case exactly', () => { + // mcp__ (5) + server (20) + __ (2) + tool (35) = 62 chars + const server = 'a'.repeat(20) + const tool = 'b'.repeat(35) + const result = buildFunctionCallToolName(server, tool) + + expect(result.length).toBe(62) + expect(result).toBe(`mcp__${'a'.repeat(20)}__${'b'.repeat(35)}`) + }) + }) + + describe('edge cases', () => { + it('should handle empty server name', () => { + const result = buildFunctionCallToolName('', 'tool') + expect(result).toBe('mcp____tool') + }) + + it('should handle empty tool name', () => { + const result = buildFunctionCallToolName('server', '') + expect(result).toBe('mcp__server__') + }) + + it('should handle both empty names', () => { + const result = buildFunctionCallToolName('', '') + expect(result).toBe('mcp____') + }) + + it('should handle whitespace-only names', () => { + const result = buildFunctionCallToolName(' ', ' ') + expect(result).toBe('mcp____') + }) + + it('should trim whitespace from names', () => { + const result = buildFunctionCallToolName(' server ', ' tool ') + expect(result).toBe('mcp__server__tool') + }) + + it('should handle unicode characters', () => { + const result = buildFunctionCallToolName('服务器', '工具') + // Unicode chars are replaced with underscores, then collapsed + expect(result).toMatch(/^mcp__/) + }) + + it('should handle mixed case', () => { + const result = buildFunctionCallToolName('MyServer', 'MyTool') + expect(result).toBe('mcp__MyServer__MyTool') + }) + }) + + describe('deterministic output', () => { + it('should produce consistent results for same input', () => { const serverName = 'github' const toolName = 'search_repos' const result1 = buildFunctionCallToolName(serverName, toolName) const result2 = buildFunctionCallToolName(serverName, toolName) + const result3 = buildFunctionCallToolName(serverName, toolName) expect(result1).toBe(result2) + expect(result2).toBe(result3) }) - it('should include serverId suffix when provided', () => { - const serverId = 'abc123def456' - const result = buildFunctionCallToolName('server', 'tool', serverId) + it('should produce different results for different inputs', () => { + const result1 = buildFunctionCallToolName('server1', 'tool') + const result2 = buildFunctionCallToolName('server2', 'tool') + const result3 = buildFunctionCallToolName('server', 'tool1') + const result4 = buildFunctionCallToolName('server', 'tool2') - // Should include last 6 chars of serverId - expect(result).toContain('ef456') - }) - }) - - describe('character sanitization', () => { - it('should replace invalid characters with underscores', () => { - const result = buildFunctionCallToolName('test@server', 'tool#name') - expect(result).not.toMatch(/[@#]/) - expect(result).toMatch(/^[a-zA-Z0-9_-]+$/) - }) - - it('should ensure name starts with a letter', () => { - const result = buildFunctionCallToolName('123server', '456tool') - expect(result).toMatch(/^[a-zA-Z]/) - }) - - it('should handle consecutive underscores/dashes', () => { - const result = buildFunctionCallToolName('my--server', 'my__tool') - expect(result).not.toMatch(/[_-]{2,}/) - }) - }) - - describe('length constraints', () => { - it('should truncate names longer than 63 characters', () => { - const longServerName = 'a'.repeat(50) - const longToolName = 'b'.repeat(50) - const result = buildFunctionCallToolName(longServerName, longToolName, 'id123456') - - expect(result.length).toBeLessThanOrEqual(63) - }) - - it('should not end with underscore or dash after truncation', () => { - const longServerName = 'a'.repeat(50) - const longToolName = 'b'.repeat(50) - const result = buildFunctionCallToolName(longServerName, longToolName, 'id123456') - - expect(result).not.toMatch(/[_-]$/) - }) - - it('should preserve serverId suffix even with long server/tool names', () => { - const longServerName = 'a'.repeat(50) - const longToolName = 'b'.repeat(50) - const serverId = 'server-id-xyz789' - - const result = buildFunctionCallToolName(longServerName, longToolName, serverId) - - // The suffix should be preserved and not truncated - expect(result).toContain('xyz789') - expect(result.length).toBeLessThanOrEqual(63) - }) - - it('should ensure two long-named servers with different IDs produce different results', () => { - const longServerName = 'a'.repeat(50) - const longToolName = 'b'.repeat(50) - const serverId1 = 'server-id-abc123' - const serverId2 = 'server-id-def456' - - const result1 = buildFunctionCallToolName(longServerName, longToolName, serverId1) - const result2 = buildFunctionCallToolName(longServerName, longToolName, serverId2) - - // Both should be within limit - expect(result1.length).toBeLessThanOrEqual(63) - expect(result2.length).toBeLessThanOrEqual(63) - - // They should be different due to preserved suffix expect(result1).not.toBe(result2) - }) - }) - - describe('edge cases with serverId', () => { - it('should handle serverId with only non-alphanumeric characters', () => { - const serverId = '------' // All dashes - const result = buildFunctionCallToolName('server', 'tool', serverId) - - // Should still produce a valid unique suffix via fallback hash - expect(result).toBeTruthy() - expect(result.length).toBeLessThanOrEqual(63) - expect(result).toMatch(/^[a-zA-Z][a-zA-Z0-9_-]*$/) - // Should have a suffix (underscore followed by something) - expect(result).toMatch(/_[a-z0-9]+$/) - }) - - it('should produce different results for different non-alphanumeric serverIds', () => { - const serverId1 = '------' - const serverId2 = '!!!!!!' - - const result1 = buildFunctionCallToolName('server', 'tool', serverId1) - const result2 = buildFunctionCallToolName('server', 'tool', serverId2) - - // Should be different because the hash fallback produces different values - expect(result1).not.toBe(result2) - }) - - it('should handle empty string serverId differently from undefined', () => { - const resultWithEmpty = buildFunctionCallToolName('server', 'tool', '') - const resultWithUndefined = buildFunctionCallToolName('server', 'tool', undefined) - - // Empty string is falsy, so both should behave the same (no suffix) - expect(resultWithEmpty).toBe(resultWithUndefined) - }) - - it('should handle serverId with mixed alphanumeric and special chars', () => { - const serverId = 'ab@#cd' // Mixed chars, last 6 chars contain some alphanumeric - const result = buildFunctionCallToolName('server', 'tool', serverId) - - // Should extract alphanumeric chars: 'abcd' from 'ab@#cd' - expect(result).toContain('abcd') + expect(result3).not.toBe(result4) }) }) describe('real-world scenarios', () => { - it('should handle GitHub MCP server instances correctly', () => { - const serverName = 'github' - const toolName = 'search_repositories' - - const githubComId = 'server-github-com-abc123' - const gheId = 'server-ghe-internal-xyz789' - - const tool1 = buildFunctionCallToolName(serverName, toolName, githubComId) - const tool2 = buildFunctionCallToolName(serverName, toolName, gheId) - - // Should be different - expect(tool1).not.toBe(tool2) - - // Both should be valid identifiers - expect(tool1).toMatch(/^[a-zA-Z][a-zA-Z0-9_-]*$/) - expect(tool2).toMatch(/^[a-zA-Z][a-zA-Z0-9_-]*$/) - - // Both should be <= 63 chars - expect(tool1.length).toBeLessThanOrEqual(63) - expect(tool2.length).toBeLessThanOrEqual(63) + it('should handle GitHub MCP server', () => { + expect(buildFunctionCallToolName('github', 'create_issue')).toBe('mcp__github__create_issue') + expect(buildFunctionCallToolName('github', 'search_repositories')).toBe('mcp__github__search_repositories') + expect(buildFunctionCallToolName('github', 'get_pull_request')).toBe('mcp__github__get_pull_request') }) - it('should handle tool names that already include server name prefix', () => { - const result = buildFunctionCallToolName('github', 'github_search_repos') - expect(result).toBeTruthy() - // Should not double the server name - expect(result.split('github').length - 1).toBeLessThanOrEqual(2) + it('should handle filesystem MCP server', () => { + expect(buildFunctionCallToolName('filesystem', 'read_file')).toBe('mcp__filesystem__read_file') + expect(buildFunctionCallToolName('filesystem', 'write_file')).toBe('mcp__filesystem__write_file') + expect(buildFunctionCallToolName('filesystem', 'list_directory')).toBe('mcp__filesystem__list_directory') + }) + + it('should handle hyphenated server names (common in npm packages)', () => { + expect(buildFunctionCallToolName('cherry-fetch', 'get_page')).toBe('mcp__cherry_fetch__get_page') + expect(buildFunctionCallToolName('mcp-server-github', 'search')).toBe('mcp__mcp_server_github__search') + }) + + it('should handle scoped npm package style names', () => { + const result = buildFunctionCallToolName('@anthropic/mcp-server', 'chat') + expect(result).toBe('mcp__anthropic_mcp_server__chat') + }) + + it('should handle tools with long descriptive names', () => { + const result = buildFunctionCallToolName('github', 'search_repositories_by_language_and_stars') + expect(result.length).toBeLessThanOrEqual(63) + expect(result).toMatch(/^mcp__github__search_repositories_by_lan/) }) }) }) diff --git a/src/main/utils/mcp.ts b/src/main/utils/mcp.ts index cfa700f2e6..34eb0e63e7 100644 --- a/src/main/utils/mcp.ts +++ b/src/main/utils/mcp.ts @@ -1,56 +1,28 @@ -export function buildFunctionCallToolName(serverName: string, toolName: string, serverId?: string) { - const sanitizedServer = serverName.trim().replace(/-/g, '_') - const sanitizedTool = toolName.trim().replace(/-/g, '_') +/** + * Builds a valid JavaScript function name for MCP tool calls. + * Format: mcp__{server_name}__{tool_name} + * + * @param serverName - The MCP server name + * @param toolName - The tool name from the server + * @returns A valid JS identifier in format mcp__{server}__{tool}, max 63 chars + */ +export function buildFunctionCallToolName(serverName: string, toolName: string): string { + // Sanitize to valid JS identifier chars (alphanumeric + underscore only) + const sanitize = (str: string): string => + str + .trim() + .replace(/[^a-zA-Z0-9]/g, '_') // Replace all non-alphanumeric with underscore + .replace(/_{2,}/g, '_') // Collapse multiple underscores + .replace(/^_+|_+$/g, '') // Trim leading/trailing underscores - // Calculate suffix first to reserve space for it - // Suffix format: "_" + 6 alphanumeric chars = 7 chars total - let serverIdSuffix = '' - if (serverId) { - // Take the last 6 characters of the serverId for brevity - serverIdSuffix = serverId.slice(-6).replace(/[^a-zA-Z0-9]/g, '') + const server = sanitize(serverName).slice(0, 20) // Keep server name short + const tool = sanitize(toolName).slice(0, 35) // More room for tool name - // Fallback: if suffix becomes empty (all non-alphanumeric chars), use a simple hash - if (!serverIdSuffix) { - const hash = serverId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0) - serverIdSuffix = hash.toString(36).slice(-6) || 'x' - } - } + let name = `mcp__${server}__${tool}` - // Reserve space for suffix when calculating max base name length - const SUFFIX_LENGTH = serverIdSuffix ? serverIdSuffix.length + 1 : 0 // +1 for underscore - const MAX_BASE_LENGTH = 63 - SUFFIX_LENGTH - - // Combine server name and tool name - let name = sanitizedTool - if (!sanitizedTool.includes(sanitizedServer.slice(0, 7))) { - name = `${sanitizedServer.slice(0, 7) || ''}-${sanitizedTool || ''}` - } - - // Replace invalid characters with underscores or dashes - // Keep a-z, A-Z, 0-9, underscores and dashes - name = name.replace(/[^a-zA-Z0-9_-]/g, '_') - - // Ensure name starts with a letter or underscore (for valid JavaScript identifier) - if (!/^[a-zA-Z]/.test(name)) { - name = `tool-${name}` - } - - // Remove consecutive underscores/dashes (optional improvement) - name = name.replace(/[_-]{2,}/g, '_') - - // Truncate base name BEFORE adding suffix to ensure suffix is never cut off - if (name.length > MAX_BASE_LENGTH) { - name = name.slice(0, MAX_BASE_LENGTH) - } - - // Handle edge case: ensure we still have a valid name if truncation left invalid chars at edges - if (name.endsWith('_') || name.endsWith('-')) { - name = name.slice(0, -1) - } - - // Now append the suffix - it will always fit within 63 chars - if (serverIdSuffix) { - name = `${name}_${serverIdSuffix}` + // Ensure max 63 chars and clean trailing underscores + if (name.length > 63) { + name = name.slice(0, 63).replace(/_+$/, '') } return name diff --git a/src/main/utils/system.ts b/src/main/utils/system.ts new file mode 100644 index 0000000000..2cd9e4bf22 --- /dev/null +++ b/src/main/utils/system.ts @@ -0,0 +1,19 @@ +import os from 'node:os' + +import { isMac, isWin } from '@main/constant' + +export const getDeviceType = () => (isMac ? 'mac' : isWin ? 'windows' : 'linux') + +export const getHostname = () => os.hostname() + +export const getCpuName = () => { + try { + const cpus = os.cpus() + if (!cpus || cpus.length === 0 || !cpus[0].model) { + return 'Unknown CPU' + } + return cpus[0].model + } catch { + return 'Unknown CPU' + } +} diff --git a/src/preload/index.ts b/src/preload/index.ts index 424253f8e3..cb8b0f6919 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -340,6 +340,7 @@ const api = { ipcRenderer.invoke(IpcChannel.VertexAI_ClearAuthCache, projectId, clientEmail) }, ovms: { + isSupported: (): Promise => ipcRenderer.invoke(IpcChannel.Ovms_IsSupported), addModel: (modelName: string, modelId: string, modelSource: string, task: string) => ipcRenderer.invoke(IpcChannel.Ovms_AddModel, modelName, modelId, modelSource, task), stopAddModel: () => ipcRenderer.invoke(IpcChannel.Ovms_StopAddModel), diff --git a/src/renderer/src/aiCore/legacy/clients/zhipu/ZhipuAPIClient.ts b/src/renderer/src/aiCore/legacy/clients/zhipu/ZhipuAPIClient.ts index ea6c141e31..9c590996f1 100644 --- a/src/renderer/src/aiCore/legacy/clients/zhipu/ZhipuAPIClient.ts +++ b/src/renderer/src/aiCore/legacy/clients/zhipu/ZhipuAPIClient.ts @@ -66,6 +66,11 @@ export class ZhipuAPIClient extends OpenAIAPIClient { public async listModels(): Promise { const models = [ + 'glm-4.7', + 'glm-4.6', + 'glm-4.6v', + 'glm-4.6v-flash', + 'glm-4.6v-flashx', 'glm-4.5', 'glm-4.5-x', 'glm-4.5-air', diff --git a/src/renderer/src/aiCore/utils/options.ts b/src/renderer/src/aiCore/utils/options.ts index 629e5b4153..a2a284da7e 100644 --- a/src/renderer/src/aiCore/utils/options.ts +++ b/src/renderer/src/aiCore/utils/options.ts @@ -538,8 +538,10 @@ function buildOllamaProviderOptions( const reasoningEffort = assistant.settings?.reasoning_effort if (enableReasoning) { if (isOpenAIOpenWeightModel(model)) { - // @ts-ignore upstream type error - providerOptions.think = reasoningEffort as any + // For gpt-oss models, Ollama accepts: 'low' | 'medium' | 'high' + if (reasoningEffort === 'low' || reasoningEffort === 'medium' || reasoningEffort === 'high') { + providerOptions.think = reasoningEffort + } } else { providerOptions.think = !['none', undefined].includes(reasoningEffort) } diff --git a/src/renderer/src/assets/images/apps/aistudio.png b/src/renderer/src/assets/images/apps/aistudio.png new file mode 100644 index 0000000000..c7cb2adebe Binary files /dev/null and b/src/renderer/src/assets/images/apps/aistudio.png differ diff --git a/src/renderer/src/assets/images/apps/aistudio.svg b/src/renderer/src/assets/images/apps/aistudio.svg deleted file mode 100644 index 2c08015593..0000000000 --- a/src/renderer/src/assets/images/apps/aistudio.svg +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/renderer/src/config/minapps.ts b/src/renderer/src/config/minapps.ts index 81a4a98723..eeefb218d2 100644 --- a/src/renderer/src/config/minapps.ts +++ b/src/renderer/src/config/minapps.ts @@ -1,7 +1,7 @@ import { loggerService } from '@logger' import ThreeMinTopAppLogo from '@renderer/assets/images/apps/3mintop.png?url' import AbacusLogo from '@renderer/assets/images/apps/abacus.webp?url' -import AIStudioLogo from '@renderer/assets/images/apps/aistudio.svg?url' +import AIStudioLogo from '@renderer/assets/images/apps/aistudio.png?url' import ApplicationLogo from '@renderer/assets/images/apps/application.png?url' import BaiduAiAppLogo from '@renderer/assets/images/apps/baidu-ai.png?url' import BaiduAiSearchLogo from '@renderer/assets/images/apps/baidu-ai-search.webp?url' diff --git a/src/renderer/src/config/models/__tests__/reasoning.test.ts b/src/renderer/src/config/models/__tests__/reasoning.test.ts index 6b00a8912b..56f9cd0b60 100644 --- a/src/renderer/src/config/models/__tests__/reasoning.test.ts +++ b/src/renderer/src/config/models/__tests__/reasoning.test.ts @@ -680,7 +680,12 @@ describe('getThinkModelType - Comprehensive Coverage', () => { expect(getThinkModelType(createModel({ id: 'o3' }))).toBe('o') expect(getThinkModelType(createModel({ id: 'o3-mini' }))).toBe('o') expect(getThinkModelType(createModel({ id: 'o4' }))).toBe('o') - expect(getThinkModelType(createModel({ id: 'gpt-oss-reasoning' }))).toBe('o') + }) + + it('should return gpt_oss for gpt-oss models', () => { + expect(getThinkModelType(createModel({ id: 'gpt-oss' }))).toBe('gpt_oss') + expect(getThinkModelType(createModel({ id: 'gpt-oss:20b' }))).toBe('gpt_oss') + expect(getThinkModelType(createModel({ id: 'gpt-oss-reasoning' }))).toBe('gpt_oss') }) }) @@ -1763,6 +1768,21 @@ describe('getModelSupportedReasoningEffortOptions', () => { 'medium', 'high' ]) + }) + + it('should return correct options for gpt-oss models', () => { + expect(getModelSupportedReasoningEffortOptions(createModel({ id: 'gpt-oss' }))).toEqual([ + 'default', + 'low', + 'medium', + 'high' + ]) + expect(getModelSupportedReasoningEffortOptions(createModel({ id: 'gpt-oss:20b' }))).toEqual([ + 'default', + 'low', + 'medium', + 'high' + ]) expect(getModelSupportedReasoningEffortOptions(createModel({ id: 'gpt-oss-reasoning' }))).toEqual([ 'default', 'low', diff --git a/src/renderer/src/config/models/default.ts b/src/renderer/src/config/models/default.ts index f87293798d..1223d0c92c 100644 --- a/src/renderer/src/config/models/default.ts +++ b/src/renderer/src/config/models/default.ts @@ -617,6 +617,24 @@ export const SYSTEM_MODELS: Record = name: 'GLM-4.6', group: 'GLM-4.6' }, + { + id: 'glm-4.6v', + provider: 'zhipu', + name: 'GLM-4.6V', + group: 'GLM-4.6V' + }, + { + id: 'glm-4.6v-flash', + provider: 'zhipu', + name: 'GLM-4.6V-Flash', + group: 'GLM-4.6V' + }, + { + id: 'glm-4.6v-flashx', + provider: 'zhipu', + name: 'GLM-4.6V-FlashX', + group: 'GLM-4.6V' + }, { id: 'glm-4.7', provider: 'zhipu', diff --git a/src/renderer/src/config/models/reasoning.ts b/src/renderer/src/config/models/reasoning.ts index 5d48e9a122..b2b6119b76 100644 --- a/src/renderer/src/config/models/reasoning.ts +++ b/src/renderer/src/config/models/reasoning.ts @@ -17,6 +17,7 @@ import { isGPT52ProModel, isGPT52SeriesModel, isOpenAIDeepResearchModel, + isOpenAIOpenWeightModel, isOpenAIReasoningModel, isSupportedReasoningEffortOpenAIModel } from './openai' @@ -41,6 +42,7 @@ export const MODEL_SUPPORTED_REASONING_EFFORT = { gpt5_2: ['none', 'low', 'medium', 'high', 'xhigh'] as const, gpt5pro: ['high'] as const, gpt52pro: ['medium', 'high', 'xhigh'] as const, + gpt_oss: ['low', 'medium', 'high'] as const, grok: ['low', 'high'] as const, grok4_fast: ['auto'] as const, gemini2_flash: ['low', 'medium', 'high', 'auto'] as const, @@ -72,6 +74,7 @@ export const MODEL_SUPPORTED_OPTIONS: ThinkingOptionConfig = { gpt5_2: ['default', ...MODEL_SUPPORTED_REASONING_EFFORT.gpt5_2] as const, gpt5_1_codex_max: ['default', ...MODEL_SUPPORTED_REASONING_EFFORT.gpt5_1_codex_max] as const, gpt52pro: ['default', ...MODEL_SUPPORTED_REASONING_EFFORT.gpt52pro] as const, + gpt_oss: ['default', ...MODEL_SUPPORTED_REASONING_EFFORT.gpt_oss] as const, grok: ['default', ...MODEL_SUPPORTED_REASONING_EFFORT.grok] as const, grok4_fast: ['default', 'none', ...MODEL_SUPPORTED_REASONING_EFFORT.grok4_fast] as const, gemini2_flash: ['default', 'none', ...MODEL_SUPPORTED_REASONING_EFFORT.gemini2_flash] as const, @@ -127,6 +130,8 @@ const _getThinkModelType = (model: Model): ThinkingModelType => { thinkingModelType = 'gpt5pro' } } + } else if (isOpenAIOpenWeightModel(model)) { + thinkingModelType = 'gpt_oss' } else if (isSupportedReasoningEffortOpenAIModel(model)) { thinkingModelType = 'o' } else if (isGrok4FastReasoningModel(model)) { diff --git a/src/renderer/src/config/providers.ts b/src/renderer/src/config/providers.ts index bae473a7d7..9e2831ee6e 100644 --- a/src/renderer/src/config/providers.ts +++ b/src/renderer/src/config/providers.ts @@ -200,7 +200,8 @@ export const SYSTEM_PROVIDERS_CONFIG: Record = name: 'TokenFlux', type: 'openai', apiKey: '', - apiHost: 'https://tokenflux.ai', + apiHost: 'https://api.tokenflux.ai/openai/v1', + anthropicApiHost: 'https://api.tokenflux.ai/anthropic', models: SYSTEM_MODELS.tokenflux, isSystem: true, enabled: false @@ -1088,7 +1089,7 @@ export const PROVIDER_URLS: Record = { websites: { official: 'https://platform.minimaxi.com/', apiKey: 'https://platform.minimaxi.com/user-center/basic-information/interface-key', - docs: 'https://platform.minimaxi.com/document/Announcement', + docs: 'https://platform.minimaxi.com/docs/api-reference/text-openai-api', models: 'https://platform.minimaxi.com/document/Models' } }, diff --git a/src/renderer/src/databases/index.ts b/src/renderer/src/databases/index.ts index fc47e37cb7..f70b81673f 100644 --- a/src/renderer/src/databases/index.ts +++ b/src/renderer/src/databases/index.ts @@ -1,3 +1,19 @@ +/** + * @deprecated Scheduled for removal in v2.0.0 + * -------------------------------------------------------------------------- + * ⚠️ NOTICE: V2 DATA&UI REFACTORING (by 0xfullex) + * -------------------------------------------------------------------------- + * STOP: Feature PRs affecting this file are currently BLOCKED. + * Only critical bug fixes are accepted during this migration phase. + * + * This file is being refactored to v2 standards. + * Any non-critical changes will conflict with the ongoing work. + * + * 🔗 Context & Status: + * - Contribution Hold: https://github.com/CherryHQ/cherry-studio/issues/10954 + * - v2 Refactor PR : https://github.com/CherryHQ/cherry-studio/pull/10162 + * -------------------------------------------------------------------------- + */ import type { CustomTranslateLanguage, FileMetadata, diff --git a/src/renderer/src/databases/upgrades.ts b/src/renderer/src/databases/upgrades.ts index 8f952e245b..83e77e7c42 100644 --- a/src/renderer/src/databases/upgrades.ts +++ b/src/renderer/src/databases/upgrades.ts @@ -1,3 +1,19 @@ +/** + * @deprecated Scheduled for removal in v2.0.0 + * -------------------------------------------------------------------------- + * ⚠️ NOTICE: V2 DATA&UI REFACTORING (by 0xfullex) + * -------------------------------------------------------------------------- + * STOP: Feature PRs affecting this file are currently BLOCKED. + * Only critical bug fixes are accepted during this migration phase. + * + * This file is being refactored to v2 standards. + * Any non-critical changes will conflict with the ongoing work. + * + * 🔗 Context & Status: + * - Contribution Hold: https://github.com/CherryHQ/cherry-studio/issues/10954 + * - v2 Refactor PR : https://github.com/CherryHQ/cherry-studio/pull/10162 + * -------------------------------------------------------------------------- + */ import { loggerService } from '@logger' import { LanguagesEnum } from '@renderer/config/translate' import type { LegacyMessage as OldMessage, Topic, TranslateLanguageCode } from '@renderer/types' diff --git a/src/renderer/src/hooks/useSettings.ts b/src/renderer/src/hooks/useSettings.ts index 55d40e435d..3a3de7f89a 100644 --- a/src/renderer/src/hooks/useSettings.ts +++ b/src/renderer/src/hooks/useSettings.ts @@ -1,3 +1,19 @@ +/** + * @deprecated Scheduled for removal in v2.0.0 + * -------------------------------------------------------------------------- + * ⚠️ NOTICE: V2 DATA&UI REFACTORING (by 0xfullex) + * -------------------------------------------------------------------------- + * STOP: Feature PRs affecting this file are currently BLOCKED. + * Only critical bug fixes are accepted during this migration phase. + * + * This file is being refactored to v2 standards. + * Any non-critical changes will conflict with the ongoing work. + * + * 🔗 Context & Status: + * - Contribution Hold: https://github.com/CherryHQ/cherry-studio/issues/10954 + * - v2 Refactor PR : https://github.com/CherryHQ/cherry-studio/pull/10162 + * -------------------------------------------------------------------------- + */ import store, { useAppDispatch, useAppSelector } from '@renderer/store' import type { AssistantIconType, SendMessageShortcut, SettingsState } from '@renderer/store/settings' import { diff --git a/src/renderer/src/hooks/useShortcuts.ts b/src/renderer/src/hooks/useShortcuts.ts index ef92a5f970..ea1c0cab67 100644 --- a/src/renderer/src/hooks/useShortcuts.ts +++ b/src/renderer/src/hooks/useShortcuts.ts @@ -1,3 +1,19 @@ +/** + * @deprecated Scheduled for removal in v2.0.0 + * -------------------------------------------------------------------------- + * ⚠️ NOTICE: V2 DATA&UI REFACTORING (by 0xfullex) + * -------------------------------------------------------------------------- + * STOP: Feature PRs affecting this file are currently BLOCKED. + * Only critical bug fixes are accepted during this migration phase. + * + * This file is being refactored to v2 standards. + * Any non-critical changes will conflict with the ongoing work. + * + * 🔗 Context & Status: + * - Contribution Hold: https://github.com/CherryHQ/cherry-studio/issues/10954 + * - v2 Refactor PR : https://github.com/CherryHQ/cherry-studio/pull/10162 + * -------------------------------------------------------------------------- + */ import { isMac, isWin } from '@renderer/config/constant' import { useAppSelector } from '@renderer/store' import { orderBy } from 'lodash' diff --git a/src/renderer/src/hooks/useStore.ts b/src/renderer/src/hooks/useStore.ts index bb77e1f0da..55720b60e6 100644 --- a/src/renderer/src/hooks/useStore.ts +++ b/src/renderer/src/hooks/useStore.ts @@ -1,3 +1,19 @@ +/** + * @deprecated Scheduled for removal in v2.0.0 + * -------------------------------------------------------------------------- + * ⚠️ NOTICE: V2 DATA&UI REFACTORING (by 0xfullex) + * -------------------------------------------------------------------------- + * STOP: Feature PRs affecting this file are currently BLOCKED. + * Only critical bug fixes are accepted during this migration phase. + * + * This file is being refactored to v2 standards. + * Any non-critical changes will conflict with the ongoing work. + * + * 🔗 Context & Status: + * - Contribution Hold: https://github.com/CherryHQ/cherry-studio/issues/10954 + * - v2 Refactor PR : https://github.com/CherryHQ/cherry-studio/pull/10162 + * -------------------------------------------------------------------------- + */ import { CHERRYAI_PROVIDER } from '@renderer/config/providers' import store, { useAppDispatch, useAppSelector } from '@renderer/store' import { diff --git a/src/renderer/src/pages/home/Inputbar/tools/components/useActivityDirectoryPanel.tsx b/src/renderer/src/pages/home/Inputbar/tools/components/useActivityDirectoryPanel.tsx index b83c00c42d..e15529e66c 100644 --- a/src/renderer/src/pages/home/Inputbar/tools/components/useActivityDirectoryPanel.tsx +++ b/src/renderer/src/pages/home/Inputbar/tools/components/useActivityDirectoryPanel.tsx @@ -9,6 +9,7 @@ import { useTranslation } from 'react-i18next' const logger = loggerService.withContext('useActivityDirectoryPanel') const MAX_FILE_RESULTS = 500 +const MAX_SEARCH_RESULTS = 20 const areFileListsEqual = (prev: string[], next: string[]) => { if (prev === next) return true if (prev.length !== next.length) return false @@ -193,11 +194,11 @@ export const useActivityDirectoryPanel = (params: Params, role: 'button' | 'mana try { const files = await window.api.file.listDirectory(dirPath, { recursive: true, - maxDepth: 4, + maxDepth: 10, includeHidden: false, includeFiles: true, includeDirectories: true, - maxEntries: MAX_FILE_RESULTS, + maxEntries: MAX_SEARCH_RESULTS, searchPattern: searchPattern || '.' }) diff --git a/src/renderer/src/pages/home/Markdown/Table.tsx b/src/renderer/src/pages/home/Markdown/Table.tsx index 01f325bd44..a91ce1cc91 100644 --- a/src/renderer/src/pages/home/Markdown/Table.tsx +++ b/src/renderer/src/pages/home/Markdown/Table.tsx @@ -4,6 +4,7 @@ import store from '@renderer/store' import { messageBlocksSelectors } from '@renderer/store/messageBlock' import { Tooltip } from 'antd' import { Check } from 'lucide-react' +import MarkdownIt from 'markdown-it' import React, { memo, useCallback } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' @@ -22,18 +23,26 @@ const Table: React.FC = ({ children, node, blockId }) => { const { t } = useTranslation() const [copied, setCopied] = useTemporaryValue(false, 2000) - const handleCopyTable = useCallback(() => { + const handleCopyTable = useCallback(async () => { const tableMarkdown = extractTableMarkdown(blockId ?? '', node?.position) if (!tableMarkdown) return - navigator.clipboard - .writeText(tableMarkdown) - .then(() => { - setCopied(true) - }) - .catch((error) => { - window.toast?.error(`${t('message.copy.failed')}: ${error}`) - }) + try { + const tableHtml = convertMarkdownTableToHtml(tableMarkdown) + + if (navigator.clipboard && window.ClipboardItem) { + const clipboardItem = new ClipboardItem({ + 'text/plain': new Blob([tableMarkdown], { type: 'text/plain' }), + 'text/html': new Blob([tableHtml], { type: 'text/html' }) + }) + await navigator.clipboard.write([clipboardItem]) + } else { + await navigator.clipboard.writeText(tableMarkdown) + } + setCopied(true) + } catch (error) { + window.toast?.error(`${t('message.copy.failed')}: ${error}`) + } }, [blockId, node?.position, setCopied, t]) return ( @@ -60,7 +69,6 @@ export function extractTableMarkdown(blockId: string, position: any): string { if (!position || !blockId) return '' const block = messageBlocksSelectors.selectById(store.getState(), blockId) - if (!block || !('content' in block) || typeof block.content !== 'string') return '' const { start, end } = position @@ -71,6 +79,16 @@ export function extractTableMarkdown(blockId: string, position: any): string { return tableLines.join('\n').trim() } +function convertMarkdownTableToHtml(markdownTable: string): string { + const md = new MarkdownIt({ + html: true, + breaks: false, + linkify: false + }) + + return md.render(markdownTable) +} + const TableWrapper = styled.div` position: relative; diff --git a/src/renderer/src/pages/settings/ProviderSettings/ProviderList.tsx b/src/renderer/src/pages/settings/ProviderSettings/ProviderList.tsx index 7502e4d806..cc19eea6a6 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/ProviderList.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/ProviderList.tsx @@ -21,6 +21,7 @@ import { startTransition, useCallback, useEffect, useRef, useState } from 'react import { useTranslation } from 'react-i18next' import { useSearchParams } from 'react-router-dom' import styled from 'styled-components' +import useSWRImmutable from 'swr/immutable' import AddProviderPopup from './AddProviderPopup' import ModelNotesPopup from './ModelNotesPopup' @@ -30,8 +31,16 @@ import UrlSchemaInfoPopup from './UrlSchemaInfoPopup' const logger = loggerService.withContext('ProviderList') const BUTTON_WRAPPER_HEIGHT = 50 -const systemType = await window.api.system.getDeviceType() -const cpuName = await window.api.system.getCpuName() + +const getIsOvmsSupported = async (): Promise => { + try { + const result = await window.api.ovms.isSupported() + return result + } catch (e) { + logger.warn('Fetching isOvmsSupported failed. Fallback to false.', e as Error) + return false + } +} const ProviderList: FC = () => { const [searchParams, setSearchParams] = useSearchParams() @@ -45,6 +54,8 @@ const ProviderList: FC = () => { const [providerLogos, setProviderLogos] = useState>({}) const listRef = useRef(null) + const { data: isOvmsSupported } = useSWRImmutable('ovms/isSupported', getIsOvmsSupported) + const setSelectedProvider = useCallback((provider: Provider) => { startTransition(() => _setSelectedProvider(provider)) }, []) @@ -278,7 +289,8 @@ const ProviderList: FC = () => { } const filteredProviders = providers.filter((provider) => { - if (provider.id === 'ovms' && (systemType !== 'windows' || !cpuName.toLowerCase().includes('intel'))) { + // don't show it when isOvmsSupported is loading + if (provider.id === 'ovms' && !isOvmsSupported) { return false } diff --git a/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx b/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx index 13680f5547..777bc61984 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx @@ -82,7 +82,8 @@ const ANTHROPIC_COMPATIBLE_PROVIDER_IDS = [ SystemProviderIds.qiniu, SystemProviderIds.dmxapi, SystemProviderIds.mimo, - SystemProviderIds.openrouter + SystemProviderIds.openrouter, + SystemProviderIds.tokenflux ] as const type AnthropicCompatibleProviderId = (typeof ANTHROPIC_COMPATIBLE_PROVIDER_IDS)[number] diff --git a/src/renderer/src/services/db/AgentMessageDataSource.ts b/src/renderer/src/services/db/AgentMessageDataSource.ts index 4ba93d2cd5..7af7a257f8 100644 --- a/src/renderer/src/services/db/AgentMessageDataSource.ts +++ b/src/renderer/src/services/db/AgentMessageDataSource.ts @@ -1,3 +1,19 @@ +/** + * @deprecated Scheduled for removal in v2.0.0 + * -------------------------------------------------------------------------- + * ⚠️ NOTICE: V2 DATA&UI REFACTORING (by 0xfullex) + * -------------------------------------------------------------------------- + * STOP: Feature PRs affecting this file are currently BLOCKED. + * Only critical bug fixes are accepted during this migration phase. + * + * This file is being refactored to v2 standards. + * Any non-critical changes will conflict with the ongoing work. + * + * 🔗 Context & Status: + * - Contribution Hold: https://github.com/CherryHQ/cherry-studio/issues/10954 + * - v2 Refactor PR : https://github.com/CherryHQ/cherry-studio/pull/10162 + * -------------------------------------------------------------------------- + */ import { loggerService } from '@logger' import store from '@renderer/store' import type { AgentPersistedMessage } from '@renderer/types/agent' diff --git a/src/renderer/src/services/db/DbService.ts b/src/renderer/src/services/db/DbService.ts index cff7cb1a45..64ff945958 100644 --- a/src/renderer/src/services/db/DbService.ts +++ b/src/renderer/src/services/db/DbService.ts @@ -1,3 +1,19 @@ +/** + * @deprecated Scheduled for removal in v2.0.0 + * -------------------------------------------------------------------------- + * ⚠️ NOTICE: V2 DATA&UI REFACTORING (by 0xfullex) + * -------------------------------------------------------------------------- + * STOP: Feature PRs affecting this file are currently BLOCKED. + * Only critical bug fixes are accepted during this migration phase. + * + * This file is being refactored to v2 standards. + * Any non-critical changes will conflict with the ongoing work. + * + * 🔗 Context & Status: + * - Contribution Hold: https://github.com/CherryHQ/cherry-studio/issues/10954 + * - v2 Refactor PR : https://github.com/CherryHQ/cherry-studio/pull/10162 + * -------------------------------------------------------------------------- + */ import { loggerService } from '@logger' import store from '@renderer/store' import type { Message, MessageBlock } from '@renderer/types/newMessage' diff --git a/src/renderer/src/services/db/DexieMessageDataSource.ts b/src/renderer/src/services/db/DexieMessageDataSource.ts index cbc015984a..f8bad4476f 100644 --- a/src/renderer/src/services/db/DexieMessageDataSource.ts +++ b/src/renderer/src/services/db/DexieMessageDataSource.ts @@ -1,3 +1,19 @@ +/** + * @deprecated Scheduled for removal in v2.0.0 + * -------------------------------------------------------------------------- + * ⚠️ NOTICE: V2 DATA&UI REFACTORING (by 0xfullex) + * -------------------------------------------------------------------------- + * STOP: Feature PRs affecting this file are currently BLOCKED. + * Only critical bug fixes are accepted during this migration phase. + * + * This file is being refactored to v2 standards. + * Any non-critical changes will conflict with the ongoing work. + * + * 🔗 Context & Status: + * - Contribution Hold: https://github.com/CherryHQ/cherry-studio/issues/10954 + * - v2 Refactor PR : https://github.com/CherryHQ/cherry-studio/pull/10162 + * -------------------------------------------------------------------------- + */ import { loggerService } from '@logger' import db from '@renderer/databases' import FileManager from '@renderer/services/FileManager' diff --git a/src/renderer/src/services/db/index.ts b/src/renderer/src/services/db/index.ts index 9b681dc6c6..a29eeb6c04 100644 --- a/src/renderer/src/services/db/index.ts +++ b/src/renderer/src/services/db/index.ts @@ -1,3 +1,19 @@ +/** + * @deprecated Scheduled for removal in v2.0.0 + * -------------------------------------------------------------------------- + * ⚠️ NOTICE: V2 DATA&UI REFACTORING (by 0xfullex) + * -------------------------------------------------------------------------- + * STOP: Feature PRs affecting this file are currently BLOCKED. + * Only critical bug fixes are accepted during this migration phase. + * + * This file is being refactored to v2 standards. + * Any non-critical changes will conflict with the ongoing work. + * + * 🔗 Context & Status: + * - Contribution Hold: https://github.com/CherryHQ/cherry-studio/issues/10954 + * - v2 Refactor PR : https://github.com/CherryHQ/cherry-studio/pull/10162 + * -------------------------------------------------------------------------- + */ /** * Unified data access layer for messages * Provides a consistent API for accessing messages from different sources diff --git a/src/renderer/src/store/assistants.ts b/src/renderer/src/store/assistants.ts index 51638be9f6..aaac1810ab 100644 --- a/src/renderer/src/store/assistants.ts +++ b/src/renderer/src/store/assistants.ts @@ -1,3 +1,19 @@ +/** + * @deprecated Scheduled for removal in v2.0.0 + * -------------------------------------------------------------------------- + * ⚠️ NOTICE: V2 DATA&UI REFACTORING (by 0xfullex) + * -------------------------------------------------------------------------- + * STOP: Feature PRs affecting this file are currently BLOCKED. + * Only critical bug fixes are accepted during this migration phase. + * + * This file is being refactored to v2 standards. + * Any non-critical changes will conflict with the ongoing work. + * + * 🔗 Context & Status: + * - Contribution Hold: https://github.com/CherryHQ/cherry-studio/issues/10954 + * - v2 Refactor PR : https://github.com/CherryHQ/cherry-studio/pull/10162 + * -------------------------------------------------------------------------- + */ // @ts-nocheck import type { PayloadAction } from '@reduxjs/toolkit' import { createSelector, createSlice } from '@reduxjs/toolkit' diff --git a/src/renderer/src/store/backup.ts b/src/renderer/src/store/backup.ts index fbb3853a12..d2986b11bf 100644 --- a/src/renderer/src/store/backup.ts +++ b/src/renderer/src/store/backup.ts @@ -1,3 +1,19 @@ +/** + * @deprecated Scheduled for removal in v2.0.0 + * -------------------------------------------------------------------------- + * ⚠️ NOTICE: V2 DATA&UI REFACTORING (by 0xfullex) + * -------------------------------------------------------------------------- + * STOP: Feature PRs affecting this file are currently BLOCKED. + * Only critical bug fixes are accepted during this migration phase. + * + * This file is being refactored to v2 standards. + * Any non-critical changes will conflict with the ongoing work. + * + * 🔗 Context & Status: + * - Contribution Hold: https://github.com/CherryHQ/cherry-studio/issues/10954 + * - v2 Refactor PR : https://github.com/CherryHQ/cherry-studio/pull/10162 + * -------------------------------------------------------------------------- + */ import type { PayloadAction } from '@reduxjs/toolkit' import { createSlice } from '@reduxjs/toolkit' diff --git a/src/renderer/src/store/codeTools.ts b/src/renderer/src/store/codeTools.ts index 44070a76e4..dc3889abb1 100644 --- a/src/renderer/src/store/codeTools.ts +++ b/src/renderer/src/store/codeTools.ts @@ -1,3 +1,19 @@ +/** + * @deprecated Scheduled for removal in v2.0.0 + * -------------------------------------------------------------------------- + * ⚠️ NOTICE: V2 DATA&UI REFACTORING (by 0xfullex) + * -------------------------------------------------------------------------- + * STOP: Feature PRs affecting this file are currently BLOCKED. + * Only critical bug fixes are accepted during this migration phase. + * + * This file is being refactored to v2 standards. + * Any non-critical changes will conflict with the ongoing work. + * + * 🔗 Context & Status: + * - Contribution Hold: https://github.com/CherryHQ/cherry-studio/issues/10954 + * - v2 Refactor PR : https://github.com/CherryHQ/cherry-studio/pull/10162 + * -------------------------------------------------------------------------- + */ import type { PayloadAction } from '@reduxjs/toolkit' import { createSlice } from '@reduxjs/toolkit' import type { Model } from '@renderer/types' diff --git a/src/renderer/src/store/copilot.ts b/src/renderer/src/store/copilot.ts index ab7e50ee84..88f9523e65 100644 --- a/src/renderer/src/store/copilot.ts +++ b/src/renderer/src/store/copilot.ts @@ -1,3 +1,19 @@ +/** + * @deprecated Scheduled for removal in v2.0.0 + * -------------------------------------------------------------------------- + * ⚠️ NOTICE: V2 DATA&UI REFACTORING (by 0xfullex) + * -------------------------------------------------------------------------- + * STOP: Feature PRs affecting this file are currently BLOCKED. + * Only critical bug fixes are accepted during this migration phase. + * + * This file is being refactored to v2 standards. + * Any non-critical changes will conflict with the ongoing work. + * + * 🔗 Context & Status: + * - Contribution Hold: https://github.com/CherryHQ/cherry-studio/issues/10954 + * - v2 Refactor PR : https://github.com/CherryHQ/cherry-studio/pull/10162 + * -------------------------------------------------------------------------- + */ import type { PayloadAction } from '@reduxjs/toolkit' import { createSlice } from '@reduxjs/toolkit' diff --git a/src/renderer/src/store/index.ts b/src/renderer/src/store/index.ts index 8d8c793c21..3d21d12cc7 100644 --- a/src/renderer/src/store/index.ts +++ b/src/renderer/src/store/index.ts @@ -1,3 +1,19 @@ +/** + * @deprecated Scheduled for removal in v2.0.0 + * -------------------------------------------------------------------------- + * ⚠️ NOTICE: V2 DATA&UI REFACTORING (by 0xfullex) + * -------------------------------------------------------------------------- + * STOP: Feature PRs affecting this file are currently BLOCKED. + * Only critical bug fixes are accepted during this migration phase. + * + * This file is being refactored to v2 standards. + * Any non-critical changes will conflict with the ongoing work. + * + * 🔗 Context & Status: + * - Contribution Hold: https://github.com/CherryHQ/cherry-studio/issues/10954 + * - v2 Refactor PR : https://github.com/CherryHQ/cherry-studio/pull/10162 + * -------------------------------------------------------------------------- + */ import { loggerService } from '@logger' import { combineReducers, configureStore } from '@reduxjs/toolkit' import { useDispatch, useSelector, useStore } from 'react-redux' @@ -67,7 +83,7 @@ const persistedReducer = persistReducer( { key: 'cherry-studio', storage, - version: 190, + version: 191, blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs', 'toolPermissions'], migrate }, diff --git a/src/renderer/src/store/inputTools.ts b/src/renderer/src/store/inputTools.ts index aad87dba9f..b9d2506523 100644 --- a/src/renderer/src/store/inputTools.ts +++ b/src/renderer/src/store/inputTools.ts @@ -1,3 +1,19 @@ +/** + * @deprecated Scheduled for removal in v2.0.0 + * -------------------------------------------------------------------------- + * ⚠️ NOTICE: V2 DATA&UI REFACTORING (by 0xfullex) + * -------------------------------------------------------------------------- + * STOP: Feature PRs affecting this file are currently BLOCKED. + * Only critical bug fixes are accepted during this migration phase. + * + * This file is being refactored to v2 standards. + * Any non-critical changes will conflict with the ongoing work. + * + * 🔗 Context & Status: + * - Contribution Hold: https://github.com/CherryHQ/cherry-studio/issues/10954 + * - v2 Refactor PR : https://github.com/CherryHQ/cherry-studio/pull/10162 + * -------------------------------------------------------------------------- + */ import type { PayloadAction } from '@reduxjs/toolkit' import { createSlice } from '@reduxjs/toolkit' import type { InputbarScope } from '@renderer/pages/home/Inputbar/types' diff --git a/src/renderer/src/store/knowledge.ts b/src/renderer/src/store/knowledge.ts index 6280a99e8d..a6a5026952 100644 --- a/src/renderer/src/store/knowledge.ts +++ b/src/renderer/src/store/knowledge.ts @@ -1,3 +1,19 @@ +/** + * @deprecated Scheduled for removal in v2.0.0 + * -------------------------------------------------------------------------- + * ⚠️ NOTICE: V2 DATA&UI REFACTORING (by 0xfullex) + * -------------------------------------------------------------------------- + * STOP: Feature PRs affecting this file are currently BLOCKED. + * Only critical bug fixes are accepted during this migration phase. + * + * This file is being refactored to v2 standards. + * Any non-critical changes will conflict with the ongoing work. + * + * 🔗 Context & Status: + * - Contribution Hold: https://github.com/CherryHQ/cherry-studio/issues/10954 + * - v2 Refactor PR : https://github.com/CherryHQ/cherry-studio/pull/10162 + * -------------------------------------------------------------------------- + */ import { loggerService } from '@logger' import type { PayloadAction } from '@reduxjs/toolkit' import { createSlice } from '@reduxjs/toolkit' diff --git a/src/renderer/src/store/llm.ts b/src/renderer/src/store/llm.ts index 15f256382e..7e53f081bd 100644 --- a/src/renderer/src/store/llm.ts +++ b/src/renderer/src/store/llm.ts @@ -1,3 +1,19 @@ +/** + * @deprecated Scheduled for removal in v2.0.0 + * -------------------------------------------------------------------------- + * ⚠️ NOTICE: V2 DATA&UI REFACTORING (by 0xfullex) + * -------------------------------------------------------------------------- + * STOP: Feature PRs affecting this file are currently BLOCKED. + * Only critical bug fixes are accepted during this migration phase. + * + * This file is being refactored to v2 standards. + * Any non-critical changes will conflict with the ongoing work. + * + * 🔗 Context & Status: + * - Contribution Hold: https://github.com/CherryHQ/cherry-studio/issues/10954 + * - v2 Refactor PR : https://github.com/CherryHQ/cherry-studio/pull/10162 + * -------------------------------------------------------------------------- + */ import type { PayloadAction } from '@reduxjs/toolkit' import { createSlice } from '@reduxjs/toolkit' import { isLocalAi } from '@renderer/config/env' diff --git a/src/renderer/src/store/mcp.ts b/src/renderer/src/store/mcp.ts index 5b8d5bcdcf..3b94248401 100644 --- a/src/renderer/src/store/mcp.ts +++ b/src/renderer/src/store/mcp.ts @@ -1,3 +1,19 @@ +/** + * @deprecated Scheduled for removal in v2.0.0 + * -------------------------------------------------------------------------- + * ⚠️ NOTICE: V2 DATA&UI REFACTORING (by 0xfullex) + * -------------------------------------------------------------------------- + * STOP: Feature PRs affecting this file are currently BLOCKED. + * Only critical bug fixes are accepted during this migration phase. + * + * This file is being refactored to v2 standards. + * Any non-critical changes will conflict with the ongoing work. + * + * 🔗 Context & Status: + * - Contribution Hold: https://github.com/CherryHQ/cherry-studio/issues/10954 + * - v2 Refactor PR : https://github.com/CherryHQ/cherry-studio/pull/10162 + * -------------------------------------------------------------------------- + */ import { loggerService } from '@logger' import { createSlice, nanoid, type PayloadAction } from '@reduxjs/toolkit' import { type BuiltinMCPServer, BuiltinMCPServerNames, type MCPConfig, type MCPServer } from '@renderer/types' diff --git a/src/renderer/src/store/memory.ts b/src/renderer/src/store/memory.ts index e28b291c19..c4976d874a 100644 --- a/src/renderer/src/store/memory.ts +++ b/src/renderer/src/store/memory.ts @@ -1,3 +1,19 @@ +/** + * @deprecated Scheduled for removal in v2.0.0 + * -------------------------------------------------------------------------- + * ⚠️ NOTICE: V2 DATA&UI REFACTORING (by 0xfullex) + * -------------------------------------------------------------------------- + * STOP: Feature PRs affecting this file are currently BLOCKED. + * Only critical bug fixes are accepted during this migration phase. + * + * This file is being refactored to v2 standards. + * Any non-critical changes will conflict with the ongoing work. + * + * 🔗 Context & Status: + * - Contribution Hold: https://github.com/CherryHQ/cherry-studio/issues/10954 + * - v2 Refactor PR : https://github.com/CherryHQ/cherry-studio/pull/10162 + * -------------------------------------------------------------------------- + */ import { createSlice, type PayloadAction } from '@reduxjs/toolkit' import { factExtractionPrompt, updateMemorySystemPrompt } from '@renderer/utils/memory-prompts' import type { MemoryConfig } from '@types' diff --git a/src/renderer/src/store/messageBlock.ts b/src/renderer/src/store/messageBlock.ts index ba0e11be0a..c2719cdb13 100644 --- a/src/renderer/src/store/messageBlock.ts +++ b/src/renderer/src/store/messageBlock.ts @@ -1,3 +1,19 @@ +/** + * @deprecated Scheduled for removal in v2.0.0 + * -------------------------------------------------------------------------- + * ⚠️ NOTICE: V2 DATA&UI REFACTORING (by 0xfullex) + * -------------------------------------------------------------------------- + * STOP: Feature PRs affecting this file are currently BLOCKED. + * Only critical bug fixes are accepted during this migration phase. + * + * This file is being refactored to v2 standards. + * Any non-critical changes will conflict with the ongoing work. + * + * 🔗 Context & Status: + * - Contribution Hold: https://github.com/CherryHQ/cherry-studio/issues/10954 + * - v2 Refactor PR : https://github.com/CherryHQ/cherry-studio/pull/10162 + * -------------------------------------------------------------------------- + */ import type { WebSearchResultBlock } from '@anthropic-ai/sdk/resources' import type OpenAI from '@cherrystudio/openai' import type { GroundingMetadata } from '@google/genai' diff --git a/src/renderer/src/store/migrate.ts b/src/renderer/src/store/migrate.ts index e0d3524f68..9375dc3b75 100644 --- a/src/renderer/src/store/migrate.ts +++ b/src/renderer/src/store/migrate.ts @@ -1,3 +1,19 @@ +/** + * @deprecated Scheduled for removal in v2.0.0 + * -------------------------------------------------------------------------- + * ⚠️ NOTICE: V2 DATA&UI REFACTORING (by 0xfullex) + * -------------------------------------------------------------------------- + * STOP: Feature PRs affecting this file are currently BLOCKED. + * Only critical bug fixes are accepted during this migration phase. + * + * This file is being refactored to v2 standards. + * Any non-critical changes will conflict with the ongoing work. + * + * 🔗 Context & Status: + * - Contribution Hold: https://github.com/CherryHQ/cherry-studio/issues/10954 + * - v2 Refactor PR : https://github.com/CherryHQ/cherry-studio/pull/10162 + * -------------------------------------------------------------------------- + */ import { loggerService } from '@logger' import { nanoid } from '@reduxjs/toolkit' import { @@ -3113,6 +3129,21 @@ const migrateConfig = { logger.error('migrate 190 error', error as Error) return state } + }, + '191': (state: RootState) => { + try { + state.llm.providers.forEach((provider) => { + if (provider.id === 'tokenflux') { + provider.apiHost = 'https://api.tokenflux.ai/openai/v1' + provider.anthropicApiHost = 'https://api.tokenflux.ai/anthropic' + } + }) + logger.info('migrate 191 success') + return state + } catch (error) { + logger.error('migrate 191 error', error as Error) + return state + } } } diff --git a/src/renderer/src/store/minapps.ts b/src/renderer/src/store/minapps.ts index 8ca59a5bd2..ac2a83440b 100644 --- a/src/renderer/src/store/minapps.ts +++ b/src/renderer/src/store/minapps.ts @@ -1,3 +1,19 @@ +/** + * @deprecated Scheduled for removal in v2.0.0 + * -------------------------------------------------------------------------- + * ⚠️ NOTICE: V2 DATA&UI REFACTORING (by 0xfullex) + * -------------------------------------------------------------------------- + * STOP: Feature PRs affecting this file are currently BLOCKED. + * Only critical bug fixes are accepted during this migration phase. + * + * This file is being refactored to v2 standards. + * Any non-critical changes will conflict with the ongoing work. + * + * 🔗 Context & Status: + * - Contribution Hold: https://github.com/CherryHQ/cherry-studio/issues/10954 + * - v2 Refactor PR : https://github.com/CherryHQ/cherry-studio/pull/10162 + * -------------------------------------------------------------------------- + */ import type { PayloadAction } from '@reduxjs/toolkit' import { createSlice } from '@reduxjs/toolkit' import { DEFAULT_MIN_APPS } from '@renderer/config/minapps' diff --git a/src/renderer/src/store/newMessage.ts b/src/renderer/src/store/newMessage.ts index cd8c0dde83..918ae3dc5b 100644 --- a/src/renderer/src/store/newMessage.ts +++ b/src/renderer/src/store/newMessage.ts @@ -1,3 +1,19 @@ +/** + * @deprecated Scheduled for removal in v2.0.0 + * -------------------------------------------------------------------------- + * ⚠️ NOTICE: V2 DATA&UI REFACTORING (by 0xfullex) + * -------------------------------------------------------------------------- + * STOP: Feature PRs affecting this file are currently BLOCKED. + * Only critical bug fixes are accepted during this migration phase. + * + * This file is being refactored to v2 standards. + * Any non-critical changes will conflict with the ongoing work. + * + * 🔗 Context & Status: + * - Contribution Hold: https://github.com/CherryHQ/cherry-studio/issues/10954 + * - v2 Refactor PR : https://github.com/CherryHQ/cherry-studio/pull/10162 + * -------------------------------------------------------------------------- + */ import { loggerService } from '@logger' import type { EntityState, PayloadAction } from '@reduxjs/toolkit' import { createEntityAdapter, createSlice } from '@reduxjs/toolkit' diff --git a/src/renderer/src/store/note.ts b/src/renderer/src/store/note.ts index 25347a8764..d571552831 100644 --- a/src/renderer/src/store/note.ts +++ b/src/renderer/src/store/note.ts @@ -1,3 +1,19 @@ +/** + * @deprecated Scheduled for removal in v2.0.0 + * -------------------------------------------------------------------------- + * ⚠️ NOTICE: V2 DATA&UI REFACTORING (by 0xfullex) + * -------------------------------------------------------------------------- + * STOP: Feature PRs affecting this file are currently BLOCKED. + * Only critical bug fixes are accepted during this migration phase. + * + * This file is being refactored to v2 standards. + * Any non-critical changes will conflict with the ongoing work. + * + * 🔗 Context & Status: + * - Contribution Hold: https://github.com/CherryHQ/cherry-studio/issues/10954 + * - v2 Refactor PR : https://github.com/CherryHQ/cherry-studio/pull/10162 + * -------------------------------------------------------------------------- + */ import type { PayloadAction } from '@reduxjs/toolkit' import { createSlice } from '@reduxjs/toolkit' import type { RootState } from '@renderer/store/index' diff --git a/src/renderer/src/store/nutstore.ts b/src/renderer/src/store/nutstore.ts index d494ec269f..bb9d426d8e 100644 --- a/src/renderer/src/store/nutstore.ts +++ b/src/renderer/src/store/nutstore.ts @@ -1,3 +1,19 @@ +/** + * @deprecated Scheduled for removal in v2.0.0 + * -------------------------------------------------------------------------- + * ⚠️ NOTICE: V2 DATA&UI REFACTORING (by 0xfullex) + * -------------------------------------------------------------------------- + * STOP: Feature PRs affecting this file are currently BLOCKED. + * Only critical bug fixes are accepted during this migration phase. + * + * This file is being refactored to v2 standards. + * Any non-critical changes will conflict with the ongoing work. + * + * 🔗 Context & Status: + * - Contribution Hold: https://github.com/CherryHQ/cherry-studio/issues/10954 + * - v2 Refactor PR : https://github.com/CherryHQ/cherry-studio/pull/10162 + * -------------------------------------------------------------------------- + */ import type { PayloadAction } from '@reduxjs/toolkit' import { createSlice } from '@reduxjs/toolkit' diff --git a/src/renderer/src/store/ocr.ts b/src/renderer/src/store/ocr.ts index 8e997bd6d5..29ee4085b7 100644 --- a/src/renderer/src/store/ocr.ts +++ b/src/renderer/src/store/ocr.ts @@ -1,3 +1,19 @@ +/** + * @deprecated Scheduled for removal in v2.0.0 + * -------------------------------------------------------------------------- + * ⚠️ NOTICE: V2 DATA&UI REFACTORING (by 0xfullex) + * -------------------------------------------------------------------------- + * STOP: Feature PRs affecting this file are currently BLOCKED. + * Only critical bug fixes are accepted during this migration phase. + * + * This file is being refactored to v2 standards. + * Any non-critical changes will conflict with the ongoing work. + * + * 🔗 Context & Status: + * - Contribution Hold: https://github.com/CherryHQ/cherry-studio/issues/10954 + * - v2 Refactor PR : https://github.com/CherryHQ/cherry-studio/pull/10162 + * -------------------------------------------------------------------------- + */ import type { PayloadAction } from '@reduxjs/toolkit' import { createSlice } from '@reduxjs/toolkit' import { BUILTIN_OCR_PROVIDERS, DEFAULT_OCR_PROVIDER } from '@renderer/config/ocr' diff --git a/src/renderer/src/store/paintings.ts b/src/renderer/src/store/paintings.ts index e5fc6f59e2..a7b509f531 100644 --- a/src/renderer/src/store/paintings.ts +++ b/src/renderer/src/store/paintings.ts @@ -1,3 +1,19 @@ +/** + * @deprecated Scheduled for removal in v2.0.0 + * -------------------------------------------------------------------------- + * ⚠️ NOTICE: V2 DATA&UI REFACTORING (by 0xfullex) + * -------------------------------------------------------------------------- + * STOP: Feature PRs affecting this file are currently BLOCKED. + * Only critical bug fixes are accepted during this migration phase. + * + * This file is being refactored to v2 standards. + * Any non-critical changes will conflict with the ongoing work. + * + * 🔗 Context & Status: + * - Contribution Hold: https://github.com/CherryHQ/cherry-studio/issues/10954 + * - v2 Refactor PR : https://github.com/CherryHQ/cherry-studio/pull/10162 + * -------------------------------------------------------------------------- + */ import { loggerService } from '@logger' import type { PayloadAction } from '@reduxjs/toolkit' import { createSlice } from '@reduxjs/toolkit' diff --git a/src/renderer/src/store/preprocess.ts b/src/renderer/src/store/preprocess.ts index 29fc2993b7..8fee31b0ef 100644 --- a/src/renderer/src/store/preprocess.ts +++ b/src/renderer/src/store/preprocess.ts @@ -1,3 +1,19 @@ +/** + * @deprecated Scheduled for removal in v2.0.0 + * -------------------------------------------------------------------------- + * ⚠️ NOTICE: V2 DATA&UI REFACTORING (by 0xfullex) + * -------------------------------------------------------------------------- + * STOP: Feature PRs affecting this file are currently BLOCKED. + * Only critical bug fixes are accepted during this migration phase. + * + * This file is being refactored to v2 standards. + * Any non-critical changes will conflict with the ongoing work. + * + * 🔗 Context & Status: + * - Contribution Hold: https://github.com/CherryHQ/cherry-studio/issues/10954 + * - v2 Refactor PR : https://github.com/CherryHQ/cherry-studio/pull/10162 + * -------------------------------------------------------------------------- + */ import type { PayloadAction } from '@reduxjs/toolkit' import { createSlice } from '@reduxjs/toolkit' import type { PreprocessProvider } from '@renderer/types' diff --git a/src/renderer/src/store/runtime.ts b/src/renderer/src/store/runtime.ts index 2ee7719469..66fd161dcd 100644 --- a/src/renderer/src/store/runtime.ts +++ b/src/renderer/src/store/runtime.ts @@ -1,3 +1,19 @@ +/** + * @deprecated Scheduled for removal in v2.0.0 + * -------------------------------------------------------------------------- + * ⚠️ NOTICE: V2 DATA&UI REFACTORING (by 0xfullex) + * -------------------------------------------------------------------------- + * STOP: Feature PRs affecting this file are currently BLOCKED. + * Only critical bug fixes are accepted during this migration phase. + * + * This file is being refactored to v2 standards. + * Any non-critical changes will conflict with the ongoing work. + * + * 🔗 Context & Status: + * - Contribution Hold: https://github.com/CherryHQ/cherry-studio/issues/10954 + * - v2 Refactor PR : https://github.com/CherryHQ/cherry-studio/pull/10162 + * -------------------------------------------------------------------------- + */ import type { PayloadAction } from '@reduxjs/toolkit' import { createSlice } from '@reduxjs/toolkit' import { AppLogo, UserAvatar } from '@renderer/config/env' diff --git a/src/renderer/src/store/selectionStore.ts b/src/renderer/src/store/selectionStore.ts index fe63ae230e..89f44dd07a 100644 --- a/src/renderer/src/store/selectionStore.ts +++ b/src/renderer/src/store/selectionStore.ts @@ -1,3 +1,19 @@ +/** + * @deprecated Scheduled for removal in v2.0.0 + * -------------------------------------------------------------------------- + * ⚠️ NOTICE: V2 DATA&UI REFACTORING (by 0xfullex) + * -------------------------------------------------------------------------- + * STOP: Feature PRs affecting this file are currently BLOCKED. + * Only critical bug fixes are accepted during this migration phase. + * + * This file is being refactored to v2 standards. + * Any non-critical changes will conflict with the ongoing work. + * + * 🔗 Context & Status: + * - Contribution Hold: https://github.com/CherryHQ/cherry-studio/issues/10954 + * - v2 Refactor PR : https://github.com/CherryHQ/cherry-studio/pull/10162 + * -------------------------------------------------------------------------- + */ import type { PayloadAction } from '@reduxjs/toolkit' import { createSlice } from '@reduxjs/toolkit' import type { ActionItem, FilterMode, SelectionState, TriggerMode } from '@renderer/types/selectionTypes' diff --git a/src/renderer/src/store/settings.ts b/src/renderer/src/store/settings.ts index 572f722746..3ba3cc4da8 100644 --- a/src/renderer/src/store/settings.ts +++ b/src/renderer/src/store/settings.ts @@ -1,3 +1,19 @@ +/** + * @deprecated Scheduled for removal in v2.0.0 + * -------------------------------------------------------------------------- + * ⚠️ NOTICE: V2 DATA&UI REFACTORING (by 0xfullex) + * -------------------------------------------------------------------------- + * STOP: Feature PRs affecting this file are currently BLOCKED. + * Only critical bug fixes are accepted during this migration phase. + * + * This file is being refactored to v2 standards. + * Any non-critical changes will conflict with the ongoing work. + * + * 🔗 Context & Status: + * - Contribution Hold: https://github.com/CherryHQ/cherry-studio/issues/10954 + * - v2 Refactor PR : https://github.com/CherryHQ/cherry-studio/pull/10162 + * -------------------------------------------------------------------------- + */ import type { PayloadAction } from '@reduxjs/toolkit' import { createSlice } from '@reduxjs/toolkit' import { DEFAULT_STREAM_OPTIONS_INCLUDE_USAGE, isMac } from '@renderer/config/constant' diff --git a/src/renderer/src/store/shortcuts.ts b/src/renderer/src/store/shortcuts.ts index 9b4cc1341a..c8fabf8b04 100644 --- a/src/renderer/src/store/shortcuts.ts +++ b/src/renderer/src/store/shortcuts.ts @@ -1,3 +1,19 @@ +/** + * @deprecated Scheduled for removal in v2.0.0 + * -------------------------------------------------------------------------- + * ⚠️ NOTICE: V2 DATA&UI REFACTORING (by 0xfullex) + * -------------------------------------------------------------------------- + * STOP: Feature PRs affecting this file are currently BLOCKED. + * Only critical bug fixes are accepted during this migration phase. + * + * This file is being refactored to v2 standards. + * Any non-critical changes will conflict with the ongoing work. + * + * 🔗 Context & Status: + * - Contribution Hold: https://github.com/CherryHQ/cherry-studio/issues/10954 + * - v2 Refactor PR : https://github.com/CherryHQ/cherry-studio/pull/10162 + * -------------------------------------------------------------------------- + */ import type { PayloadAction } from '@reduxjs/toolkit' import { createSlice } from '@reduxjs/toolkit' import type { Shortcut } from '@renderer/types' diff --git a/src/renderer/src/store/tabs.ts b/src/renderer/src/store/tabs.ts index 87d7342779..c539cf20a0 100644 --- a/src/renderer/src/store/tabs.ts +++ b/src/renderer/src/store/tabs.ts @@ -1,3 +1,19 @@ +/** + * @deprecated Scheduled for removal in v2.0.0 + * -------------------------------------------------------------------------- + * ⚠️ NOTICE: V2 DATA&UI REFACTORING (by 0xfullex) + * -------------------------------------------------------------------------- + * STOP: Feature PRs affecting this file are currently BLOCKED. + * Only critical bug fixes are accepted during this migration phase. + * + * This file is being refactored to v2 standards. + * Any non-critical changes will conflict with the ongoing work. + * + * 🔗 Context & Status: + * - Contribution Hold: https://github.com/CherryHQ/cherry-studio/issues/10954 + * - v2 Refactor PR : https://github.com/CherryHQ/cherry-studio/pull/10162 + * -------------------------------------------------------------------------- + */ import type { PayloadAction } from '@reduxjs/toolkit' import { createSlice } from '@reduxjs/toolkit' diff --git a/src/renderer/src/store/thunk/knowledgeThunk.ts b/src/renderer/src/store/thunk/knowledgeThunk.ts index 97c435d169..b353e0af51 100644 --- a/src/renderer/src/store/thunk/knowledgeThunk.ts +++ b/src/renderer/src/store/thunk/knowledgeThunk.ts @@ -1,3 +1,19 @@ +/** + * @deprecated Scheduled for removal in v2.0.0 + * -------------------------------------------------------------------------- + * ⚠️ NOTICE: V2 DATA&UI REFACTORING (by 0xfullex) + * -------------------------------------------------------------------------- + * STOP: Feature PRs affecting this file are currently BLOCKED. + * Only critical bug fixes are accepted during this migration phase. + * + * This file is being refactored to v2 standards. + * Any non-critical changes will conflict with the ongoing work. + * + * 🔗 Context & Status: + * - Contribution Hold: https://github.com/CherryHQ/cherry-studio/issues/10954 + * - v2 Refactor PR : https://github.com/CherryHQ/cherry-studio/pull/10162 + * -------------------------------------------------------------------------- + */ import { loggerService } from '@logger' import { db } from '@renderer/databases' import { addFiles as addFilesAction, addItem, updateNotes } from '@renderer/store/knowledge' diff --git a/src/renderer/src/store/thunk/messageThunk.ts b/src/renderer/src/store/thunk/messageThunk.ts index 8219fa0cce..45d7fd760a 100644 --- a/src/renderer/src/store/thunk/messageThunk.ts +++ b/src/renderer/src/store/thunk/messageThunk.ts @@ -1,3 +1,19 @@ +/** + * @deprecated Scheduled for removal in v2.0.0 + * -------------------------------------------------------------------------- + * ⚠️ NOTICE: V2 DATA&UI REFACTORING (by 0xfullex) + * -------------------------------------------------------------------------- + * STOP: Feature PRs affecting this file are currently BLOCKED. + * Only critical bug fixes are accepted during this migration phase. + * + * This file is being refactored to v2 standards. + * Any non-critical changes will conflict with the ongoing work. + * + * 🔗 Context & Status: + * - Contribution Hold: https://github.com/CherryHQ/cherry-studio/issues/10954 + * - v2 Refactor PR : https://github.com/CherryHQ/cherry-studio/pull/10162 + * -------------------------------------------------------------------------- + */ import { loggerService } from '@logger' import { AiSdkToChunkAdapter } from '@renderer/aiCore/chunk/AiSdkToChunkAdapter' import { AgentApiClient } from '@renderer/api/agent' diff --git a/src/renderer/src/store/thunk/messageThunk.v2.ts b/src/renderer/src/store/thunk/messageThunk.v2.ts index ec0aed947b..587a9baf68 100644 --- a/src/renderer/src/store/thunk/messageThunk.v2.ts +++ b/src/renderer/src/store/thunk/messageThunk.v2.ts @@ -1,3 +1,19 @@ +/** + * @deprecated Scheduled for removal in v2.0.0 + * -------------------------------------------------------------------------- + * ⚠️ NOTICE: V2 DATA&UI REFACTORING (by 0xfullex) + * -------------------------------------------------------------------------- + * STOP: Feature PRs affecting this file are currently BLOCKED. + * Only critical bug fixes are accepted during this migration phase. + * + * This file is being refactored to v2 standards. + * Any non-critical changes will conflict with the ongoing work. + * + * 🔗 Context & Status: + * - Contribution Hold: https://github.com/CherryHQ/cherry-studio/issues/10954 + * - v2 Refactor PR : https://github.com/CherryHQ/cherry-studio/pull/10162 + * -------------------------------------------------------------------------- + */ /** * V2 implementations of message thunk functions using the unified DbService * These implementations will be gradually rolled out using feature flags diff --git a/src/renderer/src/store/toolPermissions.ts b/src/renderer/src/store/toolPermissions.ts index cd31b16af8..a283956daa 100644 --- a/src/renderer/src/store/toolPermissions.ts +++ b/src/renderer/src/store/toolPermissions.ts @@ -1,3 +1,19 @@ +/** + * @deprecated Scheduled for removal in v2.0.0 + * -------------------------------------------------------------------------- + * ⚠️ NOTICE: V2 DATA&UI REFACTORING (by 0xfullex) + * -------------------------------------------------------------------------- + * STOP: Feature PRs affecting this file are currently BLOCKED. + * Only critical bug fixes are accepted during this migration phase. + * + * This file is being refactored to v2 standards. + * Any non-critical changes will conflict with the ongoing work. + * + * 🔗 Context & Status: + * - Contribution Hold: https://github.com/CherryHQ/cherry-studio/issues/10954 + * - v2 Refactor PR : https://github.com/CherryHQ/cherry-studio/pull/10162 + * -------------------------------------------------------------------------- + */ import type { PermissionUpdate } from '@anthropic-ai/claude-agent-sdk' import type { PayloadAction } from '@reduxjs/toolkit' import { createSlice } from '@reduxjs/toolkit' diff --git a/src/renderer/src/store/translate.ts b/src/renderer/src/store/translate.ts index 0e4c56e731..752a067739 100644 --- a/src/renderer/src/store/translate.ts +++ b/src/renderer/src/store/translate.ts @@ -1,3 +1,19 @@ +/** + * @deprecated Scheduled for removal in v2.0.0 + * -------------------------------------------------------------------------- + * ⚠️ NOTICE: V2 DATA&UI REFACTORING (by 0xfullex) + * -------------------------------------------------------------------------- + * STOP: Feature PRs affecting this file are currently BLOCKED. + * Only critical bug fixes are accepted during this migration phase. + * + * This file is being refactored to v2 standards. + * Any non-critical changes will conflict with the ongoing work. + * + * 🔗 Context & Status: + * - Contribution Hold: https://github.com/CherryHQ/cherry-studio/issues/10954 + * - v2 Refactor PR : https://github.com/CherryHQ/cherry-studio/pull/10162 + * -------------------------------------------------------------------------- + */ import type { PayloadAction } from '@reduxjs/toolkit' import { createSlice } from '@reduxjs/toolkit' diff --git a/src/renderer/src/store/websearch.ts b/src/renderer/src/store/websearch.ts index f166bb1949..a43db4947b 100644 --- a/src/renderer/src/store/websearch.ts +++ b/src/renderer/src/store/websearch.ts @@ -1,3 +1,19 @@ +/** + * @deprecated Scheduled for removal in v2.0.0 + * -------------------------------------------------------------------------- + * ⚠️ NOTICE: V2 DATA&UI REFACTORING (by 0xfullex) + * -------------------------------------------------------------------------- + * STOP: Feature PRs affecting this file are currently BLOCKED. + * Only critical bug fixes are accepted during this migration phase. + * + * This file is being refactored to v2 standards. + * Any non-critical changes will conflict with the ongoing work. + * + * 🔗 Context & Status: + * - Contribution Hold: https://github.com/CherryHQ/cherry-studio/issues/10954 + * - v2 Refactor PR : https://github.com/CherryHQ/cherry-studio/pull/10162 + * -------------------------------------------------------------------------- + */ import type { PayloadAction } from '@reduxjs/toolkit' import { createSlice } from '@reduxjs/toolkit' import { WEB_SEARCH_PROVIDERS } from '@renderer/config/webSearchProviders' diff --git a/src/renderer/src/types/index.ts b/src/renderer/src/types/index.ts index 6de30f3ba2..00a43c9ca4 100644 --- a/src/renderer/src/types/index.ts +++ b/src/renderer/src/types/index.ts @@ -92,6 +92,7 @@ const ThinkModelTypes = [ 'gpt5_2', 'gpt5pro', 'gpt52pro', + 'gpt_oss', 'grok', 'grok4_fast', 'gemini2_flash', diff --git a/yarn.lock b/yarn.lock index 9e93036f05..bf3a81d2b6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -20232,13 +20232,13 @@ __metadata: "ollama-ai-provider-v2@patch:ollama-ai-provider-v2@npm%3A1.5.5#~/.yarn/patches/ollama-ai-provider-v2-npm-1.5.5-8bef249af9.patch": version: 1.5.5 - resolution: "ollama-ai-provider-v2@patch:ollama-ai-provider-v2@npm%3A1.5.5#~/.yarn/patches/ollama-ai-provider-v2-npm-1.5.5-8bef249af9.patch::version=1.5.5&hash=16c016" + resolution: "ollama-ai-provider-v2@patch:ollama-ai-provider-v2@npm%3A1.5.5#~/.yarn/patches/ollama-ai-provider-v2-npm-1.5.5-8bef249af9.patch::version=1.5.5&hash=0aef28" dependencies: "@ai-sdk/provider": "npm:^2.0.0" "@ai-sdk/provider-utils": "npm:^3.0.17" peerDependencies: zod: ^4.0.16 - checksum: 10c0/aa6bd3415d08f7bbd1a3051f45b1cd3a8fa8bb01413e98de45e8888f64e6b12bca6e340453a3e82e4193ca5354397f524c6c0f7b3e9996d70f53c81374c69180 + checksum: 10c0/32ca1f543ee791ac96061a5f6d8899c00644eeb774b3b951ca1e3e3810b60753acacf8229b2c1ba099b25a01732c54e51e0df44d11f4d90ae201f483d41aa149 languageName: node linkType: hard