diff --git a/.github/workflows/auto-i18n.yml b/.github/workflows/auto-i18n.yml index 4cdd1481c..140d6208f 100644 --- a/.github/workflows/auto-i18n.yml +++ b/.github/workflows/auto-i18n.yml @@ -2,8 +2,8 @@ name: Auto I18N env: API_KEY: ${{ secrets.TRANSLATE_API_KEY }} - MODEL: ${{ vars.MODEL || 'deepseek/deepseek-v3.1'}} - BASE_URL: ${{ vars.BASE_URL || 'https://api.ppinfra.com/openai'}} + MODEL: ${{ vars.AUTO_I18N_MODEL || 'deepseek/deepseek-v3.1'}} + BASE_URL: ${{ vars.AUTO_I18N_BASE_URL || 'https://api.ppinfra.com/openai'}} on: pull_request: diff --git a/.github/workflows/nightly-build.yml b/.github/workflows/nightly-build.yml index 7f7100dc5..42d0d6615 100644 --- a/.github/workflows/nightly-build.yml +++ b/.github/workflows/nightly-build.yml @@ -99,9 +99,9 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} NODE_OPTIONS: --max-old-space-size=8192 MAIN_VITE_CHERRYAI_CLIENT_SECRET: ${{ secrets.MAIN_VITE_CHERRYAI_CLIENT_SECRET }} - MAIN_VITE_MINERU_API_KEY: ${{ vars.MAIN_VITE_MINERU_API_KEY }} - RENDERER_VITE_AIHUBMIX_SECRET: ${{ vars.RENDERER_VITE_AIHUBMIX_SECRET }} - RENDERER_VITE_PPIO_APP_SECRET: ${{ vars.RENDERER_VITE_PPIO_APP_SECRET }} + MAIN_VITE_MINERU_API_KEY: ${{ secrets.MAIN_VITE_MINERU_API_KEY }} + RENDERER_VITE_AIHUBMIX_SECRET: ${{ secrets.RENDERER_VITE_AIHUBMIX_SECRET }} + RENDERER_VITE_PPIO_APP_SECRET: ${{ secrets.RENDERER_VITE_PPIO_APP_SECRET }} - name: Build Mac if: matrix.os == 'macos-latest' @@ -110,15 +110,15 @@ jobs: env: CSC_LINK: ${{ secrets.CSC_LINK }} CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} - APPLE_ID: ${{ vars.APPLE_ID }} - APPLE_APP_SPECIFIC_PASSWORD: ${{ vars.APPLE_APP_SPECIFIC_PASSWORD }} - APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} NODE_OPTIONS: --max-old-space-size=8192 MAIN_VITE_CHERRYAI_CLIENT_SECRET: ${{ secrets.MAIN_VITE_CHERRYAI_CLIENT_SECRET }} - MAIN_VITE_MINERU_API_KEY: ${{ vars.MAIN_VITE_MINERU_API_KEY }} - RENDERER_VITE_AIHUBMIX_SECRET: ${{ vars.RENDERER_VITE_AIHUBMIX_SECRET }} - RENDERER_VITE_PPIO_APP_SECRET: ${{ vars.RENDERER_VITE_PPIO_APP_SECRET }} + MAIN_VITE_MINERU_API_KEY: ${{ secrets.MAIN_VITE_MINERU_API_KEY }} + RENDERER_VITE_AIHUBMIX_SECRET: ${{ secrets.RENDERER_VITE_AIHUBMIX_SECRET }} + RENDERER_VITE_PPIO_APP_SECRET: ${{ secrets.RENDERER_VITE_PPIO_APP_SECRET }} - name: Build Windows if: matrix.os == 'windows-latest' @@ -128,9 +128,9 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} NODE_OPTIONS: --max-old-space-size=8192 MAIN_VITE_CHERRYAI_CLIENT_SECRET: ${{ secrets.MAIN_VITE_CHERRYAI_CLIENT_SECRET }} - MAIN_VITE_MINERU_API_KEY: ${{ vars.MAIN_VITE_MINERU_API_KEY }} - RENDERER_VITE_AIHUBMIX_SECRET: ${{ vars.RENDERER_VITE_AIHUBMIX_SECRET }} - RENDERER_VITE_PPIO_APP_SECRET: ${{ vars.RENDERER_VITE_PPIO_APP_SECRET }} + MAIN_VITE_MINERU_API_KEY: ${{ secrets.MAIN_VITE_MINERU_API_KEY }} + RENDERER_VITE_AIHUBMIX_SECRET: ${{ secrets.RENDERER_VITE_AIHUBMIX_SECRET }} + RENDERER_VITE_PPIO_APP_SECRET: ${{ secrets.RENDERER_VITE_PPIO_APP_SECRET }} - name: Rename artifacts with nightly format shell: bash diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c4a772ad6..0ca1eb014 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -86,9 +86,9 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} NODE_OPTIONS: --max-old-space-size=8192 MAIN_VITE_CHERRYAI_CLIENT_SECRET: ${{ secrets.MAIN_VITE_CHERRYAI_CLIENT_SECRET }} - MAIN_VITE_MINERU_API_KEY: ${{ vars.MAIN_VITE_MINERU_API_KEY }} - RENDERER_VITE_AIHUBMIX_SECRET: ${{ vars.RENDERER_VITE_AIHUBMIX_SECRET }} - RENDERER_VITE_PPIO_APP_SECRET: ${{ vars.RENDERER_VITE_PPIO_APP_SECRET }} + MAIN_VITE_MINERU_API_KEY: ${{ secrets.MAIN_VITE_MINERU_API_KEY }} + RENDERER_VITE_AIHUBMIX_SECRET: ${{ secrets.RENDERER_VITE_AIHUBMIX_SECRET }} + RENDERER_VITE_PPIO_APP_SECRET: ${{ secrets.RENDERER_VITE_PPIO_APP_SECRET }} - name: Build Mac if: matrix.os == 'macos-latest' @@ -98,15 +98,15 @@ jobs: env: CSC_LINK: ${{ secrets.CSC_LINK }} CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} - APPLE_ID: ${{ vars.APPLE_ID }} - APPLE_APP_SPECIFIC_PASSWORD: ${{ vars.APPLE_APP_SPECIFIC_PASSWORD }} - APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} NODE_OPTIONS: --max-old-space-size=8192 MAIN_VITE_CHERRYAI_CLIENT_SECRET: ${{ secrets.MAIN_VITE_CHERRYAI_CLIENT_SECRET }} - MAIN_VITE_MINERU_API_KEY: ${{ vars.MAIN_VITE_MINERU_API_KEY }} - RENDERER_VITE_AIHUBMIX_SECRET: ${{ vars.RENDERER_VITE_AIHUBMIX_SECRET }} - RENDERER_VITE_PPIO_APP_SECRET: ${{ vars.RENDERER_VITE_PPIO_APP_SECRET }} + MAIN_VITE_MINERU_API_KEY: ${{ secrets.MAIN_VITE_MINERU_API_KEY }} + RENDERER_VITE_AIHUBMIX_SECRET: ${{ secrets.RENDERER_VITE_AIHUBMIX_SECRET }} + RENDERER_VITE_PPIO_APP_SECRET: ${{ secrets.RENDERER_VITE_PPIO_APP_SECRET }} - name: Build Windows if: matrix.os == 'windows-latest' @@ -116,9 +116,9 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} NODE_OPTIONS: --max-old-space-size=8192 MAIN_VITE_CHERRYAI_CLIENT_SECRET: ${{ secrets.MAIN_VITE_CHERRYAI_CLIENT_SECRET }} - MAIN_VITE_MINERU_API_KEY: ${{ vars.MAIN_VITE_MINERU_API_KEY }} - RENDERER_VITE_AIHUBMIX_SECRET: ${{ vars.RENDERER_VITE_AIHUBMIX_SECRET }} - RENDERER_VITE_PPIO_APP_SECRET: ${{ vars.RENDERER_VITE_PPIO_APP_SECRET }} + MAIN_VITE_MINERU_API_KEY: ${{ secrets.MAIN_VITE_MINERU_API_KEY }} + RENDERER_VITE_AIHUBMIX_SECRET: ${{ secrets.RENDERER_VITE_AIHUBMIX_SECRET }} + RENDERER_VITE_PPIO_APP_SECRET: ${{ secrets.RENDERER_VITE_PPIO_APP_SECRET }} - name: Release uses: ncipollo/release-action@v1 diff --git a/.yarn/patches/@ai-sdk-google-npm-2.0.14-376d8b03cc.patch b/.yarn/patches/@ai-sdk-google-npm-2.0.14-376d8b03cc.patch index 49bcec27d..f8868aa91 100644 --- a/.yarn/patches/@ai-sdk-google-npm-2.0.14-376d8b03cc.patch +++ b/.yarn/patches/@ai-sdk-google-npm-2.0.14-376d8b03cc.patch @@ -1,5 +1,5 @@ diff --git a/dist/index.mjs b/dist/index.mjs -index 110f37ec18c98b1d55ae2b73cc716194e6f9094d..3ea0fadd783f334db71266e45babdcce11076974 100644 +index 110f37ec18c98b1d55ae2b73cc716194e6f9094d..17e109b7778cbebb904f1919e768d21a2833d965 100644 --- a/dist/index.mjs +++ b/dist/index.mjs @@ -448,7 +448,7 @@ function convertToGoogleGenerativeAIMessages(prompt, options) { @@ -7,7 +7,7 @@ index 110f37ec18c98b1d55ae2b73cc716194e6f9094d..3ea0fadd783f334db71266e45babdcce // src/get-model-path.ts function getModelPath(modelId) { - return modelId.includes("/") ? modelId : `models/${modelId}`; -+ return `models/${modelId}`; ++ return modelId?.includes("models/") ? modelId : `models/${modelId}`; } // src/google-generative-ai-options.ts diff --git a/electron-builder.yml b/electron-builder.yml index 05fdc8b2f..56dba2795 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -125,59 +125,7 @@ afterSign: scripts/notarize.js artifactBuildCompleted: scripts/artifact-build-completed.js releaseInfo: releaseNotes: | - - 🚀 New Features: - - Refactored AI core engine for more efficient and stable content generation - - Added support for multiple AI model providers: CherryIN, AiOnly - - Added API server functionality for external application integration - - Added PaddleOCR document recognition for enhanced document processing - - Added Anthropic OAuth authentication support - - Added data storage space limit notifications - - Added font settings for global and code fonts customization - - Added auto-copy feature after translation completion - - Added keyboard shortcuts: rename topic, edit last message, etc. - - Added text attachment preview for viewing file contents in messages - - Added custom window control buttons (minimize, maximize, close) - - Support for Qwen long-text (qwen-long) and document analysis (qwen-doc) models with native file uploads - - Support for Qwen image recognition models (Qwen-Image) - - Added iFlow CLI support - - Converted knowledge base and web search to tool-calling approach for better flexibility - - 🎨 UI Improvements & Bug Fixes: - - Integrated HeroUI and Tailwind CSS framework - - Optimized message notification styles with unified toast component - - Moved free models to bottom with fixed position for easier access - - Refactored quick panel and input bar tools for smoother operation - - Optimized responsive design for navbar and sidebar - - Improved scrollbar component with horizontal scrolling support - - Fixed multiple translation issues: paste handling, file processing, state management - - Various UI optimizations and bug fixes - - 🚀 新功能: - - 重构 AI 核心引擎,提供更高效稳定的内容生成 - - 新增多个 AI 模型提供商支持:CherryIN、AiOnly - - 新增 API 服务器功能,支持外部应用集成 - - 新增 PaddleOCR 文档识别,增强文档处理能力 - - 新增 Anthropic OAuth 认证支持 - - 新增数据存储空间限制提醒 - - 新增字体设置,支持全局字体和代码字体自定义 - - 新增翻译完成后自动复制功能 - - 新增键盘快捷键:重命名主题、编辑最后一条消息等 - - 新增文本附件预览,可查看消息中的文件内容 - - 新增自定义窗口控制按钮(最小化、最大化、关闭) - - 支持通义千问长文本(qwen-long)和文档分析(qwen-doc)模型,原生文件上传 - - 支持通义千问图像识别模型(Qwen-Image) - - 新增 iFlow CLI 支持 - - 知识库和网页搜索转换为工具调用方式,提升灵活性 - - 🎨 界面改进与问题修复: - - 集成 HeroUI 和 Tailwind CSS 框架 - - 优化消息通知样式,统一 toast 组件 - - 免费模型移至底部固定位置,便于访问 - - 重构快捷面板和输入栏工具,操作更流畅 - - 优化导航栏和侧边栏响应式设计 - - 改进滚动条组件,支持水平滚动 - - 修复多个翻译问题:粘贴处理、文件处理、状态管理 - - 各种界面优化和问题修复 - - + Optimized note-taking feature, now able to quickly rename by modifying the title + Fixed issue where CherryAI free model could not be used + Fixed issue where VertexAI proxy address could not be called normally + Fixed issue where built-in tools from service providers could not be called normally diff --git a/electron.vite.config.ts b/electron.vite.config.ts index 6a1928672..b4914539c 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -34,6 +34,10 @@ export default defineConfig({ output: { manualChunks: undefined, // 彻底禁用代码分割 - 返回 null 强制单文件打包 inlineDynamicImports: true // 内联所有动态导入,这是关键配置 + }, + onwarn(warning, warn) { + if (warning.code === 'COMMONJS_VARIABLE_IN_ESM') return + warn(warning) } }, sourcemap: isDev @@ -112,6 +116,10 @@ export default defineConfig({ selectionToolbar: resolve(__dirname, 'src/renderer/selectionToolbar.html'), selectionAction: resolve(__dirname, 'src/renderer/selectionAction.html'), traceWindow: resolve(__dirname, 'src/renderer/traceWindow.html') + }, + onwarn(warning, warn) { + if (warning.code === 'COMMONJS_VARIABLE_IN_ESM') return + warn(warning) } } }, diff --git a/packages/aiCore/src/core/plugins/built-in/toolUsePlugin/promptToolUsePlugin.ts b/packages/aiCore/src/core/plugins/built-in/toolUsePlugin/promptToolUsePlugin.ts index fce028f5c..a2cc7d9af 100644 --- a/packages/aiCore/src/core/plugins/built-in/toolUsePlugin/promptToolUsePlugin.ts +++ b/packages/aiCore/src/core/plugins/built-in/toolUsePlugin/promptToolUsePlugin.ts @@ -261,22 +261,39 @@ export const createPromptToolUsePlugin = (config: PromptToolUseConfig = {}) => { return params } - context.mcpTools = params.tools + // 分离 provider-defined 和其他类型的工具 + const providerDefinedTools: ToolSet = {} + const promptTools: ToolSet = {} - // 构建系统提示符 + for (const [toolName, tool] of Object.entries(params.tools as ToolSet)) { + if (tool.type === 'provider-defined') { + // provider-defined 类型的工具保留在 tools 参数中 + providerDefinedTools[toolName] = tool + } else { + // 其他工具转换为 prompt 模式 + promptTools[toolName] = tool + } + } + + // 只有当有非 provider-defined 工具时才保存到 context + if (Object.keys(promptTools).length > 0) { + context.mcpTools = promptTools + } + + // 构建系统提示符(只包含非 provider-defined 工具) const userSystemPrompt = typeof params.system === 'string' ? params.system : '' - const systemPrompt = buildSystemPrompt(userSystemPrompt, params.tools) + const systemPrompt = buildSystemPrompt(userSystemPrompt, promptTools) let systemMessage: string | null = systemPrompt if (config.createSystemMessage) { // 🎯 如果用户提供了自定义处理函数,使用它 systemMessage = config.createSystemMessage(systemPrompt, params, context) } - // 移除 tools,改为 prompt 模式 + // 保留 provider-defined tools,移除其他 tools const transformedParams = { ...params, ...(systemMessage ? { system: systemMessage } : {}), - tools: undefined + tools: Object.keys(providerDefinedTools).length > 0 ? providerDefinedTools : undefined } context.originalParams = transformedParams return transformedParams @@ -285,8 +302,9 @@ export const createPromptToolUsePlugin = (config: PromptToolUseConfig = {}) => { let textBuffer = '' // let stepId = '' + // 如果没有需要 prompt 模式处理的工具,直接返回原始流 if (!context.mcpTools) { - throw new Error('No tools available') + return new TransformStream() } // 从 context 中获取或初始化 usage 累加器 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 4845ce4ac..95c2cdda2 100644 --- a/packages/aiCore/src/core/plugins/built-in/webSearchPlugin/helper.ts +++ b/packages/aiCore/src/core/plugins/built-in/webSearchPlugin/helper.ts @@ -1,6 +1,7 @@ import { anthropic } from '@ai-sdk/anthropic' import { google } from '@ai-sdk/google' import { openai } from '@ai-sdk/openai' +import { InferToolInput, InferToolOutput } from 'ai' import { ProviderOptionsMap } from '../../../options/types' import { OpenRouterSearchConfig } from './openrouter' @@ -58,24 +59,31 @@ export const DEFAULT_WEB_SEARCH_CONFIG: WebSearchPluginConfig = { export type WebSearchToolOutputSchema = { // Anthropic 工具 - 手动定义 - anthropicWebSearch: Array<{ - url: string - title: string - pageAge: string | null - encryptedContent: string - type: string - }> + anthropic: InferToolOutput> // OpenAI 工具 - 基于实际输出 - openaiWebSearch: { + // TODO: 上游定义不规范,是unknown + // openai: InferToolOutput> + openai: { + status: 'completed' | 'failed' + } + 'openai-chat': { status: 'completed' | 'failed' } - // Google 工具 - googleSearch: { + // TODO: 上游定义不规范,是unknown + // google: InferToolOutput> + google: { webSearchQueries?: string[] groundingChunks?: Array<{ web?: { uri: string; title: string } }> } } + +export type WebSearchToolInputSchema = { + anthropic: InferToolInput> + openai: InferToolInput> + google: InferToolInput> + 'openai-chat': InferToolInput> +} diff --git a/src/renderer/src/aiCore/chunk/AiSdkToChunkAdapter.ts b/src/renderer/src/aiCore/chunk/AiSdkToChunkAdapter.ts index 4562dcabd..1419bd62b 100644 --- a/src/renderer/src/aiCore/chunk/AiSdkToChunkAdapter.ts +++ b/src/renderer/src/aiCore/chunk/AiSdkToChunkAdapter.ts @@ -178,14 +178,13 @@ export class AiSdkToChunkAdapter { final.reasoningContent += chunk.text || '' this.onChunk({ type: ChunkType.THINKING_DELTA, - text: final.reasoningContent || '', - thinking_millsec: (chunk.providerMetadata?.metadata?.thinking_millsec as number) || 0 + text: final.reasoningContent || '' }) break case 'reasoning-end': this.onChunk({ type: ChunkType.THINKING_COMPLETE, - text: (chunk.providerMetadata?.metadata?.thinking_content as string) || final.reasoningContent + text: final.reasoningContent || '' }) final.reasoningContent = '' break diff --git a/src/renderer/src/aiCore/plugins/PluginBuilder.ts b/src/renderer/src/aiCore/plugins/PluginBuilder.ts index 7c5478eb7..7767564bd 100644 --- a/src/renderer/src/aiCore/plugins/PluginBuilder.ts +++ b/src/renderer/src/aiCore/plugins/PluginBuilder.ts @@ -5,7 +5,6 @@ import { getEnableDeveloperMode } from '@renderer/hooks/useSettings' import { Assistant } from '@renderer/types' import { AiSdkMiddlewareConfig } from '../middleware/AiSdkMiddlewareBuilder' -import reasoningTimePlugin from './reasoningTimePlugin' import { searchOrchestrationPlugin } from './searchOrchestrationPlugin' import { createTelemetryPlugin } from './telemetryPlugin' @@ -39,9 +38,9 @@ export function buildPlugins( } // 3. 推理模型时添加推理插件 - if (middlewareConfig.enableReasoning) { - plugins.push(reasoningTimePlugin) - } + // if (middlewareConfig.enableReasoning) { + // plugins.push(reasoningTimePlugin) + // } // 4. 启用Prompt工具调用时添加工具插件 if (middlewareConfig.isPromptToolUse) { diff --git a/src/renderer/src/assets/images/providers/longcat.png b/src/renderer/src/assets/images/providers/longcat.png new file mode 100644 index 000000000..146cf3ea7 Binary files /dev/null and b/src/renderer/src/assets/images/providers/longcat.png differ diff --git a/src/renderer/src/config/models/default.ts b/src/renderer/src/config/models/default.ts index 9fdced6a6..1858675ed 100644 --- a/src/renderer/src/config/models/default.ts +++ b/src/renderer/src/config/models/default.ts @@ -1804,5 +1804,19 @@ export const SYSTEM_MODELS: Record = provider: 'aionly', group: 'gemini' } + ], + longcat: [ + { + id: 'LongCat-Flash-Chat', + name: 'LongCat Flash Chat', + provider: 'longcat', + group: 'LongCat' + }, + { + id: 'LongCat-Flash-Thinking', + name: 'LongCat Flash Thinking', + provider: 'longcat', + group: 'LongCat' + } ] } diff --git a/src/renderer/src/config/providers.ts b/src/renderer/src/config/providers.ts index 20be65ba7..c27c7403f 100644 --- a/src/renderer/src/config/providers.ts +++ b/src/renderer/src/config/providers.ts @@ -27,6 +27,7 @@ import InfiniProviderLogo from '@renderer/assets/images/providers/infini.png' import JinaProviderLogo from '@renderer/assets/images/providers/jina.png' import LanyunProviderLogo from '@renderer/assets/images/providers/lanyun.png' import LMStudioProviderLogo from '@renderer/assets/images/providers/lmstudio.png' +import LongCatProviderLogo from '@renderer/assets/images/providers/longcat.png' import MinimaxProviderLogo from '@renderer/assets/images/providers/minimax.png' import MistralProviderLogo from '@renderer/assets/images/providers/mistral.png' import ModelScopeProviderLogo from '@renderer/assets/images/providers/modelscope.png' @@ -630,6 +631,16 @@ export const SYSTEM_PROVIDERS_CONFIG: Record = models: SYSTEM_MODELS['poe'], isSystem: true, enabled: false + }, + longcat: { + id: 'longcat', + name: 'LongCat', + type: 'openai', + apiKey: '', + apiHost: 'https://api.longcat.chat/openai', + models: SYSTEM_MODELS.longcat, + isSystem: true, + enabled: false } } as const @@ -692,7 +703,8 @@ export const PROVIDER_LOGO_MAP: AtLeast = { 'new-api': NewAPIProviderLogo, 'aws-bedrock': AwsProviderLogo, poe: 'poe', // use svg icon component - aionly: AiOnlyProviderLogo + aionly: AiOnlyProviderLogo, + longcat: LongCatProviderLogo } as const export function getProviderLogo(providerId: string) { @@ -1298,6 +1310,17 @@ export const PROVIDER_URLS: Record = { docs: 'https://www.aiionly.com/document', models: 'https://www.aiionly.com' } + }, + longcat: { + api: { + url: 'https://api.longcat.chat/openai' + }, + websites: { + official: 'https://longcat.chat', + apiKey: 'https://longcat.chat/platform/api_keys', + docs: 'https://longcat.chat/platform/docs/zh/', + models: 'https://longcat.chat/platform/docs/zh/APIDocs.html' + } } } diff --git a/src/renderer/src/pages/home/Messages/Blocks/ThinkingBlock.tsx b/src/renderer/src/pages/home/Messages/Blocks/ThinkingBlock.tsx index 98cad2a8c..109562f7d 100644 --- a/src/renderer/src/pages/home/Messages/Blocks/ThinkingBlock.tsx +++ b/src/renderer/src/pages/home/Messages/Blocks/ThinkingBlock.tsx @@ -5,7 +5,7 @@ import { useSettings } from '@renderer/hooks/useSettings' import { useTemporaryValue } from '@renderer/hooks/useTemporaryValue' import { MessageBlockStatus, type ThinkingMessageBlock } from '@renderer/types/newMessage' import { Collapse, message as antdMessage, Tooltip } from 'antd' -import { memo, useCallback, useEffect, useMemo, useState } from 'react' +import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' @@ -105,30 +105,37 @@ const ThinkingBlock: React.FC = ({ block }) => { const ThinkingTimeSeconds = memo( ({ blockThinkingTime, isThinking }: { blockThinkingTime: number; isThinking: boolean }) => { const { t } = useTranslation() - // const [thinkingTime, setThinkingTime] = useState(blockThinkingTime || 0) + const [displayTime, setDisplayTime] = useState(blockThinkingTime) - // FIXME: 这里统计的和请求处统计的有一定误差 - // useEffect(() => { - // let timer: NodeJS.Timeout | null = null - // if (isThinking) { - // timer = setInterval(() => { - // setThinkingTime((prev) => prev + 100) - // }, 100) - // } else if (timer) { - // // 立即清除计时器 - // clearInterval(timer) - // timer = null - // } + const timer = useRef(null) - // return () => { - // if (timer) { - // clearInterval(timer) - // timer = null - // } - // } - // }, [isThinking]) + useEffect(() => { + if (isThinking) { + if (!timer.current) { + timer.current = setInterval(() => { + setDisplayTime((prev) => prev + 100) + }, 100) + } + } else { + if (timer.current) { + clearInterval(timer.current) + timer.current = null + } + setDisplayTime(blockThinkingTime) + } - const thinkingTimeSeconds = useMemo(() => (blockThinkingTime / 1000).toFixed(1), [blockThinkingTime]) + return () => { + if (timer.current) { + clearInterval(timer.current) + timer.current = null + } + } + }, [isThinking, blockThinkingTime]) + + const thinkingTimeSeconds = useMemo( + () => ((displayTime < 1000 ? 100 : displayTime) / 1000).toFixed(1), + [displayTime] + ) return isThinking ? t('chat.thinking', { diff --git a/src/renderer/src/pages/home/Messages/Blocks/__tests__/ThinkingBlock.test.tsx b/src/renderer/src/pages/home/Messages/Blocks/__tests__/ThinkingBlock.test.tsx index 8db122d94..d57340822 100644 --- a/src/renderer/src/pages/home/Messages/Blocks/__tests__/ThinkingBlock.test.tsx +++ b/src/renderer/src/pages/home/Messages/Blocks/__tests__/ThinkingBlock.test.tsx @@ -235,13 +235,12 @@ describe('ThinkingBlock', () => { renderThinkingBlock(thinkingBlock) const activeTimeText = getThinkingTimeText() - expect(activeTimeText).toHaveTextContent('1.0s') expect(activeTimeText).toHaveTextContent('Thinking...') }) it('should handle extreme thinking times correctly', () => { const testCases = [ - { thinking_millsec: 0, expectedTime: '0.0s' }, + { thinking_millsec: 0, expectedTime: '0.1s' }, // New logic: values < 1000ms display as 0.1s { thinking_millsec: 86400000, expectedTime: '86400.0s' }, // 1 day { thinking_millsec: 259200000, expectedTime: '259200.0s' } // 3 days ] diff --git a/src/renderer/src/pages/home/Messages/Tools/MessageMcpTool.tsx b/src/renderer/src/pages/home/Messages/Tools/MessageMcpTool.tsx index b70c11798..71d3c68b8 100644 --- a/src/renderer/src/pages/home/Messages/Tools/MessageMcpTool.tsx +++ b/src/renderer/src/pages/home/Messages/Tools/MessageMcpTool.tsx @@ -58,7 +58,7 @@ const MessageMcpTool: FC = ({ block }) => { const [progress, setProgress] = useState(0) const { setTimeoutTimer } = useTimer() - const toolResponse = block.metadata?.rawMcpToolResponse + const toolResponse = block.metadata?.rawMcpToolResponse as MCPToolResponse const { id, tool, status, response } = toolResponse as MCPToolResponse const isPending = status === 'pending' diff --git a/src/renderer/src/pages/home/Messages/Tools/MessageTool.tsx b/src/renderer/src/pages/home/Messages/Tools/MessageTool.tsx index f208ecdfc..88cf2dab5 100644 --- a/src/renderer/src/pages/home/Messages/Tools/MessageTool.tsx +++ b/src/renderer/src/pages/home/Messages/Tools/MessageTool.tsx @@ -37,12 +37,13 @@ const isAgentTool = (toolName: string) => { const ChooseTool = (toolResponse: NormalToolResponse): React.ReactNode | null => { let toolName = toolResponse.tool.name + const toolType = toolResponse.tool.type if (toolName.startsWith(prefix)) { toolName = toolName.slice(prefix.length) switch (toolName) { case 'web_search': case 'web_search_preview': - return + return toolType === 'provider' ? null : case 'knowledge_search': return case 'memory_search': @@ -58,7 +59,7 @@ const ChooseTool = (toolResponse: NormalToolResponse): React.ReactNode | null => export default function MessageTool({ block }: Props) { // FIXME: 语义错误,这里已经不是 MCP tool 了,更改rawMcpToolResponse需要改用户数据, 所以暂时保留 - const toolResponse = block.metadata?.rawMcpToolResponse + const toolResponse = block.metadata?.rawMcpToolResponse as NormalToolResponse if (!toolResponse) return null diff --git a/src/renderer/src/services/messageStreaming/callbacks/thinkingCallbacks.ts b/src/renderer/src/services/messageStreaming/callbacks/thinkingCallbacks.ts index 4d717c6c6..605259b64 100644 --- a/src/renderer/src/services/messageStreaming/callbacks/thinkingCallbacks.ts +++ b/src/renderer/src/services/messageStreaming/callbacks/thinkingCallbacks.ts @@ -15,7 +15,7 @@ export const createThinkingCallbacks = (deps: ThinkingCallbacksDependencies) => // 内部维护的状态 let thinkingBlockId: string | null = null - let _thinking_millsec = 0 + let thinking_millsec_now: number = 0 return { onThinkingStart: async () => { @@ -24,27 +24,27 @@ export const createThinkingCallbacks = (deps: ThinkingCallbacksDependencies) => type: MessageBlockType.THINKING, content: '', status: MessageBlockStatus.STREAMING, - thinking_millsec: _thinking_millsec + thinking_millsec: 0 } thinkingBlockId = blockManager.initialPlaceholderBlockId! blockManager.smartBlockUpdate(thinkingBlockId, changes, MessageBlockType.THINKING, true) } else if (!thinkingBlockId) { const newBlock = createThinkingBlock(assistantMsgId, '', { status: MessageBlockStatus.STREAMING, - thinking_millsec: _thinking_millsec + thinking_millsec: 0 }) thinkingBlockId = newBlock.id await blockManager.handleBlockTransition(newBlock, MessageBlockType.THINKING) } + thinking_millsec_now = performance.now() }, - onThinkingChunk: async (text: string, thinking_millsec?: number) => { - _thinking_millsec = thinking_millsec || 0 + onThinkingChunk: async (text: string) => { if (thinkingBlockId) { const blockChanges: Partial = { content: text, - status: MessageBlockStatus.STREAMING, - thinking_millsec: _thinking_millsec + status: MessageBlockStatus.STREAMING + // thinking_millsec: performance.now() - thinking_millsec_now } blockManager.smartBlockUpdate(thinkingBlockId, blockChanges, MessageBlockType.THINKING) } @@ -52,14 +52,15 @@ export const createThinkingCallbacks = (deps: ThinkingCallbacksDependencies) => onThinkingComplete: (finalText: string) => { if (thinkingBlockId) { + const now = performance.now() const changes: Partial = { content: finalText, status: MessageBlockStatus.SUCCESS, - thinking_millsec: _thinking_millsec + thinking_millsec: now - thinking_millsec_now } blockManager.smartBlockUpdate(thinkingBlockId, changes, MessageBlockType.THINKING, true) thinkingBlockId = null - _thinking_millsec = 0 + thinking_millsec_now = 0 } else { logger.warn( `[onThinkingComplete] Received thinking.complete but last block was not THINKING (was ${blockManager.lastBlockType}) or lastBlockId is null.` diff --git a/src/renderer/src/store/migrate.ts b/src/renderer/src/store/migrate.ts index 4986b911f..acf4112bf 100644 --- a/src/renderer/src/store/migrate.ts +++ b/src/renderer/src/store/migrate.ts @@ -2570,6 +2570,7 @@ const migrateConfig = { '158': (state: RootState) => { try { state.llm.providers = state.llm.providers.filter((provider) => provider.id !== 'cherryin') + addProvider(state, 'longcat') return state } catch (error) { logger.error('migrate 158 error', error as Error) diff --git a/src/renderer/src/store/thunk/__tests__/streamCallback.integration.test.ts b/src/renderer/src/store/thunk/__tests__/streamCallback.integration.test.ts index e8c113d62..49c71aea5 100644 --- a/src/renderer/src/store/thunk/__tests__/streamCallback.integration.test.ts +++ b/src/renderer/src/store/thunk/__tests__/streamCallback.integration.test.ts @@ -425,7 +425,10 @@ describe('streamCallback Integration Tests', () => { expect(thinkingBlock).toBeDefined() expect(thinkingBlock?.content).toBe('Final thoughts') expect(thinkingBlock?.status).toBe(MessageBlockStatus.SUCCESS) - expect((thinkingBlock as any)?.thinking_millsec).toBe(3000) + // thinking_millsec 现在是本地计算的,只验证它存在且是一个合理的数字 + expect((thinkingBlock as any)?.thinking_millsec).toBeDefined() + expect(typeof (thinkingBlock as any)?.thinking_millsec).toBe('number') + expect((thinkingBlock as any)?.thinking_millsec).toBeGreaterThanOrEqual(0) }) it('should handle tool call flow', async () => { diff --git a/src/renderer/src/types/index.ts b/src/renderer/src/types/index.ts index dff9563b7..ae1e6edb6 100644 --- a/src/renderer/src/types/index.ts +++ b/src/renderer/src/types/index.ts @@ -334,7 +334,8 @@ export const SystemProviderIds = { voyageai: 'voyageai', 'aws-bedrock': 'aws-bedrock', poe: 'poe', - aionly: 'aionly' + aionly: 'aionly', + longcat: 'longcat' } as const export type SystemProviderId = keyof typeof SystemProviderIds diff --git a/yarn.lock b/yarn.lock index 2afcbd47c..e2a2ff35d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -169,13 +169,13 @@ __metadata: "@ai-sdk/google@patch:@ai-sdk/google@npm%3A2.0.14#~/.yarn/patches/@ai-sdk-google-npm-2.0.14-376d8b03cc.patch": version: 2.0.14 - resolution: "@ai-sdk/google@patch:@ai-sdk/google@npm%3A2.0.14#~/.yarn/patches/@ai-sdk-google-npm-2.0.14-376d8b03cc.patch::version=2.0.14&hash=c6aff2" + resolution: "@ai-sdk/google@patch:@ai-sdk/google@npm%3A2.0.14#~/.yarn/patches/@ai-sdk-google-npm-2.0.14-376d8b03cc.patch::version=2.0.14&hash=351f1a" dependencies: "@ai-sdk/provider": "npm:2.0.0" "@ai-sdk/provider-utils": "npm:3.0.9" peerDependencies: zod: ^3.25.76 || ^4 - checksum: 10c0/2a0a09debab8de0603243503ff5044bd3fff87d6c5de2d76d43839fa459cc85d5412b59ec63d0dcf1a6d6cab02882eb3c69f0f155129d0fc153bcde4deecbd32 + checksum: 10c0/1ed5a0732a82b981d51f63c6241ed8ee94d5c29a842764db770305cfc2f49ab6e528cac438b5357fc7b02194104c7b76d4390a1dc1d019ace9c174b0849e0da6 languageName: node linkType: hard