From ee95fad7e5cbe27d54259d59baf380d4a3553ec5 Mon Sep 17 00:00:00 2001 From: liyuyun-lyy Date: Tue, 16 Sep 2025 21:24:24 +0800 Subject: [PATCH 01/16] feat: add support for iflow cli (#10198) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 本x --- packages/shared/config/constant.ts | 3 ++- src/main/services/CodeToolsService.ts | 4 ++++ src/renderer/src/pages/code/index.ts | 12 ++++++++++-- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/shared/config/constant.ts b/packages/shared/config/constant.ts index 18246cd1e7..3dc2a45a6a 100644 --- a/packages/shared/config/constant.ts +++ b/packages/shared/config/constant.ts @@ -216,5 +216,6 @@ export enum codeTools { qwenCode = 'qwen-code', claudeCode = 'claude-code', geminiCli = 'gemini-cli', - openaiCodex = 'openai-codex' + openaiCodex = 'openai-codex', + iFlowCli = 'iflow-cli' } diff --git a/src/main/services/CodeToolsService.ts b/src/main/services/CodeToolsService.ts index 6e3bb19022..1372bf1f88 100644 --- a/src/main/services/CodeToolsService.ts +++ b/src/main/services/CodeToolsService.ts @@ -51,6 +51,8 @@ class CodeToolsService { return '@openai/codex' case codeTools.qwenCode: return '@qwen-code/qwen-code' + case codeTools.iFlowCli: + return '@iflow-ai/iflow-cli' default: throw new Error(`Unsupported CLI tool: ${cliTool}`) } @@ -66,6 +68,8 @@ class CodeToolsService { return 'codex' case codeTools.qwenCode: return 'qwen' + case codeTools.iFlowCli: + return 'iflow' default: throw new Error(`Unsupported CLI tool: ${cliTool}`) } diff --git a/src/renderer/src/pages/code/index.ts b/src/renderer/src/pages/code/index.ts index 2c36da3ec6..f286704d39 100644 --- a/src/renderer/src/pages/code/index.ts +++ b/src/renderer/src/pages/code/index.ts @@ -19,7 +19,8 @@ export const CLI_TOOLS = [ { value: codeTools.claudeCode, label: 'Claude Code' }, { value: codeTools.qwenCode, label: 'Qwen Code' }, { value: codeTools.geminiCli, label: 'Gemini CLI' }, - { value: codeTools.openaiCodex, label: 'OpenAI Codex' } + { value: codeTools.openaiCodex, label: 'OpenAI Codex' }, + { value: codeTools.iFlowCli, label: 'iFlow CLI' } ] export const GEMINI_SUPPORTED_PROVIDERS = ['aihubmix', 'dmxapi', 'new-api'] @@ -35,7 +36,8 @@ export const CLI_TOOL_PROVIDER_MAP: Record Pr providers.filter((p) => p.type === 'gemini' || GEMINI_SUPPORTED_PROVIDERS.includes(p.id)), [codeTools.qwenCode]: (providers) => providers.filter((p) => p.type.includes('openai')), [codeTools.openaiCodex]: (providers) => - providers.filter((p) => p.id === 'openai' || OPENAI_CODEX_SUPPORTED_PROVIDERS.includes(p.id)) + providers.filter((p) => p.id === 'openai' || OPENAI_CODEX_SUPPORTED_PROVIDERS.includes(p.id)), + [codeTools.iFlowCli]: (providers) => providers.filter((p) => p.type.includes('openai')) } export const getCodeToolsApiBaseUrl = (model: Model, type: EndpointType) => { @@ -144,6 +146,12 @@ export const generateToolEnvironment = ({ env.OPENAI_MODEL = model.id env.OPENAI_MODEL_PROVIDER = modelProvider.id break + + case codeTools.iFlowCli: + env.IFLOW_API_KEY = apiKey + env.IFLOW_BASE_URL = baseUrl + env.IFLOW_MODEL_NAME = model.id + break } return env From a9093b1deaf83074cac21d25400fc8ba97ca2b87 Mon Sep 17 00:00:00 2001 From: Phantom <59059173+EurFelux@users.noreply.github.com> Date: Tue, 16 Sep 2025 22:31:38 +0800 Subject: [PATCH 02/16] chore: update biome format command to ignore unmatched files (#10207) Add --no-errors-on-unmatched flag to biome format commands in lint-staged configuration to prevent errors when no matching files are found --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index b3da1832bf..9e42453df2 100644 --- a/package.json +++ b/package.json @@ -374,11 +374,11 @@ "packageManager": "yarn@4.9.1", "lint-staged": { "*.{js,jsx,ts,tsx,cjs,mjs,cts,mts}": [ - "biome format --write", + "biome format --write --no-errors-on-unmatched", "eslint --fix" ], "*.{json,yml,yaml,css,html}": [ - "biome format --write" + "biome format --write --no-errors-on-unmatched" ] } } From cc860e48b10d3ff398374c86dab82873f8836544 Mon Sep 17 00:00:00 2001 From: kangfenmao Date: Tue, 16 Sep 2025 22:45:10 +0800 Subject: [PATCH 03/16] fix: update navbar position handling in useAppInit hook - Added isLeftNavbar to the useNavbarPosition hook for improved layout management. - Adjusted background style logic to use isLeftNavbar instead of isTopNavbar for better compatibility with left-aligned navigation. - Simplified condition for transparent window styling on macOS. --- src/renderer/src/hooks/useAppInit.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/renderer/src/hooks/useAppInit.ts b/src/renderer/src/hooks/useAppInit.ts index 571e5877a8..ca61511696 100644 --- a/src/renderer/src/hooks/useAppInit.ts +++ b/src/renderer/src/hooks/useAppInit.ts @@ -38,7 +38,7 @@ export function useAppInit() { customCss, enableDataCollection } = useSettings() - const { isTopNavbar } = useNavbarPosition() + const { isTopNavbar, isLeftNavbar } = useNavbarPosition() const { minappShow } = useRuntime() const { setDefaultModel, setQuickModel, setTranslateModel } = useDefaultModel() const avatar = useLiveQuery(() => db.settings.get('image://avatar')) @@ -102,16 +102,15 @@ export function useAppInit() { }, [language]) useEffect(() => { - const transparentWindow = windowStyle === 'transparent' && isMac && !minappShow + const isMacTransparentWindow = windowStyle === 'transparent' && isMac - if (minappShow && isTopNavbar) { - window.root.style.background = - windowStyle === 'transparent' && isMac ? 'var(--color-background)' : 'var(--navbar-background)' + if (minappShow && isLeftNavbar) { + window.root.style.background = isMacTransparentWindow ? 'var(--color-background)' : 'var(--navbar-background)' return } - window.root.style.background = transparentWindow ? 'var(--navbar-background-mac)' : 'var(--navbar-background)' - }, [windowStyle, minappShow, theme, isTopNavbar]) + window.root.style.background = isMacTransparentWindow ? 'var(--navbar-background-mac)' : 'var(--navbar-background)' + }, [windowStyle, minappShow, theme]) useEffect(() => { if (isLocalAi) { From 7319fc5ef48d00c512df42dc12558c930e04afd7 Mon Sep 17 00:00:00 2001 From: kangfenmao Date: Tue, 16 Sep 2025 22:33:10 +0800 Subject: [PATCH 04/16] chore: bump version to v1.6.0-rc.1 --- electron-builder.yml | 31 ++++++++++++---------------- package.json | 2 +- src/renderer/src/hooks/useAppInit.ts | 4 ++-- 3 files changed, 16 insertions(+), 21 deletions(-) diff --git a/electron-builder.yml b/electron-builder.yml index 80722308e4..0660319150 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -126,24 +126,19 @@ artifactBuildCompleted: scripts/artifact-build-completed.js releaseInfo: releaseNotes: | ✨ 新功能: - - 重构知识库模块,提升文档处理能力和搜索性能 - - 新增 PaddleOCR 支持,增强文档识别能力 - - 支持自定义窗口控制按钮样式 - - 新增 AI SDK 包,扩展 AI 能力集成 - - 支持标签页拖拽重排序功能 - - 增强笔记编辑器的同步和日志功能 + - 集成 Perplexity SDK 和 Anthropic OAuth + - 支持 API 服务器模式,提供外部调用接口 + - 新增字体自定义设置功能 + - 笔记支持文件夹批量上传 + - 集成 HeroUI 和 Tailwind CSS 提升界面体验 - 🔧 性能优化: - - 优化 MCP 服务的日志记录和错误处理 - - 改进 WebView 服务的 User-Agent 处理 - - 优化迷你应用的标题栏样式和状态栏适配 - - 重构依赖管理,清理和优化 package.json + 🚀 性能优化: + - 优化大文件上传,支持 OpenAI 标准文件服务 + - 重构 MCP 服务,改进错误处理和状态管理 🐛 问题修复: - - 修复输入栏无限状态更新循环问题 - - 修复窗口控制提示框的鼠标悬停延迟 - - 修复翻译输入框粘贴多内容源的处理 - - 修复导航服务初始化时序问题 - - 修复 MCP 通过 JSON 添加时的参数转换 - - 修复模型作用域服务器同步时的 URL 格式 - - 标准化工具提示图标样式 + - 修复 WebSearch RAG 并发问题 + - 修复翻译页面长文本渲染布局问题 + - 修复笔记拖拽排序和无限循环问题 + - 修复 macOS CodeTool 工作目录错误 + - 修复多个 UI 组件的响应式设计问题 diff --git a/package.json b/package.json index 9e42453df2..16a9b58a87 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "CherryStudio", - "version": "1.6.0-beta.7", + "version": "1.6.0-rc.1", "private": true, "description": "A powerful AI assistant for producer.", "main": "./out/main/index.js", diff --git a/src/renderer/src/hooks/useAppInit.ts b/src/renderer/src/hooks/useAppInit.ts index ca61511696..e9e3b55706 100644 --- a/src/renderer/src/hooks/useAppInit.ts +++ b/src/renderer/src/hooks/useAppInit.ts @@ -38,7 +38,7 @@ export function useAppInit() { customCss, enableDataCollection } = useSettings() - const { isTopNavbar, isLeftNavbar } = useNavbarPosition() + const { isLeftNavbar } = useNavbarPosition() const { minappShow } = useRuntime() const { setDefaultModel, setQuickModel, setTranslateModel } = useDefaultModel() const avatar = useLiveQuery(() => db.settings.get('image://avatar')) @@ -110,7 +110,7 @@ export function useAppInit() { } window.root.style.background = isMacTransparentWindow ? 'var(--navbar-background-mac)' : 'var(--navbar-background)' - }, [windowStyle, minappShow, theme]) + }, [windowStyle, minappShow, theme, isLeftNavbar]) useEffect(() => { if (isLocalAi) { From 4f91a321a06f530a334de40be7c0a00677477558 Mon Sep 17 00:00:00 2001 From: SuYao Date: Wed, 17 Sep 2025 14:07:45 +0800 Subject: [PATCH 05/16] chore: update dependencies and VSCode settings (#10206) * chore: update dependencies and VSCode settings * chore * chore: ts --- .vscode/extensions.json | 3 +-- .vscode/settings.json | 3 +-- package.json | 3 +-- yarn.lock | 18 +++++------------- 4 files changed, 8 insertions(+), 19 deletions(-) diff --git a/.vscode/extensions.json b/.vscode/extensions.json index a08379caed..79046aa441 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -6,7 +6,6 @@ "bradlc.vscode-tailwindcss", "vitest.explorer", "oxc.oxc-vscode", - "biomejs.biome", - "typescriptteam.native-preview" + "biomejs.biome" ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 2d62fde832..141179f38c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -47,6 +47,5 @@ "search.exclude": { "**/dist/**": true, ".yarn/releases/**": true - }, - "typescript.experimental.useTsgo": true + } } diff --git a/package.json b/package.json index 16a9b58a87..bcbcbd10a5 100644 --- a/package.json +++ b/package.json @@ -237,7 +237,6 @@ "diff": "^8.0.2", "docx": "^9.0.2", "dompurify": "^3.2.6", - "dotenv": "^17.2.2", "dotenv-cli": "^7.4.2", "electron": "37.4.0", "electron-builder": "26.0.15", @@ -333,7 +332,7 @@ "tsx": "^4.20.3", "turndown-plugin-gfm": "^1.0.2", "tw-animate-css": "^1.3.8", - "typescript": "^5.6.2", + "typescript": "^5.8.2", "undici": "6.21.2", "unified": "^11.0.5", "uuid": "^10.0.0", diff --git a/yarn.lock b/yarn.lock index 630c46c05c..1d414c0861 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13328,7 +13328,6 @@ __metadata: diff: "npm:^8.0.2" docx: "npm:^9.0.2" dompurify: "npm:^3.2.6" - dotenv: "npm:^17.2.2" dotenv-cli: "npm:^7.4.2" electron: "npm:37.4.0" electron-builder: "npm:26.0.15" @@ -13437,7 +13436,7 @@ __metadata: turndown: "npm:7.2.0" turndown-plugin-gfm: "npm:^1.0.2" tw-animate-css: "npm:^1.3.8" - typescript: "npm:^5.6.2" + typescript: "npm:^5.8.2" undici: "npm:6.21.2" unified: "npm:^11.0.5" uuid: "npm:^10.0.0" @@ -16631,13 +16630,6 @@ __metadata: languageName: node linkType: hard -"dotenv@npm:^17.2.2": - version: 17.2.2 - resolution: "dotenv@npm:17.2.2" - checksum: 10c0/be66513504590aff6eccb14167625aed9bd42ce80547f4fe5d195860211971a7060949b57108dfaeaf90658f79e40edccd3f233f0a978bff507b5b1565ae162b - languageName: node - linkType: hard - "dts-resolver@npm:^2.1.1": version: 2.1.1 resolution: "dts-resolver@npm:2.1.1" @@ -27775,7 +27767,7 @@ __metadata: languageName: node linkType: hard -"typescript@npm:^5.0.0": +"typescript@npm:^5.0.0, typescript@npm:^5.8.2": version: 5.9.2 resolution: "typescript@npm:5.9.2" bin: @@ -27785,7 +27777,7 @@ __metadata: languageName: node linkType: hard -"typescript@npm:^5.4.3, typescript@npm:^5.6.2": +"typescript@npm:^5.4.3": version: 5.8.3 resolution: "typescript@npm:5.8.3" bin: @@ -27795,7 +27787,7 @@ __metadata: languageName: node linkType: hard -"typescript@patch:typescript@npm%3A^5.0.0#optional!builtin": +"typescript@patch:typescript@npm%3A^5.0.0#optional!builtin, typescript@patch:typescript@npm%3A^5.8.2#optional!builtin": version: 5.9.2 resolution: "typescript@patch:typescript@npm%3A5.9.2#optional!builtin::version=5.9.2&hash=5786d5" bin: @@ -27805,7 +27797,7 @@ __metadata: languageName: node linkType: hard -"typescript@patch:typescript@npm%3A^5.4.3#optional!builtin, typescript@patch:typescript@npm%3A^5.6.2#optional!builtin": +"typescript@patch:typescript@npm%3A^5.4.3#optional!builtin": version: 5.8.3 resolution: "typescript@patch:typescript@npm%3A5.8.3#optional!builtin::version=5.8.3&hash=5786d5" bin: From 1481149e51101fecd4409f658da766ab96ec28a5 Mon Sep 17 00:00:00 2001 From: one Date: Wed, 17 Sep 2025 14:18:48 +0800 Subject: [PATCH 06/16] fix: wrap MessageEditor with QuickPanelProvider (#10223) * fix: wrap MessageEditor with QuickPanelProvider * style: revert formatting --- src/renderer/src/pages/home/Chat.tsx | 32 ++++++++++++++-------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/renderer/src/pages/home/Chat.tsx b/src/renderer/src/pages/home/Chat.tsx index 4ea6bcdd57..d6d7f08d90 100644 --- a/src/renderer/src/pages/home/Chat.tsx +++ b/src/renderer/src/pages/home/Chat.tsx @@ -155,23 +155,23 @@ const Chat: FC = (props) => { flex={1} justify="space-between" style={{ maxWidth: chatMaxWidth, height: mainHeight }}> - - } - filter={contentSearchFilter} - includeUser={filterIncludeUser} - onIncludeUserChange={userOutlinedItemClickHandler} - /> - {messageNavigation === 'buttons' && } + + } + filter={contentSearchFilter} + includeUser={filterIncludeUser} + onIncludeUserChange={userOutlinedItemClickHandler} + /> + {messageNavigation === 'buttons' && } {isMultiSelectMode && } From a8bf55abc2b36f620d3c42280cb342007f2554ee Mon Sep 17 00:00:00 2001 From: SuYao Date: Wed, 17 Sep 2025 15:10:30 +0800 Subject: [PATCH 07/16] =?UTF-8?q?refactor:=20=E5=B0=86=E7=BD=91=E7=BB=9C?= =?UTF-8?q?=E6=90=9C=E7=B4=A2=E5=8F=82=E6=95=B0=E7=94=A8=E4=BA=8E=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E5=86=85=E7=BD=AE=E6=90=9C=E7=B4=A2=20(#10213)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Remove local provider option files and use external packages Replace local implementation of XAI and OpenRouter provider options with external packages (@ai-sdk/xai and @openrouter/ai-sdk-provider). Update web search plugin to support additional providers including OpenAI Chat and OpenRouter, with improved configuration mapping. * Bump @cherrystudio/ai-core to v1.0.0-alpha.17 fix i18n * fix(i18n): Auto update translations for PR #10213 --------- Co-authored-by: GitHub Action --- package.json | 2 +- packages/aiCore/package.json | 2 +- packages/aiCore/src/core/options/factory.ts | 2 +- .../aiCore/src/core/options/openrouter.ts | 38 -------- packages/aiCore/src/core/options/types.ts | 5 +- packages/aiCore/src/core/options/xai.ts | 86 ------------------ .../aiCore/src/core/plugins/built-in/index.ts | 8 +- .../built-in/webSearchPlugin/helper.ts | 20 ++++- .../plugins/built-in/webSearchPlugin/index.ts | 17 +++- .../built-in/webSearchPlugin/openrouter.ts | 26 ++++++ packages/aiCore/src/core/providers/schemas.ts | 7 +- .../middleware/AiSdkMiddlewareBuilder.ts | 3 + .../src/aiCore/plugins/PluginBuilder.ts | 5 +- .../aiCore/prepareParams/parameterBuilder.ts | 27 +++++- src/renderer/src/aiCore/utils/websearch.ts | 88 +++++++++++++++++-- src/renderer/src/config/models/utils.ts | 5 ++ src/renderer/src/i18n/locales/en-us.json | 7 +- src/renderer/src/i18n/locales/zh-cn.json | 8 +- src/renderer/src/i18n/locales/zh-tw.json | 7 +- src/renderer/src/i18n/translate/el-gr.json | 10 ++- src/renderer/src/i18n/translate/es-es.json | 10 ++- src/renderer/src/i18n/translate/fr-fr.json | 10 ++- src/renderer/src/i18n/translate/ja-jp.json | 10 ++- src/renderer/src/i18n/translate/pt-pt.json | 10 ++- src/renderer/src/i18n/translate/ru-ru.json | 10 ++- .../pages/home/Inputbar/ThinkingButton.tsx | 19 +++- .../pages/home/Inputbar/WebSearchButton.tsx | 16 +++- src/renderer/src/services/ApiService.ts | 4 +- src/renderer/src/store/websearch.ts | 2 + .../__tests__/blacklistMatchPattern.test.ts | 27 +++++- .../src/utils/blacklistMatchPattern.ts | 37 ++++++++ yarn.lock | 4 +- 32 files changed, 359 insertions(+), 173 deletions(-) delete mode 100644 packages/aiCore/src/core/options/openrouter.ts delete mode 100644 packages/aiCore/src/core/options/xai.ts create mode 100644 packages/aiCore/src/core/plugins/built-in/webSearchPlugin/openrouter.ts diff --git a/package.json b/package.json index bcbcbd10a5..b1bd7c8d72 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "@aws-sdk/client-bedrock-runtime": "^3.840.0", "@aws-sdk/client-s3": "^3.840.0", "@biomejs/biome": "2.2.4", - "@cherrystudio/ai-core": "workspace:^1.0.0-alpha.16", + "@cherrystudio/ai-core": "workspace:^1.0.0-alpha.17", "@cherrystudio/embedjs": "^0.1.31", "@cherrystudio/embedjs-libsql": "^0.1.31", "@cherrystudio/embedjs-loader-csv": "^0.1.31", diff --git a/packages/aiCore/package.json b/packages/aiCore/package.json index 292b679d82..642feff7c1 100644 --- a/packages/aiCore/package.json +++ b/packages/aiCore/package.json @@ -1,6 +1,6 @@ { "name": "@cherrystudio/ai-core", - "version": "1.0.0-alpha.16", + "version": "1.0.0-alpha.17", "description": "Cherry Studio AI Core - Unified AI Provider Interface Based on Vercel AI SDK", "main": "dist/index.js", "module": "dist/index.mjs", diff --git a/packages/aiCore/src/core/options/factory.ts b/packages/aiCore/src/core/options/factory.ts index 4350e9241b..ffeb15185c 100644 --- a/packages/aiCore/src/core/options/factory.ts +++ b/packages/aiCore/src/core/options/factory.ts @@ -59,7 +59,7 @@ export function createGoogleOptions(options: ExtractProviderOptions<'google'>) { /** * 创建OpenRouter供应商选项的便捷函数 */ -export function createOpenRouterOptions(options: ExtractProviderOptions<'openrouter'>) { +export function createOpenRouterOptions(options: ExtractProviderOptions<'openrouter'> | Record) { return createProviderOptions('openrouter', options) } diff --git a/packages/aiCore/src/core/options/openrouter.ts b/packages/aiCore/src/core/options/openrouter.ts deleted file mode 100644 index b351f8fda1..0000000000 --- a/packages/aiCore/src/core/options/openrouter.ts +++ /dev/null @@ -1,38 +0,0 @@ -export type OpenRouterProviderOptions = { - models?: string[] - - /** - * https://openrouter.ai/docs/use-cases/reasoning-tokens - * One of `max_tokens` or `effort` is required. - * If `exclude` is true, reasoning will be removed from the response. Default is false. - */ - reasoning?: { - exclude?: boolean - } & ( - | { - max_tokens: number - } - | { - effort: 'high' | 'medium' | 'low' - } - ) - - /** - * A unique identifier representing your end-user, which can - * help OpenRouter to monitor and detect abuse. - */ - user?: string - - extraBody?: Record - - /** - * Enable usage accounting to get detailed token usage information. - * https://openrouter.ai/docs/use-cases/usage-accounting - */ - usage?: { - /** - * When true, includes token usage information in the response. - */ - include: boolean - } -} diff --git a/packages/aiCore/src/core/options/types.ts b/packages/aiCore/src/core/options/types.ts index 724dc30698..8571fd2296 100644 --- a/packages/aiCore/src/core/options/types.ts +++ b/packages/aiCore/src/core/options/types.ts @@ -2,9 +2,8 @@ import { type AnthropicProviderOptions } from '@ai-sdk/anthropic' import { type GoogleGenerativeAIProviderOptions } from '@ai-sdk/google' import { type OpenAIResponsesProviderOptions } from '@ai-sdk/openai' import { type SharedV2ProviderMetadata } from '@ai-sdk/provider' - -import { type OpenRouterProviderOptions } from './openrouter' -import { type XaiProviderOptions } from './xai' +import { type XaiProviderOptions } from '@ai-sdk/xai' +import { type OpenRouterProviderOptions } from '@openrouter/ai-sdk-provider' export type ProviderOptions = SharedV2ProviderMetadata[T] diff --git a/packages/aiCore/src/core/options/xai.ts b/packages/aiCore/src/core/options/xai.ts deleted file mode 100644 index 7fe5672778..0000000000 --- a/packages/aiCore/src/core/options/xai.ts +++ /dev/null @@ -1,86 +0,0 @@ -// copy from @ai-sdk/xai/xai-chat-options.ts -// 如果@ai-sdk/xai暴露出了xaiProviderOptions就删除这个文件 - -import { z } from 'zod' - -const webSourceSchema = z.object({ - type: z.literal('web'), - country: z.string().length(2).optional(), - excludedWebsites: z.array(z.string()).max(5).optional(), - allowedWebsites: z.array(z.string()).max(5).optional(), - safeSearch: z.boolean().optional() -}) - -const xSourceSchema = z.object({ - type: z.literal('x'), - xHandles: z.array(z.string()).optional() -}) - -const newsSourceSchema = z.object({ - type: z.literal('news'), - country: z.string().length(2).optional(), - excludedWebsites: z.array(z.string()).max(5).optional(), - safeSearch: z.boolean().optional() -}) - -const rssSourceSchema = z.object({ - type: z.literal('rss'), - links: z.array(z.url()).max(1) // currently only supports one RSS link -}) - -const searchSourceSchema = z.discriminatedUnion('type', [ - webSourceSchema, - xSourceSchema, - newsSourceSchema, - rssSourceSchema -]) - -export const xaiProviderOptions = z.object({ - /** - * reasoning effort for reasoning models - * only supported by grok-3-mini and grok-3-mini-fast models - */ - reasoningEffort: z.enum(['low', 'high']).optional(), - - searchParameters: z - .object({ - /** - * search mode preference - * - "off": disables search completely - * - "auto": model decides whether to search (default) - * - "on": always enables search - */ - mode: z.enum(['off', 'auto', 'on']), - - /** - * whether to return citations in the response - * defaults to true - */ - returnCitations: z.boolean().optional(), - - /** - * start date for search data (ISO8601 format: YYYY-MM-DD) - */ - fromDate: z.string().optional(), - - /** - * end date for search data (ISO8601 format: YYYY-MM-DD) - */ - toDate: z.string().optional(), - - /** - * maximum number of search results to consider - * defaults to 20 - */ - maxSearchResults: z.number().min(1).max(50).optional(), - - /** - * data sources to search from - * defaults to ["web", "x"] if not specified - */ - sources: z.array(searchSourceSchema).optional() - }) - .optional() -}) - -export type XaiProviderOptions = z.infer diff --git a/packages/aiCore/src/core/plugins/built-in/index.ts b/packages/aiCore/src/core/plugins/built-in/index.ts index e7dcae8738..1f8916b09a 100644 --- a/packages/aiCore/src/core/plugins/built-in/index.ts +++ b/packages/aiCore/src/core/plugins/built-in/index.ts @@ -7,5 +7,9 @@ export const BUILT_IN_PLUGIN_PREFIX = 'built-in:' export { googleToolsPlugin } from './googleToolsPlugin' export { createLoggingPlugin } from './logging' export { createPromptToolUsePlugin } from './toolUsePlugin/promptToolUsePlugin' -export type { PromptToolUseConfig, ToolUseRequestContext, ToolUseResult } from './toolUsePlugin/type' -export { webSearchPlugin } from './webSearchPlugin' +export type { + PromptToolUseConfig, + ToolUseRequestContext, + ToolUseResult +} from './toolUsePlugin/type' +export { webSearchPlugin, type WebSearchPluginConfig } from './webSearchPlugin' diff --git a/packages/aiCore/src/core/plugins/built-in/webSearchPlugin/helper.ts b/packages/aiCore/src/core/plugins/built-in/webSearchPlugin/helper.ts index a7c7187bca..4845ce4ace 100644 --- a/packages/aiCore/src/core/plugins/built-in/webSearchPlugin/helper.ts +++ b/packages/aiCore/src/core/plugins/built-in/webSearchPlugin/helper.ts @@ -3,13 +3,16 @@ import { google } from '@ai-sdk/google' import { openai } from '@ai-sdk/openai' import { ProviderOptionsMap } from '../../../options/types' +import { OpenRouterSearchConfig } from './openrouter' /** * 从 AI SDK 的工具函数中提取参数类型,以确保类型安全。 */ -type OpenAISearchConfig = Parameters[0] -type AnthropicSearchConfig = Parameters[0] -type GoogleSearchConfig = Parameters[0] +export type OpenAISearchConfig = NonNullable[0]> +export type OpenAISearchPreviewConfig = NonNullable[0]> +export type AnthropicSearchConfig = NonNullable[0]> +export type GoogleSearchConfig = NonNullable[0]> +export type XAISearchConfig = NonNullable /** * 插件初始化时接收的完整配置对象 @@ -18,10 +21,12 @@ type GoogleSearchConfig = Parameters[0] */ export interface WebSearchPluginConfig { openai?: OpenAISearchConfig + 'openai-chat'?: OpenAISearchPreviewConfig anthropic?: AnthropicSearchConfig xai?: ProviderOptionsMap['xai']['searchParameters'] google?: GoogleSearchConfig 'google-vertex'?: GoogleSearchConfig + openrouter?: OpenRouterSearchConfig } /** @@ -31,6 +36,7 @@ export const DEFAULT_WEB_SEARCH_CONFIG: WebSearchPluginConfig = { google: {}, 'google-vertex': {}, openai: {}, + 'openai-chat': {}, xai: { mode: 'on', returnCitations: true, @@ -39,6 +45,14 @@ export const DEFAULT_WEB_SEARCH_CONFIG: WebSearchPluginConfig = { }, anthropic: { maxUses: 5 + }, + openrouter: { + plugins: [ + { + id: 'web', + max_results: 5 + } + ] } } diff --git a/packages/aiCore/src/core/plugins/built-in/webSearchPlugin/index.ts b/packages/aiCore/src/core/plugins/built-in/webSearchPlugin/index.ts index d941e0d0c7..abd3ce3e2c 100644 --- a/packages/aiCore/src/core/plugins/built-in/webSearchPlugin/index.ts +++ b/packages/aiCore/src/core/plugins/built-in/webSearchPlugin/index.ts @@ -6,7 +6,7 @@ import { anthropic } from '@ai-sdk/anthropic' import { google } from '@ai-sdk/google' import { openai } from '@ai-sdk/openai' -import { createXaiOptions, mergeProviderOptions } from '../../../options' +import { createOpenRouterOptions, createXaiOptions, mergeProviderOptions } from '../../../options' import { definePlugin } from '../../' import type { AiRequestContext } from '../../types' import { DEFAULT_WEB_SEARCH_CONFIG, WebSearchPluginConfig } from './helper' @@ -31,6 +31,13 @@ export const webSearchPlugin = (config: WebSearchPluginConfig = DEFAULT_WEB_SEAR } break } + case 'openai-chat': { + if (config['openai-chat']) { + if (!params.tools) params.tools = {} + params.tools.web_search_preview = openai.tools.webSearchPreview(config['openai-chat']) + } + break + } case 'anthropic': { if (config.anthropic) { @@ -56,6 +63,14 @@ export const webSearchPlugin = (config: WebSearchPluginConfig = DEFAULT_WEB_SEAR } break } + + case 'openrouter': { + if (config.openrouter) { + const searchOptions = createOpenRouterOptions(config.openrouter) + params.providerOptions = mergeProviderOptions(params.providerOptions, searchOptions) + } + break + } } return params diff --git a/packages/aiCore/src/core/plugins/built-in/webSearchPlugin/openrouter.ts b/packages/aiCore/src/core/plugins/built-in/webSearchPlugin/openrouter.ts new file mode 100644 index 0000000000..ebf1e7bf9a --- /dev/null +++ b/packages/aiCore/src/core/plugins/built-in/webSearchPlugin/openrouter.ts @@ -0,0 +1,26 @@ +export type OpenRouterSearchConfig = { + plugins?: Array<{ + id: 'web' + /** + * Maximum number of search results to include (default: 5) + */ + max_results?: number + /** + * Custom search prompt to guide the search query + */ + search_prompt?: string + }> + /** + * Built-in web search options for models that support native web search + */ + web_search_options?: { + /** + * Maximum number of search results to include + */ + max_results?: number + /** + * Custom search prompt to guide the search query + */ + search_prompt?: string + } +} diff --git a/packages/aiCore/src/core/providers/schemas.ts b/packages/aiCore/src/core/providers/schemas.ts index e4b8d8aa64..73ea4b8c14 100644 --- a/packages/aiCore/src/core/providers/schemas.ts +++ b/packages/aiCore/src/core/providers/schemas.ts @@ -25,7 +25,8 @@ export const baseProviderIds = [ 'xai', 'azure', 'azure-responses', - 'deepseek' + 'deepseek', + 'openrouter' ] as const /** @@ -38,6 +39,10 @@ export const baseProviderIdSchema = z.enum(baseProviderIds) */ export type BaseProviderId = z.infer +export const isBaseProvider = (id: ProviderId): id is BaseProviderId => { + return baseProviderIdSchema.safeParse(id).success +} + type BaseProvider = { id: BaseProviderId name: string diff --git a/src/renderer/src/aiCore/middleware/AiSdkMiddlewareBuilder.ts b/src/renderer/src/aiCore/middleware/AiSdkMiddlewareBuilder.ts index f0d3b2eb59..ffbe66da22 100644 --- a/src/renderer/src/aiCore/middleware/AiSdkMiddlewareBuilder.ts +++ b/src/renderer/src/aiCore/middleware/AiSdkMiddlewareBuilder.ts @@ -1,3 +1,4 @@ +import { WebSearchPluginConfig } from '@cherrystudio/ai-core/built-in/plugins' import { loggerService } from '@logger' import type { MCPTool, Message, Model, Provider } from '@renderer/types' import type { Chunk } from '@renderer/types/chunk' @@ -26,6 +27,8 @@ export interface AiSdkMiddlewareConfig { enableUrlContext: boolean mcpTools?: MCPTool[] uiMessages?: Message[] + // 内置搜索配置 + webSearchPluginConfig?: WebSearchPluginConfig } /** diff --git a/src/renderer/src/aiCore/plugins/PluginBuilder.ts b/src/renderer/src/aiCore/plugins/PluginBuilder.ts index d94ed4aab9..7c5478eb77 100644 --- a/src/renderer/src/aiCore/plugins/PluginBuilder.ts +++ b/src/renderer/src/aiCore/plugins/PluginBuilder.ts @@ -30,9 +30,8 @@ export function buildPlugins( } // 1. 模型内置搜索 - if (middlewareConfig.enableWebSearch) { - // 内置了默认搜索参数,如果改的话可以传config进去 - plugins.push(webSearchPlugin()) + if (middlewareConfig.enableWebSearch && middlewareConfig.webSearchPluginConfig) { + plugins.push(webSearchPlugin(middlewareConfig.webSearchPluginConfig)) } // 2. 支持工具调用时添加搜索插件 if (middlewareConfig.isSupportedToolUse || middlewareConfig.isPromptToolUse) { diff --git a/src/renderer/src/aiCore/prepareParams/parameterBuilder.ts b/src/renderer/src/aiCore/prepareParams/parameterBuilder.ts index 3f9e9ff071..0a89e73c62 100644 --- a/src/renderer/src/aiCore/prepareParams/parameterBuilder.ts +++ b/src/renderer/src/aiCore/prepareParams/parameterBuilder.ts @@ -5,6 +5,8 @@ import { vertexAnthropic } from '@ai-sdk/google-vertex/anthropic/edge' import { vertex } from '@ai-sdk/google-vertex/edge' +import { WebSearchPluginConfig } from '@cherrystudio/ai-core/built-in/plugins' +import { isBaseProvider } from '@cherrystudio/ai-core/core/providers/schemas' import { loggerService } from '@logger' import { isGenerateImageModel, @@ -16,8 +18,11 @@ import { isWebSearchModel } from '@renderer/config/models' import { getAssistantSettings, getDefaultModel } from '@renderer/services/AssistantService' +import store from '@renderer/store' +import { CherryWebSearchConfig } from '@renderer/store/websearch' import { type Assistant, type MCPTool, type Provider } from '@renderer/types' import type { StreamTextParams } from '@renderer/types/aiCoreTypes' +import { mapRegexToPatterns } from '@renderer/utils/blacklistMatchPattern' import type { ModelMessage, Tool } from 'ai' import { stepCountIs } from 'ai' @@ -25,6 +30,7 @@ import { getAiSdkProviderId } from '../provider/factory' import { setupToolsConfig } from '../utils/mcp' import { buildProviderOptions } from '../utils/options' import { getAnthropicThinkingBudget } from '../utils/reasoning' +import { buildProviderBuiltinWebSearchConfig } from '../utils/websearch' import { getTemperature, getTopP } from './modelParameters' const logger = loggerService.withContext('parameterBuilder') @@ -42,6 +48,7 @@ export async function buildStreamTextParams( options: { mcpTools?: MCPTool[] webSearchProviderId?: string + webSearchConfig?: CherryWebSearchConfig requestOptions?: { signal?: AbortSignal timeout?: number @@ -57,6 +64,7 @@ export async function buildStreamTextParams( enableGenerateImage: boolean enableUrlContext: boolean } + webSearchPluginConfig?: WebSearchPluginConfig }> { const { mcpTools } = options @@ -93,6 +101,12 @@ export async function buildStreamTextParams( // } // 构建真正的 providerOptions + const webSearchConfig: CherryWebSearchConfig = { + maxResults: store.getState().websearch.maxResults, + excludeDomains: store.getState().websearch.excludeDomains, + searchWithTime: store.getState().websearch.searchWithTime + } + const providerOptions = buildProviderOptions(assistant, model, provider, { enableReasoning, enableWebSearch, @@ -109,15 +123,21 @@ export async function buildStreamTextParams( maxTokens -= getAnthropicThinkingBudget(assistant, model) } - // google-vertex | google-vertex-anthropic + let webSearchPluginConfig: WebSearchPluginConfig | undefined = undefined if (enableWebSearch) { + if (isBaseProvider(aiSdkProviderId)) { + webSearchPluginConfig = buildProviderBuiltinWebSearchConfig(aiSdkProviderId, webSearchConfig) + } if (!tools) { tools = {} } if (aiSdkProviderId === 'google-vertex') { tools.google_search = vertex.tools.googleSearch({}) as ProviderDefinedTool } else if (aiSdkProviderId === 'google-vertex-anthropic') { - tools.web_search = vertexAnthropic.tools.webSearch_20250305({}) as ProviderDefinedTool + tools.web_search = vertexAnthropic.tools.webSearch_20250305({ + maxUses: webSearchConfig.maxResults, + blockedDomains: mapRegexToPatterns(webSearchConfig.excludeDomains) + }) as ProviderDefinedTool } } @@ -151,7 +171,8 @@ export async function buildStreamTextParams( return { params, modelId: model.id, - capabilities: { enableReasoning, enableWebSearch, enableGenerateImage, enableUrlContext } + capabilities: { enableReasoning, enableWebSearch, enableGenerateImage, enableUrlContext }, + webSearchPluginConfig } } diff --git a/src/renderer/src/aiCore/utils/websearch.ts b/src/renderer/src/aiCore/utils/websearch.ts index d2d0345826..e95f3e60cf 100644 --- a/src/renderer/src/aiCore/utils/websearch.ts +++ b/src/renderer/src/aiCore/utils/websearch.ts @@ -1,6 +1,13 @@ +import { + AnthropicSearchConfig, + OpenAISearchConfig, + WebSearchPluginConfig +} from '@cherrystudio/ai-core/core/plugins/built-in/webSearchPlugin/helper' +import { BaseProviderId } from '@cherrystudio/ai-core/provider' import { isOpenAIWebSearchChatCompletionOnlyModel } from '@renderer/config/models' -import { WEB_SEARCH_PROMPT_FOR_OPENROUTER } from '@renderer/config/prompts' +import { CherryWebSearchConfig } from '@renderer/store/websearch' import { Model } from '@renderer/types' +import { mapRegexToPatterns } from '@renderer/utils/blacklistMatchPattern' export function getWebSearchParams(model: Model): Record { if (model.provider === 'hunyuan') { @@ -21,11 +28,78 @@ export function getWebSearchParams(model: Model): Record { web_search_options: {} } } - - if (model.provider === 'openrouter') { - return { - plugins: [{ id: 'web', search_prompts: WEB_SEARCH_PROMPT_FOR_OPENROUTER }] - } - } return {} } + +/** + * range in [0, 100] + * @param maxResults + */ +function mapMaxResultToOpenAIContextSize(maxResults: number): OpenAISearchConfig['searchContextSize'] { + if (maxResults <= 33) return 'low' + if (maxResults <= 66) return 'medium' + return 'high' +} + +export function buildProviderBuiltinWebSearchConfig( + providerId: BaseProviderId, + webSearchConfig: CherryWebSearchConfig +): WebSearchPluginConfig { + switch (providerId) { + case 'openai': { + return { + openai: { + searchContextSize: mapMaxResultToOpenAIContextSize(webSearchConfig.maxResults) + } + } + } + case 'openai-chat': { + return { + 'openai-chat': { + searchContextSize: mapMaxResultToOpenAIContextSize(webSearchConfig.maxResults) + } + } + } + case 'anthropic': { + const anthropicSearchOptions: AnthropicSearchConfig = { + maxUses: webSearchConfig.maxResults, + blockedDomains: mapRegexToPatterns(webSearchConfig.excludeDomains) + } + return { + anthropic: anthropicSearchOptions + } + } + case 'xai': { + return { + xai: { + maxSearchResults: webSearchConfig.maxResults, + returnCitations: true, + sources: [ + { + type: 'web', + excludedWebsites: mapRegexToPatterns(webSearchConfig.excludeDomains) + }, + { type: 'news' }, + { type: 'x' } + ], + mode: 'on' + } + } + } + case 'openrouter': { + return { + openrouter: { + plugins: [ + { + id: 'web', + max_results: webSearchConfig.maxResults + } + ] + } + } + } + default: { + throw new Error(`Unsupported provider: ${providerId}`) + } + } +} diff --git a/src/renderer/src/config/models/utils.ts b/src/renderer/src/config/models/utils.ts index 3fd27308f8..39078e2924 100644 --- a/src/renderer/src/config/models/utils.ts +++ b/src/renderer/src/config/models/utils.ts @@ -229,6 +229,11 @@ export const isGPT5SeriesModel = (model: Model) => { return modelId.includes('gpt-5') } +export const isGPT5SeriesReasoningModel = (model: Model) => { + const modelId = getLowerBaseModelName(model.id) + return modelId.includes('gpt-5') && !modelId.includes('chat') +} + export const isGeminiModel = (model: Model) => { const modelId = getLowerBaseModelName(model.id) return modelId.includes('gemini') diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 4ac54e374a..768525d9b2 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -660,7 +660,12 @@ "title": "Topics", "unpin": "Unpin Topic" }, - "translate": "Translate" + "translate": "Translate", + "web_search": { + "warning": { + "openai": "The GPT-5 model's minimal reasoning effort does not support web search." + } + } }, "code": { "auto_update_to_latest": "Automatically update to latest version", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 248dbad623..551338e9d5 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -132,7 +132,6 @@ }, "title": "API 服务器" }, - "assistants": { "abbr": "助手", "clear": { @@ -661,7 +660,12 @@ "title": "话题", "unpin": "取消固定" }, - "translate": "翻译" + "translate": "翻译", + "web_search": { + "warning": { + "openai": "GPT5 模型 minimal 思考强度不支持网络搜索" + } + } }, "code": { "auto_update_to_latest": "检查更新并安装最新版本", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 4b31ca44be..fe7bf4b760 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -660,7 +660,12 @@ "title": "話題", "unpin": "取消固定" }, - "translate": "翻譯" + "translate": "翻譯", + "web_search": { + "warning": { + "openai": "GPT-5 模型的最小推理力度不支援網路搜尋。" + } + } }, "code": { "auto_update_to_latest": "檢查更新並安裝最新版本", diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index f95e243e5d..615eda601e 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -362,8 +362,9 @@ "translate": "Μετάφραση στο {{target_language}}", "translating": "Μετάφραση...", "upload": { + "attachment": "Μεταφόρτωση συνημμένου", "document": "Φόρτωση έγγραφου (το μοντέλο δεν υποστηρίζει εικόνες)", - "label": "Φόρτωση εικόνας ή έγγραφου", + "image_or_document": "Μεταφόρτωση εικόνας ή εγγράφου", "upload_from_local": "Μεταφόρτωση αρχείου από τον υπολογιστή..." }, "url_context": "Περιεχόμενο ιστοσελίδας", @@ -659,7 +660,12 @@ "title": "Θέματα", "unpin": "Ξεκαρφίτσωμα" }, - "translate": "Μετάφραση" + "translate": "Μετάφραση", + "web_search": { + "warning": { + "openai": "Το μοντέλο GPT5 με ελάχιστη ένταση σκέψης δεν υποστηρίζει αναζήτηση στο διαδίκτυο" + } + } }, "code": { "auto_update_to_latest": "Έλεγχος για ενημερώσεις και εγκατάσταση της τελευταίας έκδοσης", diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index 536e355f55..a210344a41 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -362,8 +362,9 @@ "translate": "Traducir a {{target_language}}", "translating": "Traduciendo...", "upload": { + "attachment": "Subir archivo adjunto", "document": "Subir documento (el modelo no admite imágenes)", - "label": "Subir imagen o documento", + "image_or_document": "Subir imagen o documento", "upload_from_local": "Subir archivo local..." }, "url_context": "Contexto de la página web", @@ -659,7 +660,12 @@ "title": "Tema", "unpin": "Quitar fijación" }, - "translate": "Traducir" + "translate": "Traducir", + "web_search": { + "warning": { + "openai": "El modelo GPT5 con intensidad de pensamiento mínima no admite búsqueda en la web." + } + } }, "code": { "auto_update_to_latest": "Comprobar actualizaciones e instalar la versión más reciente", diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index 00daec2d3f..3795f5ab01 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -362,8 +362,9 @@ "translate": "Traduire en {{target_language}}", "translating": "Traduction en cours...", "upload": { + "attachment": "Télécharger la pièce jointe", "document": "Télécharger un document (le modèle ne prend pas en charge les images)", - "label": "Télécharger une image ou un document", + "image_or_document": "Télécharger une image ou un document", "upload_from_local": "Télécharger un fichier local..." }, "url_context": "Contexte de la page web", @@ -659,7 +660,12 @@ "title": "Sujet", "unpin": "Annuler le fixage" }, - "translate": "Traduire" + "translate": "Traduire", + "web_search": { + "warning": { + "openai": "Le modèle GPT5 avec une intensité de réflexion minimale ne prend pas en charge la recherche sur Internet." + } + } }, "code": { "auto_update_to_latest": "Vérifier les mises à jour et installer la dernière version", diff --git a/src/renderer/src/i18n/translate/ja-jp.json b/src/renderer/src/i18n/translate/ja-jp.json index 3cdeca306e..f83153d90b 100644 --- a/src/renderer/src/i18n/translate/ja-jp.json +++ b/src/renderer/src/i18n/translate/ja-jp.json @@ -362,8 +362,9 @@ "translate": "{{target_language}}に翻訳", "translating": "翻訳中...", "upload": { + "attachment": "添付ファイルをアップロード", "document": "ドキュメントをアップロード(モデルは画像をサポートしません)", - "label": "画像またはドキュメントをアップロード", + "image_or_document": "画像またはドキュメントをアップロード", "upload_from_local": "ローカルファイルをアップロード..." }, "url_context": "URLコンテキスト", @@ -659,7 +660,12 @@ "title": "トピック", "unpin": "固定解除" }, - "translate": "翻訳" + "translate": "翻訳", + "web_search": { + "warning": { + "openai": "GPT5モデルの最小思考強度ではネット検索はサポートされません" + } + } }, "code": { "auto_update_to_latest": "最新バージョンを自動的に更新する", diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index b58593bb15..7759fc2706 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -362,8 +362,9 @@ "translate": "Traduzir para {{target_language}}", "translating": "Traduzindo...", "upload": { + "attachment": "Carregar anexo", "document": "Carregar documento (o modelo não suporta imagens)", - "label": "Carregar imagem ou documento", + "image_or_document": "Carregar imagem ou documento", "upload_from_local": "Fazer upload de arquivo local..." }, "url_context": "Contexto da Página da Web", @@ -659,7 +660,12 @@ "title": "Tópicos", "unpin": "Desfixar" }, - "translate": "Traduzir" + "translate": "Traduzir", + "web_search": { + "warning": { + "openai": "O modelo GPT5 com intensidade mínima de pensamento não suporta pesquisa na web" + } + } }, "code": { "auto_update_to_latest": "Verificar atualizações e instalar a versão mais recente", diff --git a/src/renderer/src/i18n/translate/ru-ru.json b/src/renderer/src/i18n/translate/ru-ru.json index 6364e5b83d..cec3dd0e74 100644 --- a/src/renderer/src/i18n/translate/ru-ru.json +++ b/src/renderer/src/i18n/translate/ru-ru.json @@ -362,8 +362,9 @@ "translate": "Перевести на {{target_language}}", "translating": "Перевод...", "upload": { + "attachment": "Загрузить вложение", "document": "Загрузить документ (модель не поддерживает изображения)", - "label": "Загрузить изображение или документ", + "image_or_document": "Загрузить изображение или документ", "upload_from_local": "Загрузить локальный файл..." }, "url_context": "Контекст страницы", @@ -659,7 +660,12 @@ "title": "Топики", "unpin": "Открепленные темы" }, - "translate": "Перевести" + "translate": "Перевести", + "web_search": { + "warning": { + "openai": "Модель GPT5 с минимальной интенсивностью мышления не поддерживает поиск в интернете" + } + } }, "code": { "auto_update_to_latest": "Автоматически обновлять до последней версии", diff --git a/src/renderer/src/pages/home/Inputbar/ThinkingButton.tsx b/src/renderer/src/pages/home/Inputbar/ThinkingButton.tsx index 5ee0c628c8..7e2b7edd38 100644 --- a/src/renderer/src/pages/home/Inputbar/ThinkingButton.tsx +++ b/src/renderer/src/pages/home/Inputbar/ThinkingButton.tsx @@ -8,7 +8,13 @@ import { MdiLightbulbOn80 } from '@renderer/components/Icons/SVGIcon' import { QuickPanelReservedSymbol, useQuickPanel } from '@renderer/components/QuickPanel' -import { getThinkModelType, isDoubaoThinkingAutoModel, MODEL_SUPPORTED_OPTIONS } from '@renderer/config/models' +import { + getThinkModelType, + isDoubaoThinkingAutoModel, + isGPT5SeriesReasoningModel, + isOpenAIWebSearchModel, + MODEL_SUPPORTED_OPTIONS +} from '@renderer/config/models' import { useAssistant } from '@renderer/hooks/useAssistant' import { getReasoningEffortOptionsLabel } from '@renderer/i18n/label' import { Model, ThinkingOption } from '@renderer/types' @@ -61,6 +67,15 @@ const ThinkingButton: FC = ({ ref, model, assistantId }): ReactElement => }) return } + if ( + isOpenAIWebSearchModel(model) && + isGPT5SeriesReasoningModel(model) && + assistant.enableWebSearch && + option === 'minimal' + ) { + window.toast.warning(t('chat.web_search.warning.openai')) + return + } updateAssistantSettings({ reasoning_effort: option, reasoning_effort_cache: option, @@ -68,7 +83,7 @@ const ThinkingButton: FC = ({ ref, model, assistantId }): ReactElement => }) return }, - [updateAssistantSettings] + [updateAssistantSettings, assistant.enableWebSearch, model, t] ) const panelItems = useMemo(() => { diff --git a/src/renderer/src/pages/home/Inputbar/WebSearchButton.tsx b/src/renderer/src/pages/home/Inputbar/WebSearchButton.tsx index cb3c5fb6d9..807781bac4 100644 --- a/src/renderer/src/pages/home/Inputbar/WebSearchButton.tsx +++ b/src/renderer/src/pages/home/Inputbar/WebSearchButton.tsx @@ -3,7 +3,12 @@ import { loggerService } from '@logger' import { ActionIconButton } from '@renderer/components/Buttons' import { BingLogo, BochaLogo, ExaLogo, SearXNGLogo, TavilyLogo, ZhipuLogo } from '@renderer/components/Icons' import { QuickPanelListItem, QuickPanelReservedSymbol, useQuickPanel } from '@renderer/components/QuickPanel' -import { isGeminiModel, isWebSearchModel } from '@renderer/config/models' +import { + isGeminiModel, + isGPT5SeriesReasoningModel, + isOpenAIWebSearchModel, + isWebSearchModel +} from '@renderer/config/models' import { isGeminiWebSearchProvider } from '@renderer/config/providers' import { useAssistant } from '@renderer/hooks/useAssistant' import { useTimer } from '@renderer/hooks/useTimer' @@ -115,6 +120,15 @@ const WebSearchButton: FC = ({ ref, assistantId }) => { update.enableWebSearch = false window.toast.warning(t('chat.mcp.warning.gemini_web_search')) } + if ( + isOpenAIWebSearchModel(model) && + isGPT5SeriesReasoningModel(model) && + update.enableWebSearch && + assistant.settings?.reasoning_effort === 'minimal' + ) { + update.enableWebSearch = false + window.toast.warning(t('chat.web_search.warning.openai')) + } setTimeoutTimer('updateSelectedWebSearchBuiltin', () => updateAssistant(update), 200) }, [assistant, setTimeoutTimer, t, updateAssistant]) diff --git a/src/renderer/src/services/ApiService.ts b/src/renderer/src/services/ApiService.ts index 01807027ba..d954fc1f85 100644 --- a/src/renderer/src/services/ApiService.ts +++ b/src/renderer/src/services/ApiService.ts @@ -117,7 +117,8 @@ export async function fetchChatCompletion({ const { params: aiSdkParams, modelId, - capabilities + capabilities, + webSearchPluginConfig } = await buildStreamTextParams(messages, assistant, provider, { mcpTools: mcpTools, webSearchProviderId: assistant.webSearchProviderId, @@ -132,6 +133,7 @@ export async function fetchChatCompletion({ isPromptToolUse: isPromptToolUse(assistant), isSupportedToolUse: isSupportedToolUse(assistant), isImageGenerationEndpoint: isDedicatedImageGenerationModel(assistant.model || getDefaultModel()), + webSearchPluginConfig: webSearchPluginConfig, enableWebSearch: capabilities.enableWebSearch, enableGenerateImage: capabilities.enableGenerateImage, enableUrlContext: capabilities.enableUrlContext, diff --git a/src/renderer/src/store/websearch.ts b/src/renderer/src/store/websearch.ts index 1e3fe2a25b..16029ccf47 100644 --- a/src/renderer/src/store/websearch.ts +++ b/src/renderer/src/store/websearch.ts @@ -41,6 +41,8 @@ export interface WebSearchState { providerConfig: Record } +export type CherryWebSearchConfig = Pick + export const initialState: WebSearchState = { defaultProvider: 'local-bing', providers: WEB_SEARCH_PROVIDERS, diff --git a/src/renderer/src/utils/__tests__/blacklistMatchPattern.test.ts b/src/renderer/src/utils/__tests__/blacklistMatchPattern.test.ts index 4656e30562..a5c0983479 100644 --- a/src/renderer/src/utils/__tests__/blacklistMatchPattern.test.ts +++ b/src/renderer/src/utils/__tests__/blacklistMatchPattern.test.ts @@ -26,7 +26,7 @@ import { describe, expect, it } from 'vitest' -import { MatchPatternMap } from '../blacklistMatchPattern' +import { mapRegexToPatterns, MatchPatternMap } from '../blacklistMatchPattern' function get(map: MatchPatternMap, url: string) { return map.get(url).sort() @@ -161,3 +161,28 @@ describe('blacklistMatchPattern', () => { expect(get(map, 'https://b.mozilla.org/path/')).toEqual([0, 1, 2, 6]) }) }) + +describe('mapRegexToPatterns', () => { + it('extracts domains from regex patterns', () => { + const result = mapRegexToPatterns([ + '/example\\.com/', + '/(?:www\\.)?sub\\.example\\.co\\.uk/', + '/api\\.service\\.io/', + 'https://baidu.com' + ]) + + expect(result).toEqual(['example.com', 'sub.example.co.uk', 'api.service.io', 'baidu.com']) + }) + + it('deduplicates domains across multiple patterns', () => { + const result = mapRegexToPatterns(['/example\\.com/', '/(example\\.com|test\\.org)/']) + + expect(result).toEqual(['example.com', 'test.org']) + }) + + it('ignores patterns without domain matches', () => { + const result = mapRegexToPatterns(['', 'plain-domain.com', '/^https?:\\/\\/[^/]+$/']) + + expect(result).toEqual(['plain-domain.com']) + }) +}) diff --git a/src/renderer/src/utils/blacklistMatchPattern.ts b/src/renderer/src/utils/blacklistMatchPattern.ts index 9e78ab58fb..b00e07a785 100644 --- a/src/renderer/src/utils/blacklistMatchPattern.ts +++ b/src/renderer/src/utils/blacklistMatchPattern.ts @@ -202,6 +202,43 @@ export async function parseSubscribeContent(url: string): Promise { throw error } } + +export function mapRegexToPatterns(patterns: string[]): string[] { + const patternSet = new Set() + const domainMatcher = /[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+/g + + patterns.forEach((pattern) => { + if (!pattern) { + return + } + + // Handle regex patterns (wrapped in /) + if (pattern.startsWith('/') && pattern.endsWith('/')) { + const rawPattern = pattern.slice(1, -1) + const normalizedPattern = rawPattern.replace(/\\\./g, '.').replace(/\\\//g, '/') + const matches = normalizedPattern.match(domainMatcher) + + if (matches) { + matches.forEach((match) => { + patternSet.add(match.replace(/http(s)?:\/\//g, '').toLowerCase()) + }) + } + } else if (pattern.includes('://')) { + // Handle URLs with protocol (e.g., https://baidu.com) + const matches = pattern.match(domainMatcher) + if (matches) { + matches.forEach((match) => { + patternSet.add(match.replace(/http(s)?:\/\//g, '').toLowerCase()) + }) + } + } else { + patternSet.add(pattern.toLowerCase()) + } + }) + + return Array.from(patternSet) +} + export async function filterResultWithBlacklist( response: WebSearchProviderResponse, websearch: WebSearchState diff --git a/yarn.lock b/yarn.lock index 1d414c0861..71280de91a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2309,7 +2309,7 @@ __metadata: languageName: node linkType: hard -"@cherrystudio/ai-core@workspace:^1.0.0-alpha.16, @cherrystudio/ai-core@workspace:packages/aiCore": +"@cherrystudio/ai-core@workspace:^1.0.0-alpha.17, @cherrystudio/ai-core@workspace:packages/aiCore": version: 0.0.0-use.local resolution: "@cherrystudio/ai-core@workspace:packages/aiCore" dependencies: @@ -13195,7 +13195,7 @@ __metadata: "@aws-sdk/client-bedrock-runtime": "npm:^3.840.0" "@aws-sdk/client-s3": "npm:^3.840.0" "@biomejs/biome": "npm:2.2.4" - "@cherrystudio/ai-core": "workspace:^1.0.0-alpha.16" + "@cherrystudio/ai-core": "workspace:^1.0.0-alpha.17" "@cherrystudio/embedjs": "npm:^0.1.31" "@cherrystudio/embedjs-libsql": "npm:^0.1.31" "@cherrystudio/embedjs-loader-csv": "npm:^0.1.31" From dec68ee297cb8a4a8ac421185c5bfffe36af4668 Mon Sep 17 00:00:00 2001 From: SuYao Date: Wed, 17 Sep 2025 15:54:54 +0800 Subject: [PATCH 08/16] Fix/ts (#10226) * chore: ts * chore: yarn.lock --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index b1bd7c8d72..964be0e0fd 100644 --- a/package.json +++ b/package.json @@ -332,7 +332,7 @@ "tsx": "^4.20.3", "turndown-plugin-gfm": "^1.0.2", "tw-animate-css": "^1.3.8", - "typescript": "^5.8.2", + "typescript": "~5.8.2", "undici": "6.21.2", "unified": "^11.0.5", "uuid": "^10.0.0", diff --git a/yarn.lock b/yarn.lock index 71280de91a..23a168d8f9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13436,7 +13436,7 @@ __metadata: turndown: "npm:7.2.0" turndown-plugin-gfm: "npm:^1.0.2" tw-animate-css: "npm:^1.3.8" - typescript: "npm:^5.8.2" + typescript: "npm:~5.8.2" undici: "npm:6.21.2" unified: "npm:^11.0.5" uuid: "npm:^10.0.0" @@ -27767,7 +27767,7 @@ __metadata: languageName: node linkType: hard -"typescript@npm:^5.0.0, typescript@npm:^5.8.2": +"typescript@npm:^5.0.0": version: 5.9.2 resolution: "typescript@npm:5.9.2" bin: @@ -27777,7 +27777,7 @@ __metadata: languageName: node linkType: hard -"typescript@npm:^5.4.3": +"typescript@npm:^5.4.3, typescript@npm:~5.8.2": version: 5.8.3 resolution: "typescript@npm:5.8.3" bin: @@ -27787,7 +27787,7 @@ __metadata: languageName: node linkType: hard -"typescript@patch:typescript@npm%3A^5.0.0#optional!builtin, typescript@patch:typescript@npm%3A^5.8.2#optional!builtin": +"typescript@patch:typescript@npm%3A^5.0.0#optional!builtin": version: 5.9.2 resolution: "typescript@patch:typescript@npm%3A5.9.2#optional!builtin::version=5.9.2&hash=5786d5" bin: @@ -27797,7 +27797,7 @@ __metadata: languageName: node linkType: hard -"typescript@patch:typescript@npm%3A^5.4.3#optional!builtin": +"typescript@patch:typescript@npm%3A^5.4.3#optional!builtin, typescript@patch:typescript@npm%3A~5.8.2#optional!builtin": version: 5.8.3 resolution: "typescript@patch:typescript@npm%3A5.8.3#optional!builtin::version=5.8.3&hash=5786d5" bin: From 77535b002ac58a1da7f75f71fb0d158418513c97 Mon Sep 17 00:00:00 2001 From: Phantom <59059173+EurFelux@users.noreply.github.com> Date: Wed, 17 Sep 2025 16:33:10 +0800 Subject: [PATCH 09/16] feat(Inputbar): cache mentioned models state between renders (#10197) Use a ref to track mentioned models state and cache it in a module-level variable when component unmounts to preserve state between renders --- src/renderer/src/pages/home/Inputbar/Inputbar.tsx | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx index 864ea71a21..9a130e45ff 100644 --- a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx +++ b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx @@ -71,6 +71,7 @@ interface Props { let _text = '' let _files: FileType[] = [] +let _mentionedModelsCache: Model[] = [] const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) => { const [text, setText] = useState(_text) @@ -103,7 +104,8 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = const spaceClickTimer = useRef(null) const [isTranslating, setIsTranslating] = useState(false) const [selectedKnowledgeBases, setSelectedKnowledgeBases] = useState([]) - const [mentionedModels, setMentionedModels] = useState([]) + const [mentionedModels, setMentionedModels] = useState(_mentionedModelsCache) + const mentionedModelsRef = useRef(mentionedModels) const [isDragging, setIsDragging] = useState(false) const [isFileDragging, setIsFileDragging] = useState(false) const [textareaHeight, setTextareaHeight] = useState() @@ -114,6 +116,10 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = const isGenerateImageAssistant = useMemo(() => isGenerateImageModel(model), [model]) const { setTimeoutTimer } = useTimer() + useEffect(() => { + mentionedModelsRef.current = mentionedModels + }, [mentionedModels]) + const isVisionSupported = useMemo( () => (mentionedModels.length > 0 && isVisionModels(mentionedModels)) || @@ -179,6 +185,13 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = _text = text _files = files + useEffect(() => { + // 利用useEffect清理函数在卸载组件时更新状态缓存 + return () => { + _mentionedModelsCache = mentionedModelsRef.current + } + }, []) + const focusTextarea = useCallback(() => { textareaRef.current?.focus() }, []) From 6afaf6244c5e462572b8b7b82207cf5f06321591 Mon Sep 17 00:00:00 2001 From: SuYao Date: Wed, 17 Sep 2025 17:23:23 +0800 Subject: [PATCH 10/16] Fix Anthropic API URL and add endpoint path handling (#10229) * Fix Anthropic API URL and add endpoint path handling - Remove trailing slash from Anthropic API base URL - Add isAnthropicProvider utility function - Update provider settings to show full endpoint URL for Anthropic - Add migration to clean up existing Anthropic provider URLs * Update src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx Co-authored-by: Phantom <59059173+EurFelux@users.noreply.github.com> --------- Co-authored-by: Phantom <59059173+EurFelux@users.noreply.github.com> --- src/renderer/src/config/providers.ts | 2 +- .../settings/ProviderSettings/ProviderSetting.tsx | 14 ++++++++++++-- src/renderer/src/store/index.ts | 2 +- src/renderer/src/store/migrate.ts | 15 +++++++++++++++ src/renderer/src/utils/index.ts | 4 ++++ 5 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/renderer/src/config/providers.ts b/src/renderer/src/config/providers.ts index f28a88c7c7..a710605b64 100644 --- a/src/renderer/src/config/providers.ts +++ b/src/renderer/src/config/providers.ts @@ -1020,7 +1020,7 @@ export const PROVIDER_URLS: Record = { }, anthropic: { api: { - url: 'https://api.anthropic.com/' + url: 'https://api.anthropic.com' }, websites: { official: 'https://anthropic.com/', diff --git a/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx b/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx index 0bd7b152f4..68bfc67ba2 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx @@ -16,7 +16,13 @@ import { useAppDispatch } from '@renderer/store' import { updateWebSearchProvider } from '@renderer/store/websearch' import { isSystemProvider } from '@renderer/types' import { ApiKeyConnectivity, HealthStatus } from '@renderer/types/healthCheck' -import { formatApiHost, formatApiKeys, getFancyProviderName, isOpenAIProvider } from '@renderer/utils' +import { + formatApiHost, + formatApiKeys, + getFancyProviderName, + isAnthropicProvider, + isOpenAIProvider +} from '@renderer/utils' import { formatErrorMessage } from '@renderer/utils/error' import { Button, Divider, Flex, Input, Select, Space, Switch, Tooltip } from 'antd' import Link from 'antd/es/typography/Link' @@ -212,6 +218,10 @@ const ProviderSetting: FC = ({ providerId }) => { if (provider.type === 'azure-openai') { return formatApiHost(apiHost) + 'openai/v1' } + + if (provider.type === 'anthropic') { + return formatApiHost(apiHost) + 'messages' + } return formatApiHost(apiHost) + 'responses' } @@ -361,7 +371,7 @@ const ProviderSetting: FC = ({ providerId }) => { )} - {isOpenAIProvider(provider) && ( + {(isOpenAIProvider(provider) || isAnthropicProvider(provider)) && ( diff --git a/src/renderer/src/store/index.ts b/src/renderer/src/store/index.ts index dc599d0658..3b3cfa3f0c 100644 --- a/src/renderer/src/store/index.ts +++ b/src/renderer/src/store/index.ts @@ -67,7 +67,7 @@ const persistedReducer = persistReducer( { key: 'cherry-studio', storage, - version: 155, + version: 156, blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs'], migrate }, diff --git a/src/renderer/src/store/migrate.ts b/src/renderer/src/store/migrate.ts index c939007d0b..41fa441945 100644 --- a/src/renderer/src/store/migrate.ts +++ b/src/renderer/src/store/migrate.ts @@ -2476,6 +2476,21 @@ const migrateConfig = { logger.error('migrate 155 error', error as Error) return state } + }, + '156': (state: RootState) => { + try { + state.llm.providers.forEach((provider) => { + if (provider.id === SystemProviderIds.anthropic) { + if (provider.apiHost.endsWith('/')) { + provider.apiHost = provider.apiHost.slice(0, -1) + } + } + }) + return state + } catch (error) { + logger.error('migrate 156 error', error as Error) + return state + } } } diff --git a/src/renderer/src/utils/index.ts b/src/renderer/src/utils/index.ts index 828d288ba8..64f943946a 100644 --- a/src/renderer/src/utils/index.ts +++ b/src/renderer/src/utils/index.ts @@ -205,6 +205,10 @@ export function isOpenAIProvider(provider: Provider): boolean { return !['anthropic', 'gemini', 'vertexai'].includes(provider.type) } +export function isAnthropicProvider(provider: Provider): boolean { + return provider.type === 'anthropic' +} + /** * 判断模型是否为用户手动选择 * @param {Model} model 模型对象 From 273475881e9a3cf6d5e306fa9baee25192e82e77 Mon Sep 17 00:00:00 2001 From: kangfenmao Date: Wed, 17 Sep 2025 17:18:01 +0800 Subject: [PATCH 11/16] refactor: update HeroUIProvider import in MiniWindowApp component - Changed the import of HeroUIProvider from '@heroui/react' to '@renderer/context/HeroUIProvider' for better context management. --- src/renderer/src/windows/mini/MiniWindowApp.tsx | 2 +- src/renderer/src/windows/selection/action/entryPoint.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/renderer/src/windows/mini/MiniWindowApp.tsx b/src/renderer/src/windows/mini/MiniWindowApp.tsx index b459a3e027..0f6d8bc60d 100644 --- a/src/renderer/src/windows/mini/MiniWindowApp.tsx +++ b/src/renderer/src/windows/mini/MiniWindowApp.tsx @@ -1,9 +1,9 @@ import '@renderer/databases' -import { HeroUIProvider } from '@heroui/react' import { ErrorBoundary } from '@renderer/components/ErrorBoundary' import { ToastPortal } from '@renderer/components/ToastPortal' import { getToastUtilities } from '@renderer/components/TopView/toast' +import { HeroUIProvider } from '@renderer/context/HeroUIProvider' import { useSettings } from '@renderer/hooks/useSettings' import store, { persistor } from '@renderer/store' import { useEffect } from 'react' diff --git a/src/renderer/src/windows/selection/action/entryPoint.tsx b/src/renderer/src/windows/selection/action/entryPoint.tsx index 44f24ccb97..5aa8d3f745 100644 --- a/src/renderer/src/windows/selection/action/entryPoint.tsx +++ b/src/renderer/src/windows/selection/action/entryPoint.tsx @@ -2,13 +2,13 @@ import '@renderer/assets/styles/index.css' import '@renderer/assets/styles/tailwind.css' import '@ant-design/v5-patch-for-react-19' -import { HeroUIProvider } from '@heroui/react' import KeyvStorage from '@kangfenmao/keyv-storage' import { loggerService } from '@logger' import { ToastPortal } from '@renderer/components/ToastPortal' import { getToastUtilities } from '@renderer/components/TopView/toast' import AntdProvider from '@renderer/context/AntdProvider' import { CodeStyleProvider } from '@renderer/context/CodeStyleProvider' +import { HeroUIProvider } from '@renderer/context/HeroUIProvider' import { ThemeProvider } from '@renderer/context/ThemeProvider' import storeSyncService from '@renderer/services/StoreSyncService' import store, { persistor } from '@renderer/store' From 6c9fc598d4a35f99c2c343d2e065118ae5ea53bb Mon Sep 17 00:00:00 2001 From: kangfenmao Date: Wed, 17 Sep 2025 18:57:08 +0800 Subject: [PATCH 12/16] bump: version 1.6.0-rc.2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- electron-builder.yml | 24 ++++++++++-------------- package.json | 2 +- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/electron-builder.yml b/electron-builder.yml index 0660319150..6b7056ed3a 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -125,20 +125,16 @@ afterSign: scripts/notarize.js artifactBuildCompleted: scripts/artifact-build-completed.js releaseInfo: releaseNotes: | - ✨ 新功能: - - 集成 Perplexity SDK 和 Anthropic OAuth - - 支持 API 服务器模式,提供外部调用接口 - - 新增字体自定义设置功能 - - 笔记支持文件夹批量上传 - - 集成 HeroUI 和 Tailwind CSS 提升界面体验 + 🐛 问题修复: + - 修复 Anthropic API URL 处理,移除尾部斜杠并添加端点路径处理 + - 修复 MessageEditor 缺少 QuickPanelProvider 包装的问题 + - 修复 MiniWindow 高度问题 🚀 性能优化: - - 优化大文件上传,支持 OpenAI 标准文件服务 - - 重构 MCP 服务,改进错误处理和状态管理 + - 优化输入栏提及模型状态缓存,在渲染间保持状态 + - 重构网络搜索参数支持模型内置搜索,新增 OpenAI Chat 和 OpenRouter 支持 - 🐛 问题修复: - - 修复 WebSearch RAG 并发问题 - - 修复翻译页面长文本渲染布局问题 - - 修复笔记拖拽排序和无限循环问题 - - 修复 macOS CodeTool 工作目录错误 - - 修复多个 UI 组件的响应式设计问题 + 🔧 重构改进: + - 更新 HeroUIProvider 导入路径,改善上下文管理 + - 更新依赖项和 VSCode 开发环境配置 + - 升级 @cherrystudio/ai-core 到 v1.0.0-alpha.17 diff --git a/package.json b/package.json index 964be0e0fd..8873b7a4a4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "CherryStudio", - "version": "1.6.0-rc.1", + "version": "1.6.0-rc.2", "private": true, "description": "A powerful AI assistant for producer.", "main": "./out/main/index.js", From 89d5bd817bf4a633fbab89f38a437d5dcee36656 Mon Sep 17 00:00:00 2001 From: SuYao Date: Wed, 17 Sep 2025 20:01:47 +0800 Subject: [PATCH 13/16] fix: Add AWS Bedrock reasoning extraction middleware (#10231) * Add AWS Bedrock reasoning extraction middleware - Add 'reasoning' tag to tagNameArray for broader reasoning support - Add AWS Bedrock case with gpt-oss model-specific reasoning extraction - Add openai-chat and openrouter cases to provider options switch - Remove unused zod import * Add OpenRouter provider support Updates ai-core to version alpha.18 with OpenRouter integration and improves provider ID resolution for OpenAI API hosts. --- package.json | 2 +- packages/aiCore/package.json | 2 +- packages/aiCore/src/core/providers/schemas.ts | 10 +++++++++- .../src/aiCore/middleware/AiSdkMiddlewareBuilder.ts | 12 +++++++++++- src/renderer/src/aiCore/provider/factory.ts | 3 +++ src/renderer/src/aiCore/utils/options.ts | 5 ++++- yarn.lock | 4 ++-- 7 files changed, 31 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 8873b7a4a4..b76e5f6f21 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "@aws-sdk/client-bedrock-runtime": "^3.840.0", "@aws-sdk/client-s3": "^3.840.0", "@biomejs/biome": "2.2.4", - "@cherrystudio/ai-core": "workspace:^1.0.0-alpha.17", + "@cherrystudio/ai-core": "workspace:^1.0.0-alpha.18", "@cherrystudio/embedjs": "^0.1.31", "@cherrystudio/embedjs-libsql": "^0.1.31", "@cherrystudio/embedjs-loader-csv": "^0.1.31", diff --git a/packages/aiCore/package.json b/packages/aiCore/package.json index 642feff7c1..75ed6ea34e 100644 --- a/packages/aiCore/package.json +++ b/packages/aiCore/package.json @@ -1,6 +1,6 @@ { "name": "@cherrystudio/ai-core", - "version": "1.0.0-alpha.17", + "version": "1.0.0-alpha.18", "description": "Cherry Studio AI Core - Unified AI Provider Interface Based on Vercel AI SDK", "main": "dist/index.js", "module": "dist/index.mjs", diff --git a/packages/aiCore/src/core/providers/schemas.ts b/packages/aiCore/src/core/providers/schemas.ts index 73ea4b8c14..83338cf057 100644 --- a/packages/aiCore/src/core/providers/schemas.ts +++ b/packages/aiCore/src/core/providers/schemas.ts @@ -9,7 +9,9 @@ import { createDeepSeek } from '@ai-sdk/deepseek' import { createGoogleGenerativeAI } from '@ai-sdk/google' import { createOpenAI, type OpenAIProviderSettings } from '@ai-sdk/openai' import { createOpenAICompatible } from '@ai-sdk/openai-compatible' +import { LanguageModelV2 } from '@ai-sdk/provider' import { createXai } from '@ai-sdk/xai' +import { createOpenRouter } from '@openrouter/ai-sdk-provider' import { customProvider, Provider } from 'ai' import { z } from 'zod' @@ -46,7 +48,7 @@ export const isBaseProvider = (id: ProviderId): id is BaseProviderId => { type BaseProvider = { id: BaseProviderId name: string - creator: (options: any) => Provider + creator: (options: any) => Provider | LanguageModelV2 supportsImageGeneration: boolean } @@ -124,6 +126,12 @@ export const baseProviders = [ name: 'DeepSeek', creator: createDeepSeek, supportsImageGeneration: false + }, + { + id: 'openrouter', + name: 'OpenRouter', + creator: createOpenRouter, + supportsImageGeneration: true } ] as const satisfies BaseProvider[] diff --git a/src/renderer/src/aiCore/middleware/AiSdkMiddlewareBuilder.ts b/src/renderer/src/aiCore/middleware/AiSdkMiddlewareBuilder.ts index ffbe66da22..eabdf1815f 100644 --- a/src/renderer/src/aiCore/middleware/AiSdkMiddlewareBuilder.ts +++ b/src/renderer/src/aiCore/middleware/AiSdkMiddlewareBuilder.ts @@ -140,7 +140,7 @@ export function buildAiSdkMiddlewares(config: AiSdkMiddlewareConfig): LanguageMo return builder.build() } -const tagNameArray = ['think', 'thought'] +const tagNameArray = ['think', 'thought', 'reasoning'] /** * 添加provider特定的中间件 @@ -167,6 +167,16 @@ function addProviderSpecificMiddlewares(builder: AiSdkMiddlewareBuilder, config: case 'gemini': // Gemini特定中间件 break + case 'aws-bedrock': { + if (config.model?.id.includes('gpt-oss')) { + const tagName = tagNameArray[2] + builder.add({ + name: 'thinking-tag-extraction', + middleware: extractReasoningMiddleware({ tagName }) + }) + } + break + } default: // 其他provider的通用处理 break diff --git a/src/renderer/src/aiCore/provider/factory.ts b/src/renderer/src/aiCore/provider/factory.ts index 617758753e..bfcd3da383 100644 --- a/src/renderer/src/aiCore/provider/factory.ts +++ b/src/renderer/src/aiCore/provider/factory.ts @@ -69,6 +69,9 @@ export function getAiSdkProviderId(provider: Provider): ProviderId | 'openai-com return resolvedFromType } } + if (provider.apiHost.includes('api.openai.com')) { + return 'openai-chat' + } // 3. 最后的fallback(通常会成为openai-compatible) return provider.id as ProviderId } diff --git a/src/renderer/src/aiCore/utils/options.ts b/src/renderer/src/aiCore/utils/options.ts index f85c8c7879..dec475fc8b 100644 --- a/src/renderer/src/aiCore/utils/options.ts +++ b/src/renderer/src/aiCore/utils/options.ts @@ -82,6 +82,7 @@ export function buildProviderOptions( // 应该覆盖所有类型 switch (baseProviderId) { case 'openai': + case 'openai-chat': case 'azure': providerSpecificOptions = { ...buildOpenAIProviderOptions(assistant, model, capabilities), @@ -101,13 +102,15 @@ export function buildProviderOptions( providerSpecificOptions = buildXAIProviderOptions(assistant, model, capabilities) break case 'deepseek': - case 'openai-compatible': + case 'openrouter': + case 'openai-compatible': { // 对于其他 provider,使用通用的构建逻辑 providerSpecificOptions = { ...buildGenericProviderOptions(assistant, model, capabilities), serviceTier: serviceTierSetting } break + } default: throw new Error(`Unsupported base provider ${baseProviderId}`) } diff --git a/yarn.lock b/yarn.lock index 23a168d8f9..2393eaec5b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2309,7 +2309,7 @@ __metadata: languageName: node linkType: hard -"@cherrystudio/ai-core@workspace:^1.0.0-alpha.17, @cherrystudio/ai-core@workspace:packages/aiCore": +"@cherrystudio/ai-core@workspace:^1.0.0-alpha.18, @cherrystudio/ai-core@workspace:packages/aiCore": version: 0.0.0-use.local resolution: "@cherrystudio/ai-core@workspace:packages/aiCore" dependencies: @@ -13195,7 +13195,7 @@ __metadata: "@aws-sdk/client-bedrock-runtime": "npm:^3.840.0" "@aws-sdk/client-s3": "npm:^3.840.0" "@biomejs/biome": "npm:2.2.4" - "@cherrystudio/ai-core": "workspace:^1.0.0-alpha.17" + "@cherrystudio/ai-core": "workspace:^1.0.0-alpha.18" "@cherrystudio/embedjs": "npm:^0.1.31" "@cherrystudio/embedjs-libsql": "npm:^0.1.31" "@cherrystudio/embedjs-loader-csv": "npm:^0.1.31" From c76df7fb16c8bc726dad39e68e2174615ec4bf2b Mon Sep 17 00:00:00 2001 From: SuYao Date: Wed, 17 Sep 2025 23:10:58 +0800 Subject: [PATCH 14/16] fix: Remove maxTokens check from Anthropic thinking budget (#10240) Remove maxTokens check from Anthropic thinking budget --- src/renderer/src/aiCore/utils/reasoning.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/src/aiCore/utils/reasoning.ts b/src/renderer/src/aiCore/utils/reasoning.ts index 385d8183c5..bee07b1e0d 100644 --- a/src/renderer/src/aiCore/utils/reasoning.ts +++ b/src/renderer/src/aiCore/utils/reasoning.ts @@ -312,7 +312,7 @@ export function getOpenAIReasoningParams(assistant: Assistant, model: Model): Re export function getAnthropicThinkingBudget(assistant: Assistant, model: Model): number { const { maxTokens, reasoning_effort: reasoningEffort } = getAssistantSettings(assistant) - if (maxTokens === undefined || reasoningEffort === undefined) { + if (reasoningEffort === undefined) { return 0 } const effortRatio = EFFORT_RATIO[reasoningEffort] From ca597b9b9bbf77b4feb565c2d6a02a682c6d0fe3 Mon Sep 17 00:00:00 2001 From: Phantom <59059173+EurFelux@users.noreply.github.com> Date: Wed, 17 Sep 2025 23:24:02 +0800 Subject: [PATCH 15/16] CI: improve claude translator for review, quote and email (#10230) * ci(claude-translator): extend workflow to handle pull request review events - Add support for pull_request_review and pull_request_review_comment events - Update condition logic to include new event types - Expand claude_args to include pull request review related API commands - Enhance prompt to handle new event types and more translation scenarios * ci(workflows): update concurrency group in claude-translator workflow Add github.event.review.id as additional fallback for concurrency group naming * fix(workflow): correct API method for pull_request_review event Use PATCH instead of PUT and update body parameter to match API requirements * ci: clarify comment ID label in workflow output Update the label for comment ID in workflow output to explicitly indicate when it refers to review comments * ci: fix syntax error in GitHub workflow file * fix(workflow): correct HTTP method for pull_request_review event Use PUT instead of PATCH for updating pull request reviews as per GitHub API requirements --- .github/workflows/claude-translator.yml | 61 +++++++++++++++++++------ 1 file changed, 46 insertions(+), 15 deletions(-) diff --git a/.github/workflows/claude-translator.yml b/.github/workflows/claude-translator.yml index ab2b6f7e4f..ff317f8532 100644 --- a/.github/workflows/claude-translator.yml +++ b/.github/workflows/claude-translator.yml @@ -1,6 +1,6 @@ name: Claude Translator concurrency: - group: translator-${{ github.event.comment.id || github.event.issue.number }} + group: translator-${{ github.event.comment.id || github.event.issue.number || github.event.review.id }} cancel-in-progress: false on: @@ -8,14 +8,18 @@ on: types: [opened] issue_comment: types: [created, edited] + pull_request_review: + types: [submitted, edited] + pull_request_review_comment: + types: [created, edited] jobs: translate: if: | (github.event_name == 'issues') || - (github.event_name == 'issue_comment' && github.event.sender.type != 'Bot') && - ((github.event_name == 'issue_comment' && github.event.action == 'created' && !contains(github.event.comment.body, 'This issue was translated by Claude')) || - (github.event_name == 'issue_comment' && github.event.action == 'edited')) + (github.event_name == 'issue_comment' && github.event.sender.type != 'Bot') || + (github.event_name == 'pull_request_review' && github.event.sender.type != 'Bot') || + (github.event_name == 'pull_request_review_comment' && github.event.sender.type != 'Bot') runs-on: ubuntu-latest permissions: contents: read @@ -37,23 +41,44 @@ jobs: # Now `contents: read` is safe for files, but we could make a fine-grained token to control it. # See: https://github.com/anthropics/claude-code-action/blob/main/docs/security.md github_token: ${{ secrets.TOKEN_GITHUB_WRITE }} - allowed_non_write_users: '*' + allowed_non_write_users: "*" claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - claude_args: '--allowed-tools Bash(gh issue:*),Bash(gh api:repos/*/issues:*)' + claude_args: "--allowed-tools Bash(gh issue:*),Bash(gh api:repos/*/issues:*),Bash(gh api:repos/*/pulls/*/reviews/*),Bash(gh api:repos/*/pulls/comments/*)" prompt: | - 你是一个多语言翻译助手。请完成以下任务: + 你是一个多语言翻译助手。你需要响应 GitHub Webhooks 中的以下四种事件: + + - issues + - issue_comment + - pull_request_review + - pull_request_review_comment + + 请完成以下任务: + + 1. 获取当前事件的完整信息。 + + - 如果当前事件是 issues,就获取该 issues 的信息。 + - 如果当前事件是 issue_comment,就获取该 comment 的信息。 + - 如果当前事件是 pull_request_review,就获取该 review 的信息。 + - 如果当前事件是 pull_request_review_comment,就获取该 comment 的信息。 - 1. 获取当前issue/comment的完整信息 2. 智能检测内容。 - 1. 如果是已经遵循格式要求翻译过的issue/comment,检查翻译内容和原始内容是否匹配。若不匹配,则重新翻译一次令其匹配,并遵循格式要求;若匹配,则跳过任务。 - 2. 如果是未翻译过的issue/comment,检查其内容语言。若不是英文,则翻译成英文;若已经是英文,则跳过任务。 + + - 如果获取到的信息是已经遵循格式要求翻译过的内容,则检查翻译内容和原始内容是否匹配。若不匹配,则重新翻译一次令其匹配,并遵循格式要求; + - 如果获取到的信息是未翻译过的内容,检查其内容语言。若不是英文,则翻译成英文; + - 如果获取到的信息是部分翻译为英文的内容,则将其翻译为英文; + - 如果获取到的信息包含了对已翻译内容的引用,则将引用内容清理为仅含英文的内容。引用的内容不能够包含"This xxx was translated by Claude"和"Original Content`等内容。 + - 如果获取到的信息包含了其他类型的引用,即对非 Claude 翻译的内容的引用,则直接照原样引用,不进行翻译。 + - 如果获取到的信息是通过邮件回复的内容,则在翻译时应当将邮件内容的引用放到最后。在原始内容和翻译内容中只需要回复的内容本身,不要包含对邮件内容的引用。 + - 如果获取到的信息本身不需要任何处理,则跳过任务。 + 3. 格式要求: + - 标题:英文翻译(如果非英文) - 内容格式: > [!NOTE] - > This issue/comment was translated by Claude. + > This issue/comment/review was translated by Claude. - [英文翻译内容] + [翻译内容] ---
@@ -62,15 +87,21 @@ jobs:
4. 使用gh工具更新: + - 根据环境信息中的Event类型选择正确的命令: - - 如果Event是'issues':gh issue edit [ISSUE_NUMBER] --title "[英文标题]" --body "[翻译内容 + 原始内容]" - - 如果Event是'issue_comment':gh api -X PATCH /repos/[REPO]/issues/comments/[COMMENT_ID] -f body="[翻译内容 + 原始内容]" + - 如果 Event 是 'issues': gh issue edit [ISSUE_NUMBER] --title "[英文标题]" --body "[翻译内容 + 原始内容]" + - 如果 Event 是 'issue_comment': gh api -X PATCH /repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }} -f body="[翻译内容 + 原始内容]" + - 如果 Event 是 'pull_request_review': gh api -X PUT /repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/reviews/${{ github.event.review.id }} -f body="[翻译内容]" + - 如果 Event 是 'pull_request_review_comment': gh api -X PATCH /repos/${{ github.repository }}/pulls/comments/${{ github.event.comment.id }} -f body="[翻译内容 + 原始内容]" 环境信息: - Event: ${{ github.event_name }} - Issue Number: ${{ github.event.issue.number }} - Repository: ${{ github.repository }} - - Comment ID: ${{ github.event.comment.id || 'N/A' }} (only available for comment events) + - (Review) Comment ID: ${{ github.event.comment.id || 'N/A' }} + - Pull Request Number: ${{ github.event.pull_request.number || 'N/A' }} + - Review ID: ${{ github.event.review.id || 'N/A' }} + 使用以下命令获取完整信息: gh issue view ${{ github.event.issue.number }} --json title,body,comments From 49eed449c3a4c94c560a6b6cdb6fc9f1bdf808d8 Mon Sep 17 00:00:00 2001 From: Vaayne Date: Thu, 18 Sep 2025 12:02:11 +0800 Subject: [PATCH 16/16] refactor: Remove outdated validation and planning documents for agents service --- .vscode/settings.json | 4 +- VALIDATION_REPORT.md | 238 ----------- agents-refactor-plan.md | 198 --------- docs/agents-api-ui-integration.md | 654 ------------------------------ plan.md | 241 ----------- 5 files changed, 2 insertions(+), 1333 deletions(-) delete mode 100644 VALIDATION_REPORT.md delete mode 100644 agents-refactor-plan.md delete mode 100644 docs/agents-api-ui-integration.md delete mode 100644 plan.md diff --git a/.vscode/settings.json b/.vscode/settings.json index 81bbc41671..141179f38c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -34,10 +34,10 @@ "*.css": "tailwindcss" }, "files.eol": "\n", - // "i18n-ally.displayLanguage": "zh-cn", // 界面显示语言 + "i18n-ally.displayLanguage": "zh-cn", "i18n-ally.enabledFrameworks": ["react-i18next", "i18next"], "i18n-ally.enabledParsers": ["ts", "js", "json"], // 解析语言 - "i18n-ally.fullReloadOnChanged": true, + "i18n-ally.fullReloadOnChanged": true, // 界面显示语言 "i18n-ally.keystyle": "nested", // 翻译路径格式 "i18n-ally.localesPaths": ["src/renderer/src/i18n/locales"], // "i18n-ally.namespace": true, // 开启命名空间 diff --git a/VALIDATION_REPORT.md b/VALIDATION_REPORT.md deleted file mode 100644 index 371544b90f..0000000000 --- a/VALIDATION_REPORT.md +++ /dev/null @@ -1,238 +0,0 @@ -# Agents Service Refactoring - Validation Report - -## Overview - -This report documents the comprehensive validation of the agents service refactoring completed on September 12, 2025. All tests were performed to ensure the refactored system maintains full functionality while providing improved structure and maintainability. - -## Validation Summary - -✅ **ALL VALIDATIONS PASSED** - The refactoring has been successfully completed and verified. - ---- - -## 1. Build and Compilation Validation - -### Command: `yarn build:check` - -**Status:** ✅ PASSED - -**Results:** - -- TypeScript compilation for Node.js environment: ✅ PASSED -- TypeScript compilation for Web environment: ✅ PASSED -- i18n validation: ✅ PASSED -- Test suite execution: ✅ PASSED (1420 tests across 108 files) - -**Duration:** 23.12s - -### Key Findings: - -- All TypeScript files compile without errors -- No type definition conflicts detected -- Import/export structure is correctly maintained -- All service dependencies resolve correctly - ---- - -## 2. Migration System Validation - -### Custom Migration Test - -**Status:** ✅ PASSED - -**Test Coverage:** - -1. ✅ Migration tracking table creation -2. ✅ Migration indexes creation -3. ✅ Migration record insertion/retrieval -4. ✅ Database schema creation (agents table) -5. ✅ Agent record CRUD operations -6. ✅ Session tables creation -7. ✅ Session logs table creation -8. ✅ Foreign key relationships -9. ✅ Data retrieval with joins -10. ✅ Migration cleanup - -### Key Findings: - -- Migration system initializes correctly -- All migration tables and indexes are created properly -- Transaction support works as expected -- Rollback functionality is available -- Checksum validation ensures migration integrity - ---- - -## 3. Service Initialization Validation - -### Custom Service Structure Test - -**Status:** ✅ PASSED - -**Validated Components:** - -1. ✅ All service files are present and accessible -2. ✅ Migration files are properly organized -3. ✅ Query files are correctly structured -4. ✅ Schema files are properly organized -5. ✅ Module export structure is correct -6. ✅ Backward compatibility is maintained -7. ✅ Old db.ts file has been properly removed -8. ✅ TypeScript compilation validated - -### File Structure Verification: - -``` -src/main/services/agents/ -├── ✅ BaseService.ts -├── ✅ services/ -│ ├── ✅ AgentService.ts -│ ├── ✅ SessionService.ts -│ ├── ✅ SessionLogService.ts -│ └── ✅ index.ts -├── ✅ database/ -│ ├── ✅ migrations/ -│ │ ├── ✅ 001_initial_schema.ts -│ │ ├── ✅ 002_add_session_tables.ts -│ │ ├── ✅ types.ts -│ │ └── ✅ index.ts -│ ├── ✅ queries/ -│ │ ├── ✅ agent.queries.ts -│ │ ├── ✅ session.queries.ts -│ │ ├── ✅ sessionLog.queries.ts -│ │ └── ✅ index.ts -│ ├── ✅ schema/ -│ │ ├── ✅ tables.ts -│ │ ├── ✅ indexes.ts -│ │ ├── ✅ migrations.ts -│ │ └── ✅ index.ts -│ ├── ✅ migrator.ts -│ └── ✅ index.ts -└── ✅ index.ts -``` - ---- - -## 4. Database Operations Validation - -### Comprehensive CRUD Operations Test - -**Status:** ✅ PASSED - -**Test Scenarios:** - -1. ✅ Database schema setup (tables + indexes) -2. ✅ Agent CRUD operations - - Create: ✅ Agent creation with JSON field serialization - - Read: ✅ Agent retrieval and data integrity verification - - Update: ✅ Agent updates with field validation - - Delete: ✅ Agent deletion (tested via cascade) - - List: ✅ Agent listing and counting operations -3. ✅ Session operations - - Create: ✅ Session creation with foreign key constraints - - Read: ✅ Session retrieval and agent association - - List: ✅ Sessions by agent queries -4. ✅ Session Log operations - - Create: ✅ Multiple log types creation - - Read: ✅ Log retrieval ordered by timestamp -5. ✅ Foreign Key constraints - - Cascade Delete: ✅ Agent deletion cascades to sessions and logs - - Referential Integrity: ✅ Foreign key relationships maintained -6. ✅ Concurrent operations - - Parallel Creation: ✅ 5 concurrent agents created successfully - - Data Integrity: ✅ All concurrent operations verified - -### Performance Metrics: - -- Agent CRUD operations: < 50ms per operation -- Migration system: < 100ms initialization -- Concurrent operations: Successfully handled 5 parallel operations - ---- - -## 5. Backward Compatibility Validation - -### Compatibility Checks: - -- ✅ Export structure maintains backward compatibility -- ✅ Legacy query exports available via `AgentQueries_Legacy` -- ✅ Service singleton instances preserved -- ✅ Database interface unchanged for external consumers -- ✅ Migration system added without breaking existing functionality - ---- - -## 6. Code Quality and Structure - -### Improvements Delivered: - -1. **Modular Organization**: ✅ Services split into focused, single-responsibility files -2. **Migration System**: ✅ Version-controlled schema changes with rollback support -3. **Query Organization**: ✅ SQL queries organized by entity type -4. **Schema Management**: ✅ Table and index definitions centralized -5. **Type Safety**: ✅ TypeScript interfaces for all operations -6. **Error Handling**: ✅ Comprehensive error handling and logging -7. **Testing**: ✅ All existing tests continue to pass - -### Benefits Realized: - -- **Maintainability**: Easier to locate and modify specific functionality -- **Scalability**: Simple to add new entities without affecting existing code -- **Production Readiness**: Atomic migrations with transaction support -- **Team Development**: Reduced merge conflicts with smaller, focused files -- **Documentation**: Clear structure makes codebase more navigable - ---- - -## 7. Security and Safety Validation - -### Security Measures Verified: - -- ✅ SQL injection protection via parameterized queries -- ✅ Transaction isolation for atomic operations -- ✅ Foreign key constraints prevent orphaned records -- ✅ JSON field validation and safe parsing -- ✅ Migration checksums prevent tampering - ---- - -## 8. Performance Validation - -### Database Operations: - -- ✅ Index utilization verified for common queries -- ✅ Foreign key constraints optimized with indexes -- ✅ JSON field operations efficient -- ✅ Concurrent access handled properly - ---- - -## Cleanup - -The following temporary test files were created for validation and can be safely removed: - -- `/Users/weliu/workspace/cherry-studio/migration-validation-test.js` -- `/Users/weliu/workspace/cherry-studio/service-initialization-test.js` -- `/Users/weliu/workspace/cherry-studio/database-operations-test.js` - ---- - -## Final Recommendation - -✅ **APPROVED FOR PRODUCTION** - -The agents service refactoring has been successfully completed and thoroughly validated. All functionality is preserved while delivering significant improvements in code organization, maintainability, and scalability. The migration system is production-ready and will support future schema evolution safely. - -## Next Steps - -1. The refactoring is complete and ready for deployment -2. Consider removing temporary test files -3. Monitor the system in production to validate real-world performance -4. Begin utilizing the new modular structure for future feature development - ---- - -**Validation completed:** September 12, 2025 -**Total validation time:** ~45 minutes -**Tests executed:** 1420 + custom validation tests -**Overall result:** ✅ SUCCESS diff --git a/agents-refactor-plan.md b/agents-refactor-plan.md deleted file mode 100644 index a3edc9f998..0000000000 --- a/agents-refactor-plan.md +++ /dev/null @@ -1,198 +0,0 @@ -# Agents Service Refactoring Plan - -## Overview - -Restructure the agents service to split database operations into smaller, more manageable files with migration support. - -## New Folder Structure - -``` -src/main/services/agents/ -├── database/ -│ ├── migrations/ -│ │ ├── types.ts # Migration interfaces -│ │ ├── 001_initial_schema.ts # Initial tables & indexes -│ │ ├── 002_add_session_tables.ts # Session related tables -│ │ └── index.ts # Export all migrations -│ ├── queries/ -│ │ ├── agent.queries.ts # Agent CRUD queries -│ │ ├── session.queries.ts # Session CRUD queries -│ │ ├── sessionLog.queries.ts # Session log queries -│ │ └── index.ts # Export all queries -│ ├── schema/ -│ │ ├── tables.ts # Table definitions -│ │ ├── indexes.ts # Index definitions -│ │ ├── migrations.ts # Migration tracking table -│ │ └── index.ts # Export all schema -│ ├── migrator.ts # Migration runner class -│ └── index.ts # Main database exports -├── services/ -│ ├── AgentService.ts # Agent business logic -│ ├── SessionService.ts # Session business logic -│ ├── SessionLogService.ts # Session log business logic -│ └── index.ts # Export all services -├── BaseService.ts # Shared database utilities with migration support -└── index.ts # Main module exports -``` - -## Implementation Tasks - -### Task 1: Create Folder Structure and Migration System Infrastructure - -**Status**: ✅ COMPLETED -**Agent**: `general-purpose` -**Description**: Create all necessary directories and implement the migration system infrastructure - -**Subtasks**: - -- [x] Create database/, database/migrations/, database/queries/, database/schema/, services/ directories -- [x] Implement migration types and interfaces in database/migrations/types.ts -- [x] Build Migrator class with transaction support in database/migrator.ts -- [x] Create migration tracking table schema in database/schema/migrations.ts - ---- - -### Task 2: Split Database Queries from db.ts - -**Status**: ✅ COMPLETED -**Agent**: `general-purpose` -**Description**: Extract and organize queries from the current db.ts file into separate, focused files - -**Subtasks**: - -- [x] Move agent queries to database/queries/agent.queries.ts -- [x] Move session queries to database/queries/session.queries.ts -- [x] Move session log queries to database/queries/sessionLog.queries.ts -- [x] Extract table definitions to database/schema/tables.ts -- [x] Extract index definitions to database/schema/indexes.ts -- [x] Create index files for queries and schema directories -- [x] Update db.ts to maintain backward compatibility by re-exporting split queries - ---- - -### Task 3: Create Initial Migration Files - -**Status**: ✅ COMPLETED -**Agent**: `general-purpose` -**Description**: Create migration files based on existing schema - -**Subtasks**: - -- [x] Create 001_initial_schema.ts with agents table and indexes -- [x] Create 002_add_session_tables.ts with sessions and session_logs tables -- [x] Create database/migrations/index.ts to export all migrations - ---- - -### Task 4: Update BaseService with Migration Support - -**Status**: ✅ COMPLETED -**Agent**: `general-purpose` -**Description**: Integrate migration system into BaseService initialization - -**Subtasks**: - -- [x] Update BaseService.ts to use Migrator on initialize -- [x] Keep existing JSON serialization utilities -- [x] Update database initialization flow - ---- - -### Task 5: Reorganize Service Files - -**Status**: ✅ COMPLETED -**Agent**: `general-purpose` -**Description**: Move service files to services subdirectory and update imports - -**Subtasks**: - -- [x] Move AgentService.ts to services/ -- [x] Move SessionService.ts to services/ -- [x] Move SessionLogService.ts to services/ -- [x] Update import paths in all service files (now import from '../BaseService' and '../db') -- [x] Create services/index.ts to export all services - ---- - -### Task 6: Create Export Structure and Clean Up - -**Status**: ✅ COMPLETED -**Agent**: `general-purpose` -**Description**: Create proper export hierarchy and clean up old files - -**Subtasks**: - -- [x] Create main agents/index.ts with clean exports -- [x] Create database/index.ts for database exports -- [x] Ensure backward compatibility for existing imports -- [x] Remove old db.ts file -- [x] Update any external imports if needed - ---- - -### Task 7: Test and Validate Refactoring - -**Status**: ✅ COMPLETED -**Agent**: `general-purpose` -**Description**: Ensure all functionality works after refactoring - -**Subtasks**: - -- [x] Run build check: `yarn build:check` ✅ PASSED (1420 tests, TypeScript compilation successful) -- [x] Run tests: `yarn test` ✅ PASSED (All existing tests continue to pass) -- [x] Validate migration system works ✅ PASSED (11 migration tests, transaction support verified) -- [x] Check that all services initialize correctly ✅ PASSED (File structure, exports, backward compatibility) -- [x] Verify database operations work as expected ✅ PASSED (CRUD operations, foreign keys, concurrent operations) - -**Additional Validation**: - -- [x] Created comprehensive validation report (VALIDATION_REPORT.md) -- [x] Validated migration system with custom test suite -- [x] Verified service initialization and file structure -- [x] Tested complete database operations including concurrent access -- [x] Confirmed backward compatibility maintained -- [x] Validated security measures and performance optimizations - ---- - -## Benefits of This Refactoring - -1. **Single Responsibility**: Each file handles one specific concern -2. **Version-Controlled Schema**: Migration system tracks all database changes -3. **Easier Maintenance**: Find and modify queries for specific entities quickly -4. **Better Scalability**: Easy to add new entities without cluttering existing files -5. **Clear Organization**: Logical grouping makes navigation intuitive -6. **Production Ready**: Atomic migrations with transaction support -7. **Reduced Merge Conflicts**: Smaller files mean fewer conflicts in team development - -## Migration Best Practices Implemented - -- ✅ Version-controlled migrations with tracking table -- ✅ Atomic operations with transaction support -- ✅ Rollback capability (optional down migrations) -- ✅ Incremental updates (only run pending migrations) -- ✅ Safe for production deployments - ---- - -**Progress Summary**: 7/7 tasks completed 🎉 - -**Status**: ✅ **REFACTORING COMPLETED SUCCESSFULLY** - -All tasks have been completed and thoroughly validated. The agents service refactoring delivers: - -- ✅ Modular, maintainable code structure -- ✅ Production-ready migration system -- ✅ Complete backward compatibility -- ✅ Comprehensive test validation -- ✅ Enhanced developer experience - -**Final deliverables:** - -- 📁 Reorganized service architecture with clear separation of concerns -- 🗃️ Database migration system with transaction support and rollback capability -- 📋 Comprehensive validation report (VALIDATION_REPORT.md) -- ✅ All 1420+ tests passing with full TypeScript compliance -- 🔒 Security hardening with parameterized queries and foreign key constraints - -**Ready for production deployment** 🚀 diff --git a/docs/agents-api-ui-integration.md b/docs/agents-api-ui-integration.md deleted file mode 100644 index fc12c01735..0000000000 --- a/docs/agents-api-ui-integration.md +++ /dev/null @@ -1,654 +0,0 @@ -# Agent API UI Integration Guide - -## Overview - -This document provides comprehensive guidance for UI components to integrate with the new Agent API system. The agents data is now stored in the database and accessed through API endpoints instead of Redux state management. - -## Key Changes from Previous Implementation - -### Data Storage -- **Before**: Agent data stored in Redux store -- **After**: Agent data stored in SQLite database, accessed via REST API - -### State Management -- **Before**: Redux actions and selectors for agent operations -- **After**: Direct API calls using fetch/axios, no Redux dependency - -### Data Flow -- **Before**: Component → Redux Action → State Update → Component Re-render -- **After**: Component → API Call → UI Update → Database - -## API Endpoints Overview - -### Base Configuration -- **Base URL**: `http://localhost:23333/v1` -- **Authentication**: Bearer token (API key format: `cs-sk-{uuid}`) -- **Content-Type**: `application/json` - -### Agent Management (`/agents`) - -| Method | Endpoint | Description | Request Body | Response | -|--------|----------|-------------|--------------|----------| -| POST | `/agents` | Create new agent | `CreateAgentRequest` | `AgentEntity` | -| GET | `/agents` | List agents (paginated) | Query params | `{ data: AgentEntity[], total: number }` | -| GET | `/agents/{id}` | Get specific agent | - | `AgentEntity` | -| PUT | `/agents/{id}` | Replace agent (complete update) | `UpdateAgentRequest` | `AgentEntity` | -| PATCH | `/agents/{id}` | Partially update agent | `Partial` | `AgentEntity` | -| DELETE | `/agents/{id}` | Delete agent | - | `204 No Content` | - -### Session Management (`/agents/{agentId}/sessions`) - -| Method | Endpoint | Description | Request Body | Response | -|--------|----------|-------------|--------------|----------| -| POST | `/agents/{agentId}/sessions` | Create session | `CreateSessionRequest` | `AgentSessionEntity` | -| GET | `/agents/{agentId}/sessions` | List agent sessions | Query params | `{ data: AgentSessionEntity[], total: number }` | -| GET | `/agents/{agentId}/sessions/{id}` | Get specific session | - | `AgentSessionEntity` | -| PUT | `/agents/{agentId}/sessions/{id}` | Replace session (complete update) | `UpdateSessionRequest` | `AgentSessionEntity` | -| PATCH | `/agents/{agentId}/sessions/{id}` | Partially update session | `Partial` | `AgentSessionEntity` | -| DELETE | `/agents/{agentId}/sessions/{id}` | Delete session | - | `204 No Content` | - -### Message Streaming (`/agents/{agentId}/sessions/{sessionId}/messages`) - -| Method | Endpoint | Description | Request Body | Response | -|--------|----------|-------------|--------------|----------| -| POST | `/agents/{agentId}/sessions/{sessionId}/messages` | Send message to agent | `CreateMessageRequest` | **Stream Response** | -| GET | `/agents/{agentId}/sessions/{sessionId}/messages` | List session messages | Query params | `{ data: SessionMessageEntity[], total: number }` | - -## HTTP Methods: PUT vs PATCH - -Both agents and sessions support two types of update operations: - -### PUT - Complete Replacement -- **Purpose**: Replaces the entire resource with the provided data -- **Behavior**: All fields in the request body will be applied to the resource -- **Use Case**: When you want to completely update a resource with a new set of values -- **Example**: Updating an agent's configuration completely - -```typescript -// PUT - Replace entire agent -await fetch('/v1/agents/agent-123', { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - name: 'New Agent Name', - model: 'gpt-4', - instructions: 'New instructions', - built_in_tools: ['search', 'calculator'], - // All other fields will be reset to defaults if not provided - }) -}) -``` - -### PATCH - Partial Update -- **Purpose**: Updates only the specified fields, leaving others unchanged -- **Behavior**: Only the fields present in the request body will be modified -- **Use Case**: When you want to update specific fields without affecting others -- **Example**: Updating only an agent's name or instructions - -```typescript -// PATCH - Update only specific fields -await fetch('/v1/agents/agent-123', { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - name: 'Updated Agent Name' - // All other fields remain unchanged - }) -}) -``` - -### Validation -Both methods use the same validation rules: -- All fields are optional for both PUT and PATCH -- When provided, fields must meet their validation criteria (e.g., `name` cannot be empty) -- The same middleware (`validateAgentUpdate` for agents, `validateSessionUpdate` for sessions) handles both operations - -## Data Types & Schemas - -### AgentEntity -```typescript -interface AgentEntity { - id: string - type: AgentType - name: string - description?: string - avatar?: string - instructions?: string - - // Core configuration - model: string // Required - main model ID - plan_model?: string - small_model?: string - built_in_tools?: string[] - mcps?: string[] - knowledges?: string[] - configuration?: Record - accessible_paths?: string[] - permission_mode?: PermissionMode - max_steps?: number - - // Timestamps - created_at: string - updated_at: string -} -``` - -### AgentSessionEntity -```typescript -interface AgentSessionEntity { - id: string - name?: string - main_agent_id: string - sub_agent_ids?: string[] - user_goal?: string - status: SessionStatus - external_session_id?: string - - // Configuration overrides (inherits from agent if not specified) - model?: string - plan_model?: string - small_model?: string - built_in_tools?: string[] - mcps?: string[] - knowledges?: string[] - configuration?: Record - accessible_paths?: string[] - permission_mode?: PermissionMode - max_steps?: number - - // Timestamps - created_at: string - updated_at: string -} -``` - -### SessionMessageEntity -```typescript -interface SessionMessageEntity { - id: number - session_id: string - parent_id?: number - role: SessionMessageRole // 'user' | 'agent' | 'system' | 'tool' - type: SessionMessageType - content: Record - metadata?: Record - created_at: string - updated_at: string -} -``` - -## Creating Agents - -### Minimal Agent Creation -For early stage implementation, only use these essential fields: - -```typescript -const createAgentRequest = { - name: string, // Required - model: string, // Required - instructions?: string, // System prompt - built_in_tools?: string[], - mcps?: string[], - knowledges?: string[] -} -``` - -### Example: Create Agent -```typescript -async function createAgent(agentData: CreateAgentRequest): Promise { - const response = await fetch('/v1/agents', { - method: 'POST', - headers: { - 'Authorization': `Bearer ${apiKey}`, - 'Content-Type': 'application/json' - }, - body: JSON.stringify(agentData) - }) - - if (!response.ok) { - throw new Error(`Agent creation failed: ${response.statusText}`) - } - - return await response.json() -} -``` - -### Example: List Agents -```typescript -async function listAgents(limit = 20, offset = 0): Promise<{data: AgentEntity[], total: number}> { - const response = await fetch(`/v1/agents?limit=${limit}&offset=${offset}`, { - headers: { - 'Authorization': `Bearer ${apiKey}` - } - }) - - return await response.json() -} -``` - -## Managing Agent Sessions - -### Session Creation -```typescript -async function createSession(agentId: string, sessionData: CreateSessionRequest): Promise { - const response = await fetch(`/v1/agents/${agentId}/sessions`, { - method: 'POST', - headers: { - 'Authorization': `Bearer ${apiKey}`, - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - user_goal: sessionData.user_goal, // User's goal as input message - model: sessionData.model, // Override agent's model if needed - // tools and mcps can be overridden per session - }) - }) - - return await response.json() -} -``` - -### Session Updates -Sessions can be updated using either PUT (complete replacement) or PATCH (partial update): - -#### Complete Session Replacement (PUT) -```typescript -async function replaceSession(agentId: string, sessionId: string, sessionData: UpdateSessionRequest): Promise { - const response = await fetch(`/v1/agents/${agentId}/sessions/${sessionId}`, { - method: 'PUT', - headers: { - 'Authorization': `Bearer ${apiKey}`, - 'Content-Type': 'application/json' - }, - body: JSON.stringify(sessionData) // Complete session configuration - }) - - return await response.json() -} -``` - -#### Partial Session Update (PATCH) -```typescript -async function updateSession(agentId: string, sessionId: string, updates: Partial): Promise { - const response = await fetch(`/v1/agents/${agentId}/sessions/${sessionId}`, { - method: 'PATCH', - headers: { - 'Authorization': `Bearer ${apiKey}`, - 'Content-Type': 'application/json' - }, - body: JSON.stringify(updates) // Only the fields to update - }) - - return await response.json() -} -``` - -#### Session Status Management -Sessions have five possible statuses: -- `idle`: Ready to process messages -- `running`: Currently processing -- `completed`: Task finished successfully -- `failed`: Encountered an error -- `stopped`: Manually stopped by user - -```typescript -// Update only the session status using PATCH -async function updateSessionStatus(agentId: string, sessionId: string, status: SessionStatus): Promise { - return await updateSession(agentId, sessionId, { status }) -} -``` - -## Message Streaming Integration - -### Sending Messages to Agents -The core interaction point is the message endpoint that accepts user messages and returns streamed responses: - -```typescript -async function sendMessageToAgent( - agentId: string, - sessionId: string, - message: CreateMessageRequest -): Promise { - - const response = await fetch(`/v1/agents/${agentId}/sessions/${sessionId}/messages`, { - method: 'POST', - headers: { - 'Authorization': `Bearer ${apiKey}`, - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - role: 'user', - type: 'message', - content: { - text: message.text, - // Include any additional context - } - }) - }) - - return response.body // Returns AI SDK streamText compatible stream -} -``` - -### Processing Streamed Responses -The response follows AI SDK's `streamText` format: - -```typescript -async function handleAgentResponse(stream: ReadableStream) { - const reader = stream.getReader() - const decoder = new TextDecoder() - - try { - while (true) { - const { done, value } = await reader.read() - - if (done) break - - const chunk = decoder.decode(value) - const lines = chunk.split('\n') - - for (const line of lines) { - if (line.startsWith('data: ')) { - const data = line.slice(6) - - if (data === '[DONE]') { - return // Stream completed - } - - try { - const parsed = JSON.parse(data) - - // Handle different stream events - switch (parsed.type) { - case 'text-delta': - updateUI(parsed.textDelta) - break - case 'tool-call': - handleToolCall(parsed.toolCall) - break - case 'tool-result': - handleToolResult(parsed.toolResult) - break - case 'finish': - handleFinish(parsed.finishReason) - break - } - } catch (parseError) { - console.error('Failed to parse stream data:', parseError) - } - } - } - } - } finally { - reader.releaseLock() - } -} -``` - -## UI Component Integration Patterns - -### Agent List Component -```typescript -function AgentList() { - const [agents, setAgents] = useState([]) - const [loading, setLoading] = useState(true) - - useEffect(() => { - async function loadAgents() { - try { - const result = await listAgents() - setAgents(result.data) - } catch (error) { - console.error('Failed to load agents:', error) - } finally { - setLoading(false) - } - } - - loadAgents() - }, []) - - const handleDeleteAgent = async (agentId: string) => { - try { - await fetch(`/v1/agents/${agentId}`, { - method: 'DELETE', - headers: { 'Authorization': `Bearer ${apiKey}` } - }) - - setAgents(agents.filter(agent => agent.id !== agentId)) - } catch (error) { - console.error('Failed to delete agent:', error) - } - } - - if (loading) return
Loading...
- - return ( -
- {agents.map(agent => ( - handleDeleteAgent(agent.id)} - /> - ))} -
- ) -} -``` - -### Agent Chat Component -```typescript -function AgentChat({ agentId }: { agentId: string }) { - const [session, setSession] = useState(null) - const [messages, setMessages] = useState([]) - const [inputMessage, setInputMessage] = useState('') - const [isStreaming, setIsStreaming] = useState(false) - - // Create session on component mount - useEffect(() => { - async function initSession() { - try { - const newSession = await createSession(agentId, { - user_goal: "General conversation" - }) - setSession(newSession) - - // Load existing messages - const messagesResult = await fetch(`/v1/agents/${agentId}/sessions/${newSession.id}/messages`, { - headers: { 'Authorization': `Bearer ${apiKey}` } - }).then(r => r.json()) - - setMessages(messagesResult.data) - } catch (error) { - console.error('Failed to initialize session:', error) - } - } - - initSession() - }, [agentId]) - - const sendMessage = async () => { - if (!session || !inputMessage.trim() || isStreaming) return - - setIsStreaming(true) - - try { - // Add user message to UI - const userMessage = { - role: 'user' as const, - content: { text: inputMessage }, - created_at: new Date().toISOString() - } - setMessages(prev => [...prev, userMessage as any]) - setInputMessage('') - - // Send to agent and handle streaming response - const stream = await sendMessageToAgent(agentId, session.id, { - text: inputMessage - }) - - let agentResponse = '' - await handleAgentResponse(stream, (delta: string) => { - agentResponse += delta - // Update UI with streaming text - setMessages(prev => { - const last = prev[prev.length - 1] - if (last?.role === 'agent') { - return [...prev.slice(0, -1), { ...last, content: { text: agentResponse } }] - } else { - return [...prev, { - role: 'agent', - content: { text: agentResponse }, - created_at: new Date().toISOString() - } as any] - } - }) - }) - - } catch (error) { - console.error('Failed to send message:', error) - } finally { - setIsStreaming(false) - } - } - - return ( -
-
- {messages.map((message, index) => ( -
-
{message.content.text}
-
- ))} -
- -
- setInputMessage(e.target.value)} - onKeyPress={(e) => e.key === 'Enter' && sendMessage()} - disabled={isStreaming} - placeholder="Type your message..." - /> - -
-
- ) -} -``` - -## Error Handling - -### API Error Response Format -```typescript -interface ApiError { - error: { - message: string - type: 'validation_error' | 'not_found' | 'internal_error' | 'authentication_error' - code?: string - details?: any[] - } -} -``` - -### Error Handling Pattern -```typescript -async function apiRequest(url: string, options: RequestInit = {}): Promise { - try { - const response = await fetch(url, { - ...options, - headers: { - 'Authorization': `Bearer ${apiKey}`, - 'Content-Type': 'application/json', - ...options.headers - } - }) - - if (!response.ok) { - const error: ApiError = await response.json() - throw new Error(`${error.error.type}: ${error.error.message}`) - } - - return await response.json() - } catch (error) { - console.error('API request failed:', error) - throw error - } -} -``` - -## Best Practices - -### 1. Agent Configuration -- **Minimal Setup**: Start with just `name`, `model`, and `instructions` -- **Gradual Enhancement**: Add `tools`, `mcps`, and `knowledges` as needed -- **Configuration Inheritance**: Sessions inherit agent settings but can override them - -### 2. Session Management -- **Single Goal Per Session**: Each session should have one clear `user_goal` -- **Status Tracking**: Always update session status appropriately -- **Resource Cleanup**: Delete completed/failed sessions to manage storage - -### 3. Message Streaming -- **Progressive Enhancement**: Show streaming text immediately for better UX -- **Error Recovery**: Handle stream interruptions gracefully -- **Tool Visualization**: Display tool calls and results appropriately - -### 4. Performance Considerations -- **Pagination**: Always use `limit` and `offset` for large lists -- **Caching**: Consider caching agent lists locally -- **Debouncing**: Debounce API calls for real-time updates - -### 5. User Experience -- **Loading States**: Show loading indicators during API calls -- **Error Messages**: Display user-friendly error messages -- **Optimistic Updates**: Update UI immediately, rollback on errors - -## Migration from Redux Implementation - -### Step 1: Remove Redux Dependencies -```typescript -// Before -import { useSelector, useDispatch } from 'react-redux' -import { createAgent, listAgents } from '../store/agents' - -// After -import { apiRequest } from '../services/api' -``` - -### Step 2: Replace Redux Hooks -```typescript -// Before -const agents = useSelector(state => state.agents.list) -const dispatch = useDispatch() - -// After -const [agents, setAgents] = useState([]) -``` - -### Step 3: Replace Action Dispatches -```typescript -// Before -dispatch(createAgent(agentData)) - -// After -const newAgent = await apiRequest('/v1/agents', { - method: 'POST', - body: JSON.stringify(agentData) -}) -setAgents(prev => [...prev, newAgent]) -``` - -## Conclusion - -This new API-based approach provides: -- **Better Performance**: Database storage with efficient queries -- **Real-time Streaming**: AI SDK compatible message streaming -- **Scalability**: Proper pagination and resource management -- **Flexibility**: Session-level configuration overrides -- **Reliability**: Proper error handling and status management - -The migration from Redux to direct API integration simplifies the data flow and provides better control over agent interactions. \ No newline at end of file diff --git a/plan.md b/plan.md deleted file mode 100644 index b4790f01b7..0000000000 --- a/plan.md +++ /dev/null @@ -1,241 +0,0 @@ -Overview - -Implement comprehensive CRUD APIs for agent, agentSession, and agentSessionLogs management -in Cherry Studio's API server using RESTful URL conventions. - -Architecture Overview - -1. Service Layer - -- Create AgentService class in src/main/services/agents/AgentService.ts - - Handles database operations using SQL queries from db.ts - - Manages SQLite database initialization and connections - - Provides business logic for agent operations - -2. API Routes - -- Create route files in src/main/apiServer/routes/: - - agents.ts - Agent CRUD endpoints - - sessions.ts - Session CRUD endpoints - - session-logs.ts - Session logs CRUD endpoints - -3. Database Integration - -- Use SQLite with @libsql/client (following MemoryService pattern) -- Database location: userData/agents.db -- Leverage existing SQL queries in src/main/services/agents/db.ts - -Implementation Steps - -Phase 1: Database Service Setup - -1. Create AgentService class with database initialization -2. Implement database connection management -3. Add database initialization to main process startup -4. Create helper methods for JSON field serialization/deserialization - -Phase 2: Agent CRUD Operations - -1. Implement service methods: - -- createAgent(agent: Omit) -- getAgent(id: string) -- listAgents(options?: { limit?: number, offset?: number }) -- updateAgent(id: string, updates: Partial) -- deleteAgent(id: string) - -2. Create API routes: - -- POST /v1/agents - Create agent -- GET /v1/agents - List all agents -- GET /v1/agents/:agentId - Get agent by ID -- PUT /v1/agents/:agentId - Update agent -- DELETE /v1/agents/:agentId - Delete agent - -Phase 3: Session CRUD Operations - -1. Implement service methods: - -- createSession(session: Omit) -- getSession(id: string) -- listSessions(agentId?: string, options?: { status?: SessionStatus, limit?: number, - offset?: number }) -- updateSession(id: string, updates: Partial) -- updateSessionStatus(id: string, status: SessionStatus) -- deleteSession(id: string) -- getSessionWithAgent(id: string) - Get session with merged agent configuration - -2. Create API routes (RESTful nested resources): - -- POST /v1/agents/:agentId/sessions - Create session for specific agent -- GET /v1/agents/:agentId/sessions - List sessions for specific agent -- GET /v1/agents/:agentId/sessions/:sessionId - Get specific session -- PUT /v1/agents/:agentId/sessions/:sessionId - Update session -- PATCH /v1/agents/:agentId/sessions/:sessionId/status - Update session status -- DELETE /v1/agents/:agentId/sessions/:sessionId - Delete session - -Additional convenience endpoints: - -- GET /v1/sessions - List all sessions (across all agents) -- GET /v1/sessions/:sessionId - Get session by ID (without agent context) - -Phase 4: Session Logs CRUD Operations - -1. Implement service methods: - -- createSessionLog(log: Omit) -- getSessionLog(id: number) -- listSessionLogs(sessionId: string, options?: { limit?: number, offset?: number }) -- updateSessionLog(id: number, updates: { content?: any, metadata?: any }) -- deleteSessionLog(id: number) -- getSessionLogTree(sessionId: string) - Get logs with parent-child relationships -- bulkCreateSessionLogs(logs: Array<...>) - Batch insert logs - -2. Create API routes (RESTful nested resources): - -- POST /v1/agents/:agentId/sessions/:sessionId/logs - Create log entry -- GET /v1/agents/:agentId/sessions/:sessionId/logs - List logs for session -- GET /v1/agents/:agentId/sessions/:sessionId/logs/:logId - Get specific log -- PUT /v1/agents/:agentId/sessions/:sessionId/logs/:logId - Update log -- DELETE /v1/agents/:agentId/sessions/:sessionId/logs/:logId - Delete log -- POST /v1/agents/:agentId/sessions/:sessionId/logs/bulk - Bulk create logs - -Additional convenience endpoints: - -- GET /v1/sessions/:sessionId/logs - Get logs without agent context -- GET /v1/session-logs/:logId - Get specific log by ID - -Phase 5: Route Organization - -1. Mount routes with proper nesting: - // In app.ts - apiRouter.use('/agents', agentsRoutes) - // agentsRoutes will handle: - // - /agents/_ - // - /agents/:agentId/sessions/_ - // - /agents/:agentId/sessions/:sessionId/logs/\* - -// Convenience routes -apiRouter.use('/sessions', sessionsRoutes) -apiRouter.use('/session-logs', sessionLogsRoutes) - -2. Use Express Router mergeParams for nested routes: - // In agents.ts - const sessionsRouter = express.Router({ mergeParams: true }) - router.use('/:agentId/sessions', sessionsRouter) - -Phase 6: OpenAPI Documentation - -1. Add Swagger schemas for new entities: - -- AgentEntity schema -- AgentSessionEntity schema -- SessionLogEntity schema -- Request/Response schemas - -2. Document all new endpoints with: - -- Clear path parameters (agentId, sessionId, logId) -- Request body schemas -- Response examples -- Error responses -- Proper grouping by resource - -Phase 7: Validation & Error Handling - -1. Add path parameter validation: - -- Validate agentId exists before processing session requests -- Validate sessionId belongs to agentId -- Validate logId belongs to sessionId - -2. Implement middleware for: - -- Request validation using express-validator -- Resource existence checks -- Permission validation (future consideration) -- Transaction support for complex operations - -Phase 8: Testing - -1. Unit tests for service methods -2. Integration tests for API endpoints -3. Test nested resource validation -4. Test cascading deletes -5. Test transaction rollbacks - -File Structure - -src/ -├── main/ -│ └── services/ -│ └── agents/ -│ ├── index.ts (existing) -│ ├── db.ts (existing) -│ └── AgentService.ts (new) -├── main/ -│ └── apiServer/ -│ └── routes/ -│ ├── agents.ts (new - includes nested routes) -│ ├── sessions.ts (new - convenience endpoints) -│ └── session-logs.ts (new - convenience endpoints) -└── renderer/ -└── src/ -└── types/ -└── agent.ts (existing) - -API Endpoint Summary - -Agent Endpoints - -- POST /v1/agents -- GET /v1/agents -- GET /v1/agents/:agentId -- PUT /v1/agents/:agentId -- DELETE /v1/agents/:agentId - -Session Endpoints (RESTful) - -- POST /v1/agents/:agentId/sessions -- GET /v1/agents/:agentId/sessions -- GET /v1/agents/:agentId/sessions/:sessionId -- PUT /v1/agents/:agentId/sessions/:sessionId -- PATCH /v1/agents/:agentId/sessions/:sessionId/status -- DELETE /v1/agents/:agentId/sessions/:sessionId - -Session Convenience Endpoints - -- GET /v1/sessions -- GET /v1/sessions/:sessionId - -Session Log Endpoints (RESTful) - -- POST /v1/agents/:agentId/sessions/:sessionId/logs -- GET /v1/agents/:agentId/sessions/:sessionId/logs -- GET /v1/agents/:agentId/sessions/:sessionId/logs/:logId -- PUT /v1/agents/:agentId/sessions/:sessionId/logs/:logId -- DELETE /v1/agents/:agentId/sessions/:sessionId/logs/:logId -- POST /v1/agents/:agentId/sessions/:sessionId/logs/bulk - -Session Log Convenience Endpoints - -- GET /v1/sessions/:sessionId/logs -- GET /v1/session-logs/:logId - -Key Considerations - -- Follow RESTful URL conventions with proper resource nesting -- Validate parent-child relationships in nested routes -- Use Express Router with mergeParams for nested routing -- Implement proper cascading deletes -- Add transaction support for data consistency -- Follow existing patterns from MemoryService -- Ensure backward compatibility -- Add rate limiting for write operations - -Dependencies - -- @libsql/client - SQLite database client -- express-validator - Request validation -- swagger-jsdoc - API documentation -- Existing types from @types/agent.ts