mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-14 06:07:23 +08:00
Merge remote-tracking branch 'origin/main' into migrate/v6-2
This commit is contained in:
commit
cd7e27e111
@ -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<z.ZodBoolean>;
|
||||
+ think: z.ZodOptional<z.ZodUnion<[z.ZodBoolean, z.ZodEnum<['low', 'medium', 'high']>]>>;
|
||||
+ think: z.ZodOptional<z.ZodUnion<[z.ZodBoolean, z.ZodLiteral<"low">, z.ZodLiteral<"medium">, z.ZodLiteral<"high">]>>;
|
||||
options: z.ZodOptional<z.ZodObject<{
|
||||
num_ctx: z.ZodOptional<z.ZodNumber>;
|
||||
repeat_last_n: z.ZodOptional<z.ZodNumber>;
|
||||
@ -29,7 +29,7 @@ index 8dd9b498050dbecd8dd6b901acf1aa8ca38a49af..ed644349c9d38fe2a66b2fb44214f7c1
|
||||
|
||||
declare const ollamaCompletionProviderOptions: z.ZodObject<{
|
||||
- think: z.ZodOptional<z.ZodBoolean>;
|
||||
+ think: z.ZodOptional<z.ZodUnion<[z.ZodBoolean, z.ZodEnum<['low', 'medium', 'high']>]>>;
|
||||
+ think: z.ZodOptional<z.ZodUnion<[z.ZodBoolean, z.ZodLiteral<"low">, z.ZodLiteral<"medium">, z.ZodLiteral<"high">]>>;
|
||||
user: z.ZodOptional<z.ZodString>;
|
||||
suffix: z.ZodOptional<z.ZodString>;
|
||||
echo: z.ZodOptional<z.ZodBoolean>;
|
||||
@ -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(),
|
||||
|
||||
129
docs/en/references/fuzzy-search.md
Normal file
129
docs/en/references/fuzzy-search.md
Normal file
@ -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
|
||||
129
docs/zh/references/fuzzy-search.md
Normal file
129
docs/zh/references/fuzzy-search.md
Normal file
@ -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()`:主搜索协调
|
||||
@ -134,68 +134,38 @@ artifactBuildCompleted: scripts/artifact-build-completed.js
|
||||
releaseInfo:
|
||||
releaseNotes: |
|
||||
<!--LANG:en-->
|
||||
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
|
||||
|
||||
<!--LANG:zh-CN-->
|
||||
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 地址处理
|
||||
- [文件] 允许更多文件扩展名
|
||||
<!--LANG:END-->
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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<LanguageModel>
|
||||
export async function createModels(configs: ModelConfig[]): Promise<LanguageModel[]>
|
||||
```
|
||||
|
||||
### 4.2 Runtime Layer (运行时层)
|
||||
|
||||
**职责**:运行时执行器和用户面向的API接口
|
||||
|
||||
**核心组件**:
|
||||
|
||||
- `executor.ts`: 运行时执行器类
|
||||
- `plugin-engine.ts`: 插件引擎(原PluginEnabledAiClient)
|
||||
- `index.ts`: 便捷函数和工厂方法
|
||||
|
||||
**设计特点**:
|
||||
|
||||
- 提供三种使用方式:类实例、静态工厂、函数式调用
|
||||
- 自动集成模型创建和插件处理
|
||||
- 完整的类型安全支持
|
||||
- 为 OpenAI Agents SDK 预留扩展接口
|
||||
|
||||
**核心API**:
|
||||
|
||||
```typescript
|
||||
// 运行时执行器
|
||||
export class RuntimeExecutor<T extends ProviderId = ProviderId> {
|
||||
static create<T extends ProviderId>(
|
||||
providerId: T,
|
||||
options: ProviderSettingsMap[T],
|
||||
plugins?: AiPlugin[]
|
||||
): RuntimeExecutor<T>
|
||||
|
||||
async streamText(modelId: string, params: StreamTextParams): Promise<StreamTextResult>
|
||||
async generateText(modelId: string, params: GenerateTextParams): Promise<GenerateTextResult>
|
||||
async streamObject(modelId: string, params: StreamObjectParams): Promise<StreamObjectResult>
|
||||
async generateObject(modelId: string, params: GenerateObjectParams): Promise<GenerateObjectResult>
|
||||
}
|
||||
|
||||
// 便捷函数式API
|
||||
export async function streamText<T extends ProviderId>(
|
||||
providerId: T,
|
||||
options: ProviderSettingsMap[T],
|
||||
modelId: string,
|
||||
params: StreamTextParams,
|
||||
plugins?: AiPlugin[]
|
||||
): Promise<StreamTextResult>
|
||||
```
|
||||
|
||||
### 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<string | null>
|
||||
loadTemplate?: (templateName: string, context: AiRequestContext) => any | null | Promise<any | null>
|
||||
|
||||
// 【Sequential】串行钩子 - 链式执行,支持数据转换
|
||||
transformParams?: (params: any, context: AiRequestContext) => any | Promise<any>
|
||||
transformResult?: (result: any, context: AiRequestContext) => any | Promise<any>
|
||||
|
||||
// 【Parallel】并行钩子 - 不依赖顺序,用于副作用
|
||||
onRequestStart?: (context: AiRequestContext) => void | Promise<void>
|
||||
onRequestEnd?: (context: AiRequestContext, result: any) => void | Promise<void>
|
||||
onError?: (error: Error, context: AiRequestContext) => void | Promise<void>
|
||||
|
||||
// 【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功能预留了完整的架构空间
|
||||
- **良好的维护性**: 职责分离明确,代码易于维护
|
||||
- **广泛的适用性**: 既适合简单调用也适合复杂应用
|
||||
@ -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)
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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<T> {
|
||||
data: T
|
||||
timestamp: number
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -130,16 +130,18 @@ interface DirectoryListOptions {
|
||||
includeDirectories?: boolean
|
||||
maxEntries?: number
|
||||
searchPattern?: string
|
||||
fuzzy?: boolean
|
||||
}
|
||||
|
||||
const DEFAULT_DIRECTORY_LIST_OPTIONS: Required<DirectoryListOptions> = {
|
||||
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<DirectoryListOptions>): Promise<string[]> {
|
||||
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<DirectoryListOptions>, 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<DirectoryListOptions>
|
||||
): Promise<string[]> {
|
||||
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<boolean> => {
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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<Tool[]> {
|
||||
public async listMcpTools(
|
||||
agentType: AgentType,
|
||||
ids?: string[]
|
||||
): Promise<{ tools: Tool[]; legacyIdMap: Map<string, string> }> {
|
||||
const tools: Tool[] = []
|
||||
const legacyIdMap = new Map<string, string>()
|
||||
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__<serverId>__<toolName>" (double underscore separators, server ID based)
|
||||
* - "mcp_<serverId>_<toolName>" (single underscore separators)
|
||||
* Current format: "mcp__<serverName>__<toolName>" (double underscore separators).
|
||||
*
|
||||
* This keeps persisted data compatible without requiring a database migration.
|
||||
*/
|
||||
protected normalizeAllowedTools(
|
||||
allowedTools: string[] | undefined,
|
||||
tools: Tool[],
|
||||
legacyIdMap?: Map<string, string>
|
||||
): string[] | undefined {
|
||||
if (!allowedTools || allowedTools.length === 0) {
|
||||
return allowedTools
|
||||
}
|
||||
|
||||
const resolvedLegacyIdMap = new Map<string, string>()
|
||||
|
||||
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<SlashCommand[]> {
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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
|
||||
*/
|
||||
|
||||
@ -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 }
|
||||
|
||||
@ -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 }
|
||||
}
|
||||
|
||||
|
||||
91
src/main/services/agents/tests/BaseService.test.ts
Normal file
91
src/main/services/agents/tests/BaseService.test.ts
Normal file
@ -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, string>
|
||||
): 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<string, string>([
|
||||
['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<string, string>([['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)
|
||||
})
|
||||
})
|
||||
@ -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/)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -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
|
||||
|
||||
19
src/main/utils/system.ts
Normal file
19
src/main/utils/system.ts
Normal file
@ -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'
|
||||
}
|
||||
}
|
||||
@ -340,6 +340,7 @@ const api = {
|
||||
ipcRenderer.invoke(IpcChannel.VertexAI_ClearAuthCache, projectId, clientEmail)
|
||||
},
|
||||
ovms: {
|
||||
isSupported: (): Promise<boolean> => 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),
|
||||
|
||||
@ -66,6 +66,11 @@ export class ZhipuAPIClient extends OpenAIAPIClient {
|
||||
|
||||
public async listModels(): Promise<OpenAI.Models.Model[]> {
|
||||
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',
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
BIN
src/renderer/src/assets/images/apps/aistudio.png
Normal file
BIN
src/renderer/src/assets/images/apps/aistudio.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.2 KiB |
@ -1,27 +0,0 @@
|
||||
<svg width="256" height="256" viewBox="0 0 256 256" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="256" height="256" rx="32" fill="#0057CE"/>
|
||||
<mask id="path-2-inside-1_4113_89308" fill="white">
|
||||
<path d="M169.6 131.626C173.075 129.641 176.32 128.241 180.1 126.943C183.74 125.695 187.444 124.664 191.186 123.735C194.915 122.806 198.682 122.017 202.449 121.228C206.216 120.439 209.958 119.675 213.598 118.314C231.429 111.619 242.221 93.6357 239.612 74.9396C237.003 56.2435 221.692 41.8237 202.691 40.1564C194.062 39.4055 185.726 41.4164 178.013 44.9418C170.326 48.4545 163.288 53.4435 157.166 59.158C144.795 70.676 135.657 85.4649 130.083 101.208C124.47 117.054 122.37 134.095 123.694 150.806C124.356 159.129 125.883 167.504 128.326 175.509C130.719 183.362 134.181 191.469 138.839 198.342C136.828 185.475 138.559 172.175 143.917 160.262C149.262 148.375 158.121 138.193 169.6 131.626Z"/>
|
||||
</mask>
|
||||
<path d="M169.6 131.626C173.075 129.641 176.32 128.241 180.1 126.943C183.74 125.695 187.444 124.664 191.186 123.735C194.915 122.806 198.682 122.017 202.449 121.228C206.216 120.439 209.958 119.675 213.598 118.314C231.429 111.619 242.221 93.6357 239.612 74.9396C237.003 56.2435 221.692 41.8237 202.691 40.1564C194.062 39.4055 185.726 41.4164 178.013 44.9418C170.326 48.4545 163.288 53.4435 157.166 59.158C144.795 70.676 135.657 85.4649 130.083 101.208C124.47 117.054 122.37 134.095 123.694 150.806C124.356 159.129 125.883 167.504 128.326 175.509C130.719 183.362 134.181 191.469 138.839 198.342C136.828 185.475 138.559 172.175 143.917 160.262C149.262 148.375 158.121 138.193 169.6 131.626Z" fill="white" stroke="white" stroke-width="32" mask="url(#path-2-inside-1_4113_89308)"/>
|
||||
<path d="M162.246 150.4C161.915 153.913 163.073 157.464 165.542 160.06C168.011 162.657 171.499 164.031 174.668 165.253C178.13 166.577 181.12 167.658 184.353 169.529C187.433 171.311 190.157 173.526 192.435 176.262C201.802 187.449 200.937 203.867 190.462 214.049C179.988 224.23 163.379 224.778 152.243 215.321C149.404 212.903 146.884 209.798 144.81 206.756C141.654 186.52 147.775 165.317 162.246 150.4Z" fill="white"/>
|
||||
<mask id="path-4-outside-2_4113_89308" maskUnits="userSpaceOnUse" x="136" y="138.4" width="71" height="92" fill="black">
|
||||
<rect fill="white" x="136" y="138.4" width="71" height="92"/>
|
||||
<path d="M162.246 150.4C165.542 153.666 163.073 157.464 165.542 160.06C168.011 162.657 171.499 164.031 174.668 165.253C178.13 166.577 181.12 167.658 184.353 169.529C187.433 171.311 190.157 173.526 192.435 176.262C201.802 187.449 200.937 203.867 190.462 214.049C179.988 224.23 163.379 224.778 152.243 215.321C149.404 212.903 146.884 209.798 144.81 206.756C141.654 186.52 147.775 165.317 162.246 150.4Z"/>
|
||||
</mask>
|
||||
<path d="M162.246 150.4C165.542 153.666 163.073 157.464 165.542 160.06C168.011 162.657 171.499 164.031 174.668 165.253C178.13 166.577 181.12 167.658 184.353 169.529C187.433 171.311 190.157 173.526 192.435 176.262C201.802 187.449 200.937 203.867 190.462 214.049C179.988 224.23 163.379 224.778 152.243 215.321C149.404 212.903 146.884 209.798 144.81 206.756C141.654 186.52 147.775 165.317 162.246 150.4Z" stroke="#0057CE" stroke-width="16" mask="url(#path-4-outside-2_4113_89308)"/>
|
||||
<mask id="path-5-inside-3_4113_89308" fill="white">
|
||||
<path d="M50.4113 61.9063C63.3547 61.8935 75.9164 69.008 85.0163 76.9879C94.6761 85.4641 102.16 96.2567 107.085 107.991C112.036 119.789 114.416 132.542 114.327 145.282C114.238 157.665 111.769 171.079 106.296 182.394C105.774 167.821 100.123 153.885 90.3107 143.003C88.5926 141.107 86.7981 139.389 84.6599 137.938C82.5218 136.487 80.2691 135.418 77.8382 134.565C73.1164 132.911 67.7838 132.134 62.8711 131.6C57.8057 131.04 52.7149 130.709 47.6622 129.971C42.4695 129.207 37.8114 128.087 33.1787 125.427C19.688 117.715 13.1463 102.009 17.1808 87.1441C21.2153 72.2661 34.846 61.919 50.4113 61.9063Z"/>
|
||||
</mask>
|
||||
<path d="M50.4113 61.9063C63.3547 61.8935 75.9164 69.008 85.0163 76.9879C94.6761 85.4641 102.16 96.2567 107.085 107.991C112.036 119.789 114.416 132.542 114.327 145.282C114.238 157.665 111.769 171.079 106.296 182.394C105.774 167.821 100.123 153.885 90.3107 143.003C88.5926 141.107 86.7981 139.389 84.6599 137.938C82.5218 136.487 80.2691 135.418 77.8382 134.565C73.1164 132.911 67.7838 132.134 62.8711 131.6C57.8057 131.04 52.7149 130.709 47.6622 129.971C42.4695 129.207 37.8114 128.087 33.1787 125.427C19.688 117.715 13.1463 102.009 17.1808 87.1441C21.2153 72.2661 34.846 61.919 50.4113 61.9063Z" fill="white" stroke="white" stroke-width="32" mask="url(#path-5-inside-3_4113_89308)"/>
|
||||
<mask id="path-6-inside-4_4113_89308" fill="white">
|
||||
<path d="M82.5802 149.38C81.3584 148.03 80.0857 146.745 78.673 145.6C80.4294 148.578 80.6075 151.95 79.8694 155.196C79.1312 158.429 77.5021 161.419 75.4403 163.99C73.3149 166.625 70.8204 168.725 68.1095 170.71C65.7423 172.441 62.2932 174.656 60.1551 176.73C53.8679 182.839 52.5824 192.384 57.0369 199.893C61.4914 207.415 70.5277 210.979 78.9912 208.535C83.662 207.186 87.6202 204.144 90.7638 200.67C93.9455 197.157 96.5291 192.983 98.5655 188.757C98.0437 174.185 92.3928 160.261 82.5802 149.38Z"/>
|
||||
</mask>
|
||||
<path d="M82.5802 149.38C81.3584 148.03 80.0857 146.745 78.673 145.6C80.4294 148.578 80.6075 151.95 79.8694 155.196C79.1312 158.429 77.5021 161.419 75.4403 163.99C73.3149 166.625 70.8204 168.725 68.1095 170.71C65.7423 172.441 62.2932 174.656 60.1551 176.73C53.8679 182.839 52.5824 192.384 57.0369 199.893C61.4914 207.415 70.5277 210.979 78.9912 208.535C83.662 207.186 87.6202 204.144 90.7638 200.67C93.9455 197.157 96.5291 192.983 98.5655 188.757C98.0437 174.185 92.3928 160.261 82.5802 149.38Z" stroke="white" stroke-width="24" mask="url(#path-6-inside-4_4113_89308)"/>
|
||||
<mask id="path-7-outside-5_4113_89308" maskUnits="userSpaceOnUse" x="45.3994" y="138.6" width="62" height="79" fill="black">
|
||||
<rect fill="white" x="45.3994" y="138.6" width="62" height="79"/>
|
||||
<path d="M82.5802 149.38C81.3584 148.03 80.0857 146.745 78.673 145.6C80.4294 148.578 80.6075 151.95 79.8694 155.196C79.1312 158.429 77.5021 161.419 75.4403 163.99C73.3149 166.625 70.8204 168.725 68.1095 170.71C65.7423 172.441 62.2932 174.656 60.1551 176.73C53.8679 182.839 52.5824 192.384 57.0369 199.893C61.4914 207.415 70.5277 210.979 78.9912 208.535C83.662 207.186 87.6202 204.144 90.7638 200.67C93.9455 197.157 96.5291 192.983 98.5655 188.757C98.0437 174.185 92.3928 160.261 82.5802 149.38Z"/>
|
||||
</mask>
|
||||
<path d="M82.5802 149.38C81.3584 148.03 80.0857 146.745 78.673 145.6C80.4294 148.578 80.6075 151.95 79.8694 155.196C79.1312 158.429 77.5021 161.419 75.4403 163.99C73.3149 166.625 70.8204 168.725 68.1095 170.71C65.7423 172.441 62.2932 174.656 60.1551 176.73C53.8679 182.839 52.5824 192.384 57.0369 199.893C61.4914 207.415 70.5277 210.979 78.9912 208.535C83.662 207.186 87.6202 204.144 90.7638 200.67C93.9455 197.157 96.5291 192.983 98.5655 188.757C98.0437 174.185 92.3928 160.261 82.5802 149.38Z" fill="white"/>
|
||||
<path d="M82.5802 149.38C81.3584 148.03 80.0857 146.745 78.673 145.6C80.4294 148.578 80.6075 151.95 79.8694 155.196C79.1312 158.429 77.5021 161.419 75.4403 163.99C73.3149 166.625 70.8204 168.725 68.1095 170.71C65.7423 172.441 62.2932 174.656 60.1551 176.73C53.8679 182.839 52.5824 192.384 57.0369 199.893C61.4914 207.415 70.5277 210.979 78.9912 208.535C83.662 207.186 87.6202 204.144 90.7638 200.67C93.9455 197.157 96.5291 192.983 98.5655 188.757C98.0437 174.185 92.3928 160.261 82.5802 149.38Z" stroke="#0057CE" stroke-width="16" mask="url(#path-7-outside-5_4113_89308)"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 7.3 KiB |
@ -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'
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -617,6 +617,24 @@ export const SYSTEM_MODELS: Record<SystemProviderId | 'defaultModel', Model[]> =
|
||||
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',
|
||||
|
||||
@ -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)) {
|
||||
|
||||
@ -200,7 +200,8 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
|
||||
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<SystemProviderId, ProviderUrls> = {
|
||||
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'
|
||||
}
|
||||
},
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 || '.'
|
||||
})
|
||||
|
||||
|
||||
@ -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<Props> = ({ 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;
|
||||
|
||||
|
||||
@ -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<boolean> => {
|
||||
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<Record<string, string>>({})
|
||||
const listRef = useRef<DraggableVirtualListRef>(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
|
||||
}
|
||||
|
||||
|
||||
@ -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]
|
||||
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
|
||||
@ -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
|
||||
},
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -92,6 +92,7 @@ const ThinkModelTypes = [
|
||||
'gpt5_2',
|
||||
'gpt5pro',
|
||||
'gpt52pro',
|
||||
'gpt_oss',
|
||||
'grok',
|
||||
'grok4_fast',
|
||||
'gemini2_flash',
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user