Compare commits

..

22 Commits

Author SHA1 Message Date
MyPrototypeWhat
bd6d6bd56e refactor: update ClaudeCodeService initialization to remove config parameter
- Modified the initialization of the Claude code provider in ClaudeCodeService to no longer accept a configuration parameter, simplifying the setup process.
- This change enhances clarity and reduces potential configuration errors during provider instantiation.
2025-09-04 17:37:10 +08:00
MyPrototypeWhat
7a23386de4 feat: enhance AI core with Claude code integration and new provider support
- Added ClaudeCodeService for managing Claude code interactions via HTTP.
- Updated IPC channels to include new provider functionalities, enabling communication with the Claude code service.
- Enhanced electron configuration to support new AI core paths and dependencies.
- Updated package.json to include new dependencies for AI SDK and express.
- Refactored tsconfig to include paths for the new AI core modules, improving module resolution.

This update improves the integration of AI capabilities and enhances the overall functionality of the application.
2025-09-04 17:21:50 +08:00
MyPrototypeWhat
a227f6dcb9 Feat/aisdk package (#7404)
* feat: enhance AI SDK middleware integration and support

- Added AiSdkMiddlewareBuilder for dynamic middleware construction based on various conditions.
- Updated ModernAiProvider to utilize new middleware configuration, improving flexibility in handling completions.
- Refactored ApiService to pass middleware configuration during AI completions, enabling better control over processing.
- Introduced new README documentation for the middleware builder, outlining usage and supported conditions.

* feat: enhance AI core functionality with smoothStream integration

- Added smoothStream to the middleware exports in index.ts for improved streaming capabilities.
- Updated PluginEnabledAiClient to conditionally apply middlewares, removing the default simulateStreamingMiddleware.
- Modified ModernAiProvider to utilize smoothStream in streamText, enhancing text processing with configurable chunking and delay options.

* feat: enhance AI SDK documentation and client functionality

- Added detailed usage examples for the native provider registry in the README.md, demonstrating how to create and utilize custom provider registries.
- Updated ApiClientFactory to enforce type safety for model instances.
- Refactored PluginEnabledAiClient methods to support both built-in logic and custom registry usage for text and object generation, improving flexibility and usability.

* refactor: update AiSdkToChunkAdapter and middleware for improved chunk handling

- Modified convertAndEmitChunk method to handle new chunk types and streamline processing.
- Adjusted thinkingTimeMiddleware to remove unnecessary parameters and enhance clarity.
- Updated middleware integration in AiSdkMiddlewareBuilder for better middleware management.

* feat: add Cherry Studio transformation and settings plugins

- Introduced cherryStudioTransformPlugin for converting Cherry Studio messages to AI SDK format, enhancing compatibility.
- Added cherryStudioSettingsPlugin to manage Assistant settings like temperature and TopP.
- Implemented createCherryStudioContext function for preparing context metadata for Cherry Studio calls.

* fix: refine experimental_transform handling and improve chunking logic

- Updated PluginEnabledAiClient to streamline the handling of experimental_transform parameters.
- Adjusted ModernAiProvider's smoothStream configuration for better chunking of text, enhancing processing efficiency.
- Re-enabled block updates in messageThunk for improved state management.

* refactor: update ApiClientFactory and index_new for improved type handling and provider mapping

- Changed the type of options in ClientConfig to 'any' for flexibility.
- Overloaded createImageClient method to support different provider settings.
- Added vertexai mapping to the provider type mapping in index_new.ts for enhanced compatibility.

* feat: enhance AI SDK documentation and client functionality

- Added detailed usage examples for the native provider registry in the README.md, demonstrating how to create and utilize custom provider registries.
- Updated ApiClientFactory to enforce type safety for model instances.
- Refactored PluginEnabledAiClient methods to support both built-in logic and custom registry usage for text and object generation, improving flexibility and usability.

* feat: add openai-compatible provider and enhance provider configuration

- Introduced the @ai-sdk/openai-compatible package to support compatibility with OpenAI.
- Added a new ProviderConfigFactory and ProviderConfigBuilder for streamlined provider configuration.
- Updated the provider registry to include the new Google Vertex AI import path.
- Enhanced the index.ts to export new provider configuration utilities for better type safety and usability.
- Refactored ApiService and middleware to integrate the new provider configurations effectively.

* feat: enhance Vertex AI provider integration and configuration

- Added support for Google Vertex AI credentials in the provider configuration.
- Refactored the VertexAPIClient to handle both standard and VertexProvider types.
- Implemented utility functions to check Vertex AI configuration completeness and create VertexProvider instances.
- Updated provider mapping in index_new.ts to ensure proper handling of Vertex AI settings.

* feat: enhance provider options and examples for AI SDK

- Introduced new utility functions for creating and merging provider options, improving type safety and usability.
- Added comprehensive examples for OpenAI, Anthropic, Google, and generic provider options to demonstrate usage.
- Refactored existing code to streamline provider configuration and enhance clarity in the options management.
- Updated the PluginEnabledAiClient to simplify the handling of model parameters and improve overall functionality.

* feat: add patch for Google Vertex AI and enhance private key handling

- Introduced a patch for the @ai-sdk/google-vertex package to improve URL handling based on region.
- Added a new utility function to format private keys, ensuring correct PEM structure and validation.
- Updated the ProviderConfigBuilder to utilize the new private key formatting function for Google credentials.
- Created a pnpm workspace configuration to manage patched dependencies effectively.

* feat: add OpenAI Compatible provider and enhance provider configuration

- Introduced a new OpenAI Compatible provider to the AiProviderRegistry, allowing for integration with the @ai-sdk/openai-compatible package.
- Updated provider configuration logic to support the new provider, including adjustments to API host formatting and options management.
- Refactored middleware to streamline handling of OpenAI-specific configurations.

* fix: enhance anthropic provider configuration and middleware handling

- Updated providerToAiSdkConfig to support both OpenAI and Anthropic providers, improving flexibility in API host formatting.
- Refactored thinkingTimeMiddleware to ensure all chunks are correctly enqueued, enhancing middleware functionality.
- Corrected parameter naming in getAnthropicReasoningParams for consistency and clarity in configuration.

* feat: enhance AI SDK chunk handling and tool call processing

- Introduced ToolCallChunkHandler for managing tool call events and results, improving the handling of tool interactions.
- Updated AiSdkToChunkAdapter to utilize the new handler, streamlining the processing of tool call chunks.
- Refactored transformParameters to support dynamic tool integration and improved parameter handling.
- Adjusted provider mapping in factory.ts to include new provider types, enhancing compatibility with various AI services.
- Removed obsolete cherryStudioTransformPlugin to clean up the codebase and focus on more relevant functionality.

* feat: enhance provider ID resolution in AI SDK

- Updated getAiSdkProviderId function to include mapping for provider types, improving compatibility with third-party SDKs.
- Refined return logic to ensure correct provider ID resolution, enhancing overall functionality and support for various providers.

* refactor: restructure AI Core architecture and enhance client functionality

- Updated the AI Core documentation to reflect the new architecture and design principles, emphasizing modularity and type safety.
- Refactored the client structure by removing obsolete files and consolidating client creation logic into a more streamlined format.
- Introduced a new core module for managing execution and middleware, improving the overall organization of the codebase.
- Enhanced the orchestration layer to provide a clearer API for users, integrating the creation and execution processes more effectively.
- Added comprehensive type definitions and utility functions for better type safety and usability across the SDK.

* refactor: simplify AI Core architecture and enhance runtime execution

- Restructured the AI Core documentation to reflect a simplified two-layer architecture, focusing on clear responsibilities between models and runtime layers.
- Removed the orchestration layer and consolidated its functionality into the runtime layer, streamlining the API for users.
- Introduced a new runtime executor for managing plugin-enhanced AI calls, improving the handling of execution and middleware.
- Updated the core modules to enhance type safety and usability, including comprehensive type definitions for model creation and execution configurations.
- Removed obsolete files and refactored existing code to improve organization and maintainability across the SDK.

* feat: enhance AI Core runtime with advanced model handling and middleware support

- Introduced new high-level APIs for model creation and configuration, improving usability for advanced users.
- Enhanced the RuntimeExecutor to support both direct model usage and model ID resolution, allowing for more flexible execution options.
- Updated existing methods to accept middleware configurations, streamlining the integration of custom processing logic.
- Refactored the plugin system to better accommodate middleware, enhancing the overall extensibility of the AI Core.
- Improved documentation to reflect the new capabilities and usage patterns for the runtime APIs.

* feat: enhance plugin system with new reasoning and text plugins

- Introduced `reasonPlugin` and `textPlugin` to improve chunk processing and handling of reasoning content.
- Updated `transformStream` method signatures for better type safety and usability.
- Enhanced `ThinkingTimeMiddleware` to accurately track thinking time using `performance.now()`.
- Refactored `ThinkingBlock` component to utilize block thinking time directly, improving performance and clarity.
- Added logging for middleware builder to assist in debugging and monitoring middleware configurations.

* refactor: update RuntimeExecutor and introduce MCP Prompt Plugin

- Changed `pluginClient` to `pluginEngine` in `RuntimeExecutor` for clarity and consistency.
- Updated method calls in `RuntimeExecutor` to use the new `pluginEngine`.
- Enhanced `AiSdkMiddlewareBuilder` to include `mcpTools` in the middleware configuration.
- Added `MCPPromptPlugin` to support tool calls within prompts, enabling recursive processing and improved handling of tool interactions.
- Updated `ApiService` to pass `mcpTools` during chat completion requests, enhancing integration with the new plugin system.

* feat: enhance MCP Prompt plugin and recursive call capabilities

- Updated `tsconfig.web.json` to support wildcard imports for `@cherrystudio/ai-core`.
- Enhanced `package.json` to include type definitions and imports for built-in plugins.
- Introduced recursive call functionality in `PluginManager` and `PluginEngine`, allowing for improved handling of tool interactions.
- Added `MCPPromptPlugin` to facilitate tool calls within prompts, enabling recursive processing of tool results.
- Refactored `transformStream` methods across plugins to accommodate new parameters and improve type safety.

* <type>: <subject>
<body>
<footer>
用來簡要描述影響本次變動,概述即可

* feat: enhance MCP Prompt plugin with recursive call support and context handling

- Updated `AiRequestContext` to enforce `recursiveCall` and added `isRecursiveCall` for better state management.
- Modified `createContext` to initialize `recursiveCall` with a placeholder function.
- Enhanced `MCPPromptPlugin` to utilize a custom `createSystemMessage` function for improved message handling during recursive calls.
- Refactored `PluginEngine` to manage recursive call states, ensuring proper execution flow and context integrity.

* feat: update package dependencies and introduce new patches for AI SDK tools

- Added patches for `@ai-sdk/google-vertex` and `@ai-sdk/openai-compatible` to enhance functionality and fix issues.
- Updated `package.json` to reflect new dependency versions and patch paths.
- Refactored `transformParameters` and `ApiService` to support new tool configurations and improve parameter handling.
- Introduced utility functions for setting up tools and managing options, enhancing the overall integration of tools within the AI SDK.

* refactor: disable console logging in MCP Prompt plugin for cleaner output

- Commented out console log statements in the `createMCPPromptPlugin` to reduce noise during execution.
- Maintained the structure and functionality of the plugin while improving readability and performance.

* feat: enhance ModernAiProvider with new reasoning plugins and dynamic middleware construction

- Introduced `reasoningTimePlugin` and `smoothReasoningPlugin` to improve reasoning content handling and processing.
- Refactored `ModernAiProvider` to dynamically build plugin arrays based on middleware configuration, enhancing flexibility.
- Removed the obsolete `ThinkingTimeMiddleware` to streamline middleware management.
- Updated `buildAiSdkMiddlewares` to reflect changes in middleware handling and improve clarity in the configuration process.
- Enhanced logging for better visibility into plugin and middleware configurations during execution.

* feat: introduce MCP Prompt Plugin and refactor built-in plugin structure

- Added `mcpPromptPlugin.ts` to encapsulate MCP Prompt functionality, providing a structured approach for tool calls within prompts.
- Updated `index.ts` to reference the new `mcpPromptPlugin`, enhancing modularity and clarity in the built-in plugins.
- Removed the outdated `example-plugins.ts` file to streamline the plugin directory and focus on essential components.

* refactor: rename and restructure message handling in Conversation and Orchestrate services

- Renamed `prepareMessagesForLlm` to `prepareMessagesForModel` in `ConversationService` for clarity.
- Updated `OrchestrationService` to use the new method name and introduced a new function `transformMessagesAndFetch` for improved message processing.
- Adjusted imports in `messageThunk` to reflect the changes in the orchestration service, enhancing code readability and maintainability.

* refactor: streamline error handling and logging in ModernAiProvider

- Commented out the try-catch block in the `ModernAiProvider` class to simplify the code structure.
- Enhanced readability by removing unnecessary error logging while maintaining the core functionality of the AI processing flow.
- Updated `messageThunk` to incorporate an abort controller for improved request management during message processing.

* fix: update reasoningTimePlugin and smoothReasoningPlugin for improved performance tracking

- Changed the invocation of `reasoningTimePlugin` to a direct reference in `ModernAiProvider`.
- Initialized `thinkingStartTime` with `performance.now()` in `reasoningTimePlugin` for accurate timing.
- Removed `thinking_millsec` from the enqueued chunks in `smoothReasoningPlugin` to streamline data handling.
- Added console logging for performance tracking in `reasoningTimePlugin` to aid in debugging.

* feat: extend buildStreamTextParams to include capabilities for enhanced AI functionality

- Updated the return type of `buildStreamTextParams` to include `capabilities` for reasoning, web search, and image generation.
- Modified `fetchChatCompletion` to utilize the new capabilities structure, improving middleware configuration based on model capabilities.

* refactor: simplify provider validation and enhance plugin configuration

- Commented out the provider support check in `RuntimeExecutor` to streamline initialization.
- Updated `providerToAiSdkConfig` to utilize `AiCore.isSupported` for improved provider validation.
- Enhanced middleware configuration in `ModernAiProvider` to ensure tools are only added when enabled and available.
- Added comments in `transformParameters` for clarity on parameter handling and plugin activation.

* refactor: remove OpenRouter provider support and streamline reasoning logic

- Commented out the OpenRouter provider in `registry.ts` and related configurations due to excessive bugs.
- Simplified reasoning logic in `transformParameters.ts` and `options.ts` by removing unnecessary checks for `enableReasoning`.
- Enhanced logging in `transformParameters.ts` to provide better insights into reasoning capabilities.
- Updated `getReasoningEffort` to handle cases where reasoning effort is not defined, improving model compatibility.

* chore: update OpenRouter provider to version 0.7.2 and add support functions

- Updated the OpenRouter provider dependency in `package.json` and `yarn.lock` to version 0.7.2.
- Added a new function `createOpenRouterOptions` in `factory.ts` for creating OpenRouter provider options.
- Updated type definitions in `types.ts` and `registry.ts` to include OpenRouter provider settings, enhancing provider management.

* refactor: streamline reasoning plugins and remove unused components

- Removed the `reasoningTimePlugin` and `mcpPromptPlugin` to simplify the plugin architecture.
- Updated the `smoothReasoningPlugin` to enhance its functionality and reduce delay in processing.
- Adjusted the `textPlugin` to align with the new delay settings for smoother output.
- Modified the `ModernAiProvider` to utilize the updated `smoothReasoningPlugin` without the removed plugins.

* refactor: update reasoning plugins and enhance performance

- Replaced `smoothReasoningPlugin` with `reasoningTimePlugin` to improve reasoning time tracking.
- Commented out the unused `textPlugin` in the plugin list for better clarity.
- Adjusted delay settings in both `smoothReasoningPlugin` and `textPlugin` for optimized processing.
- Enhanced logging in reasoning plugins for better debugging and performance insights.

* feat: implement useSmoothStream hook for dynamic text rendering

- Added a new custom hook `useSmoothStream` to manage smooth text streaming with adjustable delays.
- Integrated the `useSmoothStream` hook into the `Markdown` component to enhance content display during streaming.
- Improved state management for displayed content and stream completion status in the `Markdown` component.

* feat: enhance OpenAI provider handling and add providerParams utility module

- Updated the `createBaseModel` function to handle OpenAI provider responses in strict mode.
- Modified `providerToAiSdkConfig` to include specific options for OpenAI when in strict mode.
- Introduced a new utility module `providerParams.ts` for managing provider-specific parameters, including OpenAI, Anthropic, and Gemini configurations.
- Added functions to retrieve service tiers, specific parameters, and reasoning efforts for various providers, improving overall provider management.

* feat: enhance OpenAI model handling with utility function

- Introduced `isOpenAIChatCompletionOnlyModel` utility function to determine if a model ID corresponds to OpenAI's chat completion-only models.
- Updated `createBaseModel` function to utilize the new utility for improved handling of OpenAI provider responses in strict mode.
- Refactored reasoning parameters in `getOpenAIReasoningParams` for consistency and clarity.

* refactor: remove providerParams utility module

* feat: add web search plugin for enhanced AI provider capabilities

- Introduced a new `webSearchPlugin` to provide unified web search functionality across multiple AI providers.
- Added helper functions for adapting web search parameters for OpenAI, Gemini, and Anthropic providers.
- Updated the built-in plugin index to export the new web search plugin and its configuration type.
- Created a new `helper.ts` file to encapsulate web search adaptation logic and support checks for provider compatibility.

* chore: migrate to v5

* refactor: migrate to v5 patch-1

* fix: unexpected chunk

* refactor: streamline model configuration and factory functions

- Updated the `createModel` function to accept a simplified `ModelConfig` interface, enhancing clarity and usability.
- Refactored `createBaseModel` to destructure parameters for better readability and maintainability.
- Removed the `ModelCreator.ts` file as its functionality has been integrated into the factory functions.
- Adjusted type definitions in `types.ts` to reflect changes in model configuration structure, ensuring consistency across the codebase.

* refactor: migrate to v5 patch-2

* fix(provider):  config error

* fix(provider): config error patch-1

* feat: support image

* refactor: enhance model configuration and plugin execution

- Simplified the `createModel` function to directly accept the `ModelConfig` object, improving clarity.
- Updated `createBaseModel` to include `extraModelConfig` for extended configuration options.
- Introduced `executeConfigureContext` method in `PluginManager` to handle context configuration for plugins.
- Adjusted type definitions in `types.ts` to ensure consistency with the new configuration structure.
- Refactored plugin execution methods in `PluginEngine` to utilize the resolved model directly, enhancing the flow of data through the plugin system.

* fix: openai-gemini support

* refactor: update type exports and enhance web search functionality

- Added `ReasoningPart`, `FilePart`, and `ImagePart` to type exports in `index.ts`.
- Refactored `transformParameters.ts` to include `enableWebSearch` option and integrate web search tools.
- Introduced new utility `getWebSearchTools` in `websearch.ts` to manage web search tool configurations based on model type.
- Commented out deprecated code in `smoothReasoningPlugin.ts` and `textPlugin.ts` for potential removal.

* fix: format apihost

* feat: add XAI provider options and enhance web search plugin

- Introduced `createXaiOptions` function for XAI provider configuration.
- Added `XaiProviderOptions` type and validation schema in `xai.ts`.
- Updated `ProviderOptionsMap` to include XAI options.
- Enhanced `webSearchPlugin` to support XAI-specific search parameters.
- Refactored helper functions to integrate new XAI options into provider configurations.

* feat: conditionally enable web search plugin based on configuration

- Updated the logic to add the `webSearchPlugin` only if `middlewareConfig.enableWebSearch` is true.
- Added comments to clarify the use of default search parameters and configuration options.

* feat: aihubmix support

* refactor: improve web search plugin and middleware integration

- Cleaned up the web search plugin code by commenting out unused sections for clarity.
- Enhanced middleware handling for the OpenAI provider by wrapping the logic in a block for better readability.
- Removed redundant imports from various files to streamline the codebase.
- Added `enableWebSearch` parameter to the fetchChatCompletion function for improved functionality.

* fix: azure-openai provider

* feat: enhance web search plugin configuration

- Added `sources` array to the default web search configuration, allowing for multiple source types including 'web', 'x', and 'news'.
- This update improves the flexibility and functionality of the web search plugin.

* chore: update ai package version to 5.0.0-beta.9 in package.json and yarn.lock

* feat: enhance model handling and provider integration

- Updated `createBaseModel` to differentiate between OpenAI chat and response models.
- Introduced new utility functions for model identification: `isOpenAIReasoningModel`, `isOpenAILLMModel`, and `getModelToProviderId`.
- Improved `transformParameters` to conditionally set the system prompt based on the assistant's prompt.
- Refactored `getAiSdkProviderIdForAihubmix` to simplify provider identification logic.
- Enhanced `getAiSdkProviderId` to support provider type checks.

* feat: enhance web search plugin and tool handling

- Refactored `helper.ts` to export new types `AnthropicSearchInput` and `AnthropicSearchOutput` for better integration with the web search plugin.
- Updated `index.ts` to include the new types in the exports for improved type safety.
- Modified `AiSdkToChunkAdapter.ts` to handle tool calls more flexibly by introducing a `GenericProviderTool` type, allowing for better differentiation between MCP tools and provider-executed tools.
- Adjusted `handleTooCallChunk.ts` to accommodate the new tool type structure, enhancing the handling of tool call responses.
- Updated type definitions in `index.ts` to reflect changes in tool handling logic.

* feat: enhance provider settings and model configuration

- Updated `ModelConfig` to include a `mode` property for better differentiation between 'chat' and 'responses'.
- Modified `createBaseModel` to conditionally set the provider based on the new `mode` property in `providerSettings`.
- Refactored `RuntimeExecutor` to utilize the updated `ModelConfig` for improved type safety and clarity in provider settings.
- Adjusted imports in `executor.ts` and `types.ts` to align with the new model configuration structure.

* feat: enhance AiSdkToChunkAdapter for web search results handling

- Updated `AiSdkToChunkAdapter` to include `webSearchResults` in the final output structure for improved web search integration.
- Modified `convertAndEmitChunk` method to handle `finish-step` events, differentiating between Google and other web search results.
- Adjusted the handling of `source` events to accumulate web search results for better processing.
- Enhanced citation formatting in `messageBlock.ts` to support new web search result structures.

* feat: integrate web search tool and enhance tool handling

- Added `webSearchTool` to facilitate web search functionality within the SDK.
- Updated `AiSdkToChunkAdapter` to utilize `BaseTool` for improved type handling.
- Refactored `transformParameters` to support `webSearchProviderId` for enhanced web search integration.
- Introduced new `BaseTool` type structure to unify tool definitions across the codebase.
- Adjusted imports and type definitions to align with the new tool handling logic.

* feat: enhance web search functionality and tool integration

- Introduced `extractSearchKeywords` function to facilitate keyword extraction from user messages for web searches.
- Updated `webSearchTool` to streamline the execution of web searches without requiring a request ID.
- Enhanced `WebSearchService` methods to be static for improved accessibility and clarity.
- Modified `ApiService` to pass `webSearchProviderId` for better integration with the web search functionality.
- Improved `ToolCallChunkHandler` to handle built-in tools more effectively.

* feat: add type property to server tools in MCPService

- Enhanced the server tool structure by adding a `type` property set to 'mcp' for better identification and handling of tools within the MCPService.

* chore: update dependencies and versions in package.json and yarn.lock

- Upgraded various SDK packages to their latest beta versions for improved functionality and compatibility.
- Updated `@ai-sdk/provider-utils` to version 3.0.0-beta.3.
- Adjusted dependencies in `package.json` to reflect the latest versions, including `@ai-sdk/amazon-bedrock`, `@ai-sdk/anthropic`, `@ai-sdk/azure`, and others.
- Removed outdated versions from `yarn.lock` and ensured consistency across the project.

* fix: update provider identification logic in aiCore

- Refactored the provider identification in `index_new.ts` to use `actualProvider.type` instead of `actualProvider.id` for better clarity and accuracy in determining OpenAI response modes.
- Removed redundant type checks in `factory.ts` to streamline the provider ID retrieval process.

* chore: update package dependencies and improve AI SDK chunk handling

- Bumped versions of several dependencies in package.json, including `@swc/plugin-styled-components` to 8.0.4 and `@vitejs/plugin-react-swc` to 3.10.2.
- Enhanced `AiSdkToChunkAdapter` to streamline chunk processing, including better handling of text and reasoning events.
- Added console logging for debugging in `BlockManager` and `messageThunk` to track state changes and callback executions.
- Updated integration tests to reflect changes in message structure and types.

* refactor: reorganize AiSdkToChunkAdapter and enhance tool call handling

- Moved AiSdkToChunkAdapter to a new directory structure for better organization.
- Implemented detailed handling for tool call events in ToolCallChunkHandler, including creation, updates, and completions.
- Added a new method to handle tool call creation and improved state management for active tool calls.
- Updated StreamProcessingService to support new chunk types and callbacks for block creation.
- Enhanced type definitions and added comments for clarity in the new chunk handling logic.

* refactor: enhance provider settings and update web search plugin configuration

- Updated providerSettings to allow optional 'mode' parameter for various providers, enhancing flexibility in model configuration.
- Refactored web search plugin to integrate Google search capabilities and streamline provider options handling.
- Removed deprecated code and improved type definitions for better clarity and maintainability.
- Added console logging for debugging purposes in the provider configuration process.

* chore: add repository metadata and homepage to package.json

- Included repository URL, bugs URL, and homepage in package.json for better project visibility and issue tracking.
- This update enhances the package's metadata, making it easier for users to find relevant resources and report issues.

* chore: remove deprecated patches for @ai-sdk/google-vertex and @ai-sdk/openai-compatible

- Deleted outdated patch files for @ai-sdk/google-vertex and @ai-sdk/openai-compatible from the project.
- Updated package.json to reflect the removal of these patches, streamlining dependency management.

* chore: update package.json and add tsdown configuration for build process

- Changed the main and types entries in package.json to point to the dist directory for better output management.
- Added a new tsdown.config.ts file to define the build configuration, specifying entry points, output directory, and formats for the project.

* chore(aiCore/version): update version to 1.0.0-alpha.0

* feat: enhance AI core functionality and introduce new tool components

- Updated README to reflect the addition of a powerful plugin system and built-in web search capabilities.
- Refactored tool call handling in `ToolCallChunkHandler` to improve state management and response formatting.
- Introduced new components `MessageMcpTool`, `MessageTool`, and `MessageTools` for better handling of tool responses and user interactions.
- Updated type definitions to support new tool response structures and improved overall code organization.
- Enhanced spinner component to accept React nodes for more flexible content rendering.

* feat: add React Native support to aiCore package

Add React Native compatibility configuration to package.json, including the
react-native field and updated exports mapping. Include documentation for
React Native usage with metro.config.js setup instructions.

* chore: bump @cherrystudio/ai-core version to 1.0.0-alpha.1

* chore: bump @cherrystudio/ai-core version to 1.0.0-alpha.2 and update exports

- Updated version in package.json to 1.0.0-alpha.2.
- Added new path mapping for @cherrystudio/ai-core in tsconfig.web.json.
- Refactored export paths in tsdown.config.ts and index.ts for consistency.
- Cleaned up type exports in index.ts and types.ts for better organization.

* refactor: reorganize provider and model exports for improved structure

- Updated exports in index.ts and related files to streamline provider and model management.
- Introduced a new ModelCreator module for better encapsulation of model creation logic.
- Refactored type imports to enhance clarity and maintainability across the codebase.
- Removed deprecated provider configurations and cleaned up unused code for better performance.

* chore: update @cherrystudio/ai-core version to 1.0.0-alpha.4 and clean up dependencies

- Bumped version in package.json to 1.0.0-alpha.4.
- Removed deprecated dependencies from package.json and yarn.lock for improved clarity.
- Updated README to reflect changes in supported providers and installation instructions.
- Refactored provider registration and usage examples for better clarity and usability.

* refactor: streamline AI provider registration by replacing dynamic imports with direct creator functions

- Updated the AiProviderRegistry to use direct references to creator functions for each AI provider, improving clarity and performance.
- Removed dynamic import statements for providers, simplifying the registration process and enhancing maintainability.

* chore: bump @cherrystudio/ai-core version to 1.0.0-alpha.5

- Updated version in package.json to 1.0.0-alpha.5.
- Enhanced provider configuration validation in createProvider function for improved error handling.

* docs: update AI SDK architecture and README for enhanced clarity and new features

- Revised AI SDK architecture diagram to reflect changes in component relationships, replacing PluginEngine with RuntimeExecutor.
- Updated README to highlight core features, including a refined plugin system, improved architecture design, and new built-in plugins.
- Added detailed examples for using built-in plugins and creating custom plugins, enhancing documentation for better usability.
- Included future version roadmap and related resources for user reference.

* feat: enhance web search tool functionality and type definitions

- Introduced new `WebSearchToolOutputSchema` type to standardize output from web search tools.
- Updated `webSearchTool` and `webSearchToolWithExtraction` to utilize Zod for input and output schema validation.
- Refactored tool execution logic to improve error handling and response formatting.
- Cleaned up unused type imports and comments for better code clarity.

* fix: conditionally enable reasoning middleware for OpenAI and Azure providers

- Added a check to enable the 'thinking-tag-extraction' middleware only if reasoning is enabled in the configuration for OpenAI and Azure providers.
- Commented out the provider type check in `getAiSdkProviderId` to prevent issues with retrieving provider options.

* feat: update OpenAI provider integration and enhance type definitions

- Bumped version of `@ai-sdk/openai-compatible` to 1.0.0-beta.8 in package.json.
- Introduced a new provider configuration for 'OpenAI Responses' in AiProviderRegistry, allowing for more flexible response handling.
- Updated type definitions to include 'openai-responses' in ProviderSettingsMap for improved type safety.
- Refactored getModelToProviderId function to return a more specific ProviderId type.

* feat: enhance ToolCallChunkHandler with detailed chunk handling and remove unused plugins

- Updated `handleToolCallCreated` method to support additional chunk types with optional provider metadata.
- Removed deprecated `smoothReasoningPlugin` and `textPlugin` files to clean up the codebase.
- Cleaned up unused type imports in `tool.ts` for improved clarity and maintainability.

* <type>: <subject>
<body>
<footer>
用來簡要描述影響本次變動,概述即可

* refactor: update message handling in searchOrchestrationPlugin for improved type safety

- Replaced `Message` type with `ModelMessage` in various functions to enhance type consistency.
- Refactored `getMessageContent` function to utilize the new `ModelMessage` type for better content extraction.
- Updated `storeConversationMemory` and `analyzeSearchIntent` functions to align with the new type definitions, ensuring clearer memory storage and intent analysis processes.

* chore: bump @cherrystudio/ai-core version to 1.0.0-alpha.6 and refactor web search tool

- Updated version in package.json to 1.0.0-alpha.6.
- Simplified response structure in ToolCallChunkHandler by removing unnecessary nesting.
- Refactored input schema for web search tool to enhance type safety and clarity.
- Cleaned up commented-out code in MessageTool for improved readability.

* refactor: consolidate queue utility imports in messageThunk.ts

- Combined separate imports of `getTopicQueue` and `waitForTopicQueue` from the queue utility into a single import statement for improved code clarity and organization.

* feat: implement knowledge search tool and enhance search orchestration logic

- Added a new `knowledgeSearchTool` to facilitate knowledge base searches based on user queries and intent analysis.
- Refactored `analyzeSearchIntent` to simplify message context construction and improve prompt formatting.
- Introduced a flag to prevent concurrent analysis processes in `searchOrchestrationPlugin`.
- Updated tool configuration logic to conditionally add the knowledge search tool based on the presence of knowledge bases and user settings.
- Cleaned up commented-out code for better readability and maintainability.

* refactor: enhance search orchestration and web search tool integration

- Updated `searchOrchestrationPlugin` to improve handling of assistant configurations and prevent concurrent analysis.
- Refactored `webSearchTool` to utilize pre-extracted keywords for more efficient web searches.
- Introduced a new `MessageKnowledgeSearch` component for displaying knowledge search results.
- Cleaned up commented-out code and improved type safety across various components.
- Enhanced the integration of web search results in the UI for better user experience.

* refactor: streamline async function syntax and enhance plugin event handling

- Simplified async function syntax in `RuntimeExecutor` and `PluginEngine` for improved readability.
- Updated `AiSdkToChunkAdapter` to refine condition checks for Google metadata.
- Enhanced `searchOrchestrationPlugin` to log conversation messages and improve memory storage logic.
- Improved memory processing by ensuring fallback for existing memories.
- Added new citation block handling in `toolCallbacks` for better integration with web search results.

* feat: integrate image generation capabilities and enhance testing framework

- Added support for image generation in the `RuntimeExecutor` with a new `generateImage` method.
- Updated `aiCore` package to include `vitest` for testing, with new test scripts added.
- Enhanced type definitions to accommodate image model handling in plugins.
- Introduced new methods for resolving and executing image generation with plugins.
- Updated package dependencies in `package.json` to include `vitest` and ensure compatibility with new features.

* refactor: enhance image generation handling and tool integration

- Updated image generation logic to support new model types and improved size handling.
- Refactored middleware configuration to better manage tool usage and reasoning capabilities.
- Introduced new utility functions for checking model compatibility with image generation.
- Enhanced the integration of plugins for improved functionality during image generation processes.
- Removed deprecated knowledge search tool to streamline the codebase.

* refactor: migrate to v5 patch-1

* fix: migrate to v5-patch2

* refactor: restructure aiCore for improved modularity and legacy support

- Introduced a new `index_new.ts` file to facilitate the modern AI provider while maintaining backward compatibility with the legacy `index.ts`.
- Created a `legacy` directory to house existing clients and middleware, ensuring a clear separation from new implementations.
- Updated import paths across various modules to reflect the new structure, enhancing code organization and maintainability.
- Added comprehensive middleware and utility functions to support the new architecture, improving overall functionality and extensibility.
- Enhanced plugin management with a dedicated `PluginBuilder` for better integration and configuration of AI plugins.

* chore(yarn.lock): remove deprecated provider entries and clean up dependencies

* chore(package.json): bump version to 1.0.0-alpha.7

* feat(tests): add unit tests for utility functions in utils.test.ts

- Implemented tests for `createErrorChunk`, `capitalize`, and `isAsyncIterable` functions.
- Ensured comprehensive coverage for various input scenarios, including error handling and edge cases.

* feat(aiCore): enhance AI SDK with tracing and telemetry support

- Integrated tracing capabilities into the ModernAiProvider, allowing for better tracking of AI completions and image generation processes.
- Added a new TelemetryPlugin to inject telemetry data into AI SDK requests, ensuring compatibility with existing tracing systems.
- Updated middleware and plugin configurations to support topic-based tracing, improving the overall observability of AI interactions.
- Introduced comprehensive logging throughout the AI SDK processes to facilitate debugging and performance monitoring.
- Added unit tests for new functionalities to ensure reliability and maintainability.

* fix: update MessageKnowledgeSearch to use knowledgeReferences

- Modified MessageKnowledgeSearch component to display additional context from toolInput.
- Updated the fetch complete message to reflect the count of knowledgeReferences instead of toolOutput.
- Adjusted the mapping of results to iterate over knowledgeReferences for rendering.

* refactor(aiCore): update ModernAiProvider constructor and clean up unused code

- Modified the constructor of ModernAiProvider to accept an optional provider parameter, enhancing flexibility in provider selection.
- Removed deprecated and unused functions related to search keyword extraction and search summary fetching, streamlining the codebase.
- Updated import statements and adjusted related logic to reflect the removal of obsolete functions, improving maintainability.

* feat(Dropdown): replace RightOutlined with ChevronRight icon and update useIcons to use ChevronDown

- Introduced a patch to replace the RightOutlined icon with ChevronRight in the Dropdown component for improved visual consistency.
- Updated the useIcons hook to utilize ChevronDown instead of DownOutlined, enhancing the icon set with Lucide icons.
- Adjusted icon properties for better customization and styling options.

* feat(aiCore): add enableUrlContext capability and new support function

- Enhanced the buildStreamTextParams function to include enableUrlContext in the capabilities object, improving the parameter set for AI interactions.
- Introduced a new isSupportedFlexServiceTier function to streamline model support checks, enhancing code clarity and maintainability.

* chore: update dependencies and remove unused patches

- Updated various package versions in yarn.lock for improved compatibility and performance.
- Removed obsolete patches for antd and openai, streamlining the dependency management.
- Adjusted icon imports in Dropdown and useIcons to utilize Lucide icons for better visual consistency.

* refactor(types): update ProviderId type definition for better compatibility

- Changed ProviderId type from a union of keyof ExtensibleProviderSettingsMap and string to an intersection, enhancing type safety.
- Renamed appendTrace method to appendMessageTrace in SpanManagerService for clarity and consistency.
- Updated references to appendTrace in useMessageOperations and ApiService to use the new method name.
- Added a new appendTrace method in SpanManagerService to bind existing traces, improving trace management.
- Adjusted topicId handling in fetchMessagesSummary to default to an empty string for better consistency.

* refactor(types): modify ProviderId type definition for improved flexibility

- Updated ProviderId type from an intersection of keyof ExtensibleProviderSettingsMap and string to a union, allowing for greater compatibility with dynamic provider settings.

* fix: file name

* fix: missing dependencies

* fix: remove default renderer from MessageTool

* feat(provider): enhance provider registration and validation system

- Introduced a new Zod-based schema for provider validation, improving type safety and consistency.
- Added support for dynamic provider IDs and enhanced the provider registration process.
- Updated the AiProviderRegistry to utilize the new validation functions, ensuring robust provider management.
- Added tests for the provider registry to validate dynamic imports and functionality.
- Updated yarn.lock to reflect the latest dependency versions.

* feat(toolUsePlugin): enhance tool parsing and extraction functionality

- Updated the `defaultParseToolUse` function to return both parsed results and remaining content, improving usability.
- Introduced a new `TagExtractor` class for flexible tag extraction, supporting various tag formats.
- Modified type definitions to reflect changes in parsing function signatures.
- Enhanced handling of tool call events in the `ToolCallChunkHandler` for better integration with the new parsing logic.
- Added `isBuiltIn` property to the `MCPTool` interface for clearer tool categorization.

* feat(aiCore): update vitest version and enhance provider validation

- Upgraded `vitest` dependency to version 3.2.4 in package.json and yarn.lock for improved testing capabilities.
- Removed console error logging in provider validation functions to streamline error handling.
- Added comprehensive tests for the AiProviderRegistry functionality, ensuring robust provider management and dynamic registration.
- Introduced new test cases for provider schemas to validate configurations and IDs.
- Deleted outdated registry test file to maintain a clean test suite.

* feat(toolUsePlugin): refactor tool execution and event management

- Extracted `StreamEventManager` and `ToolExecutor` classes from `promptToolUsePlugin.ts` to improve code organization and reduce complexity.
- Enhanced tool execution logic with better error handling and event management.
- Updated the `createPromptToolUsePlugin` function to utilize the new classes for cleaner implementation.
- Improved recursive call handling and result formatting for tool executions.
- Streamlined the overall flow of tool calls and event emissions within the plugin.

* feat(dependencies): update ai-sdk packages and improve logging

- Upgraded `@ai-sdk/gateway` to version 1.0.8 and `@ai-sdk/provider-utils` to version 3.0.4 in yarn.lock for enhanced functionality.
- Updated `ai` dependency in package.json to version ^5.0.16 for better compatibility.
- Added logging functionality in `AiSdkToChunkAdapter` to track chunk types and improve debugging.
- Refactored plugin imports to streamline code and enhance readability.
- Removed unnecessary console logs in `searchOrchestrationPlugin` to clean up the codebase.

* feat(dependencies): update ai-sdk packages and improve type safety

- Upgraded multiple `@ai-sdk` packages in `yarn.lock` and `package.json` to their latest versions for enhanced functionality and compatibility.
- Improved type safety in `searchOrchestrationPlugin` by adding optional chaining to handle potential undefined values in knowledge bases.
- Cleaned up dependency declarations to use caret (^) for versioning, ensuring compatibility with future updates.

* feat(aiCore): enhance tool response handling and type definitions

- Updated the ToolCallChunkHandler to support both MCPTool and NormalToolResponse types, improving flexibility in tool response management.
- Refactored type definitions for MCPToolResponse and introduced NormalToolResponse to better differentiate between tool response types.
- Enhanced logging in MCP utility functions for improved error tracking and debugging.
- Cleaned up type imports and ensured consistent handling of tool responses across various chunks.

* feat(tools): refactor MemorySearchTool and WebSearchTool for improved response handling

- Updated MemorySearchTool to utilize aiSdk for better integration and removed unused imports.
- Refactored WebSearchTool to streamline search results handling, changing from an array to a structured object for clarity.
- Adjusted MessageTool and MessageWebSearchTool components to reflect changes in tool response structure.
- Enhanced error handling and logging in tool callbacks for improved debugging and user feedback.

* feat(aiCore): update ai-sdk-provider and enhance message conversion logic

- Upgraded `@openrouter/ai-sdk-provider` to version ^1.1.2 in package.json and yarn.lock for improved functionality.
- Enhanced `convertMessageToSdkParam` and related functions to support additional model parameters, improving message conversion for various AI models.
- Integrated logging for error handling in file processing functions to aid in debugging and user feedback.
- Added support for native PDF input handling based on model capabilities, enhancing file processing features.

* feat(dependencies): update @ai-sdk/openai and @ai-sdk/provider-utils versions

- Upgraded `@ai-sdk/openai` to version 2.0.19 in `yarn.lock` and `package.json` for improved functionality and compatibility.
- Updated `@ai-sdk/provider-utils` to version 3.0.5, enhancing dependency management.
- Added `TypedToolError` type export in `index.ts` for better error handling.
- Removed unnecessary console logs in `webSearchPlugin` for cleaner code.
- Refactored type handling in `createProvider` to ensure proper type assertions.
- Enforced `topicId` as a required field in the `ModernAiProvider` configuration for stricter validation.

* [WIP]refactor(aiCore): restructure models and introduce ModelResolver

- Removed outdated ConfigManager and factory files to streamline model management.
- Added ModelResolver for improved model ID resolution, supporting both traditional and namespaced formats.
- Introduced DynamicProviderRegistry for dynamic provider management, enhancing flexibility in model handling.
- Updated index exports to reflect the new structure and maintain compatibility with existing functionality.

* feat(aiCore): introduce Hub Provider and enhance provider management

- Added a new example file demonstrating the usage of the Hub Provider for routing to multiple underlying providers.
- Implemented the Hub Provider to support model ID parsing and routing based on a specified format.
- Refactored provider management by introducing a Registry Management class for better organization and retrieval of provider instances.
- Updated the Provider Initializer to streamline the initialization and registration of providers, enhancing overall flexibility and usability.
- Removed outdated files related to provider creation and dynamic registration to simplify the codebase.

* feat(aiCore): enhance dynamic provider registration and refactor HubProvider

- Introduced dynamic provider registration functionality, allowing for flexible management of providers through a new registry system.
- Refactored HubProvider to streamline model resolution and improve error handling for unsupported models.
- Added utility functions for managing dynamic providers, including registration, cleanup, and alias resolution.
- Updated index exports to include new dynamic provider APIs, enhancing overall usability and integration.
- Removed outdated provider files and simplified the provider management structure for better maintainability.

* chore(aiCore): bump version to 1.0.0-alpha.9 in package.json

* feat(aiCore): add MemorySearchTool and WebSearchTool components

- Introduced MessageMemorySearch and MessageWebSearch components for handling memory and web search tool responses.
- Updated MemorySearchTool and WebSearchTool to improve response handling and integrate with the new components.
- Removed unused console logs and streamlined code for better readability and maintainability.
- Added new dependencies in package.json for enhanced functionality.

* refactor(aiCore): improve type handling and response structures

- Updated AiSdkToChunkAdapter to refine web search result handling.
- Modified McpToolChunkMiddleware to ensure consistent type usage for tool responses.
- Enhanced type definitions in chunk.ts and index.ts for better clarity and type safety.
- Adjusted MessageWebSearch styles for improved UI consistency.
- Refactored parseToolUse function to align with updated MCPTool response structures.

* feat(aiCore): enhance provider management and registration system

- Added support for a new provider configuration structure in package.json, enabling better integration of provider types.
- Updated tsdown.config.ts to include new entry points for provider modules, improving build organization.
- Refactored index.ts to streamline exports and enhance type handling for provider-related functionalities.
- Simplified provider initialization and registration processes, allowing for more flexible provider management.
- Improved type definitions and removed deprecated methods to enhance code clarity and maintainability.

* chore(aiCore): bump version to 1.0.0-alpha.10 in package.json

* fix(aiCore): update tool call status and enhance execution flow

- Changed tool call status from 'invoking' to 'pending' for better clarity in execution state.
- Updated the tool execution logic to include user confirmation for non-auto-approved tools, improving user interaction.
- Refactored the handling of experimental context in the tool execution parameters to support chunk streaming.
- Commented out unused tool input event cases in AiSdkToChunkAdapter for cleaner code.

* feat(types): add VertexProvider type for Google Cloud integration

Introduced a new VertexProvider type that includes properties for Google credentials and project details, enhancing type safety and support for Google Cloud functionalities.

* refactor(logging): 将console替换为logger以统一日志管理

* fix(i18n): 更新多语言文件中 websearch.fetch_complete 的翻译格式

统一将“已完成 X 次搜索”改为“X 个搜索结果”格式,并添加 avatar.builtin 字段翻译

* refactor(aiCore): streamline type exports and enhance provider registration

Removed unused type exports from the aiCore module and consolidated type definitions for better clarity. Updated provider registration tests to reflect new configurations and improved error handling for non-existent providers. Enhanced the overall structure of the provider management system, ensuring better type safety and consistency across the codebase.

* chore(dependencies): update ai and related packages to version 5.0.26 and 1.0.15

Updated the 'ai' package to version 5.0.26 and '@ai-sdk/gateway' to version 1.0.15. Also, updated '@ai-sdk/provider-utils' to version 3.0.7 and 'eventsource-parser' to version 3.0.5. Adjusted type definitions in aiCore for better type safety in plugin parameters and results.

* chore(config): add new aliases for ai-core in Vite and TypeScript configuration

Updated the Vite and TypeScript configuration files to include new path aliases for the ai-core package, enhancing module resolution for core providers and built-in plugins. This change improves the organization and accessibility of the ai-core components within the project.

* feat(release): update version to 1.6.0-beta.1 and enhance release notes with new features, improvements, and bug fixes

- Integrated a new AI SDK architecture for better performance
- Added OCR functionality for image text recognition
- Introduced a code tools page with environment variable settings
- Enhanced the MCP server list with search capabilities
- Improved SVG preview and HTML content rendering
- Fixed multiple issues including document preprocessing failures and path handling on Windows
- Optimized performance and memory usage across various components

* refactor(modelResolver): replace ':' with '|' as the default separator for model IDs

Updated the ModelResolver and related components to use '|' as the default separator instead of ':'. This change improves compatibility and resolves potential conflicts with model ID suffixes. Adjusted model resolution logic accordingly to ensure consistent behavior across the application.

* feat(aihubmix): add 'type' property to provider configuration for Gemini integration

* refactor(errorHandling): improve error serialization and update error handling in callbacks

- Updated the error handling in the `onError` callback to use `AISDKError` type for better type safety.
- Introduced a new `serializeError` function to standardize error serialization.
- Modified the `ErrorBlock` component to directly access the error message.
- Removed deprecated error formatting functions to streamline the error utility module.

* feat(i18n): add error detail translations and enhance error handling UI

- Added new translation keys for error details in multiple languages, including 'detail', 'details', 'message', 'requestBody', 'requestUrl', 'stack', and 'status'.
- Updated the ErrorBlock component to display a modal with detailed error information, allowing users to view and copy error details easily.
- Improved the user experience by providing a clear and accessible way to understand error messages and their context.

* feat(inputbar): enhance MCP tools visibility with prompt tool support

- Updated the Inputbar component to include the `isPromptToolUse` function, allowing for better visibility of MCP tools based on the assistant's capabilities.
- This change improves user experience by expanding the conditions under which MCP tools are displayed.

* refactor(aiCore): enhance completions methods with developer mode support

- Introduced a check for developer mode in the completions methods to enable tracing capabilities when a topic ID is provided.
- Updated the method signatures and internal calls to streamline the handling of completions with and without tracing.
- Improved code organization by making the completionsForTrace method private and renaming it for clarity.

* feat(ErrorBlock): add centered modal display for error details

- Updated the ErrorBlock component to include a centered modal for displaying error details, enhancing the user interface and accessibility of error information.

* chore(aiCore): update version to 1.0.0-alpha.11 and refactor model resolution logic

- Bumped the version of the ai-core package to 1.0.0-alpha.11.
- Removed the `isOpenAIChatCompletionOnlyModel` utility function to simplify model resolution.
- Adjusted the `providerToAiSdkConfig` function to accept a model parameter for improved configuration handling.
- Updated the `ModernAiProvider` class to utilize the new model parameter in its configuration.
- Cleaned up deprecated code related to search keyword extraction and reasoning parameters.

* fix(inputbar): conditionally display knowledge icon based on MCP tools visibility

- Updated the Inputbar component to conditionally show the knowledge icon only when both `showKnowledgeIcon` and `showMcpTools` are true. This change enhances the visibility logic for the knowledge icon, improving the user interface based on the current context.

* feat(aiCore): introduce provider configuration enhancements and initialization

- Added a new provider configuration module to handle special provider logic and formatting.
- Implemented asynchronous preparation of special provider configurations in the ModernAiProvider class.
- Refactored provider initialization logic to support dynamic registration of new AI providers.
- Updated utility functions to streamline provider option building and improve compatibility with new provider configurations.

* refactor(aiCore): streamline provider options and enhance OpenAI handling

- Simplified the OpenAI mode handling in the provider configuration.
- Added service tier settings to provider-specific options for better configuration management.
- Refactored the `buildOpenAIProviderOptions` function to remove redundant parameters and improve clarity.

* refactor(aiCore): enhance temperature and TopP parameter handling

- Updated `getTemperature` and `getTopP` functions to incorporate reasoning effort checks for Claude models.
- Refactored logic to ensure temperature and TopP settings are only returned when applicable.
- Improved clarity and maintainability of parameter retrieval functions.

* feat(releaseNotes): update release notes with new features and improvements

- Added a modal for detailed error information with multi-language support.
- Enhanced AI Core with version upgrade and improved parameter handling.
- Refactored error handling for better type safety and performance.
- Removed deprecated code and improved provider initialization logic.

* refactor(aiCore): clean up KnowledgeSearchTool and searchOrchestrationPlugin

- Commented out unused parameters and code in the KnowledgeSearchTool to improve clarity and maintainability.
- Removed the tool choice assignment in searchOrchestrationPlugin to streamline the logic.
- Updated instructions handling in KnowledgeSearchTool to eliminate unnecessary fields.

* chore(package): bump version to 1.6.0-beta.2

* refactor(transformParameters): 移除DEFAULT_MAX_TOKENS并替换console.log为logger.debug

移除未使用的DEFAULT_MAX_TOKENS常量,直接使用传入的maxTokens参数
将调试日志输出从console.log改为更规范的logger.debug

* docs(OrchestrateService): 添加注释说明暂时未使用的类和函数用途

* feat(OrchestrateService): 添加提示变量替换功能

在调用API前替换assistant.prompt中的变量,以支持动态提示内容

* refactor(providers): 重构基础 providers 定义和类型导出

将基础 provider IDs 和 schema 定义移到文件顶部
移除从 baseProviders 动态生成 IDs 的逻辑
使用 satisfies 约束 baseProviders 类型

* refactor(aiCore): 重构provider选项构建逻辑以支持更多provider类型

重构buildProviderOptions函数,使用schema验证providerId并分为基础provider和自定义provider处理
新增对deepseek、openai-compatible等provider的支持

* feat(reasoning): 增强模型推理控制逻辑,支持更多提供商和模型类型

添加对Hunyuan、DeepSeek混合推理等模型的支持
优化OpenRouter、Qwen等提供商的推理控制逻辑
统一不同提供商的思考控制方式
添加日志记录和错误处理

* refactor(aiCore): improve provider registration and model resolution logic

- Enhanced the model resolution logic to support dynamic provider IDs for chat modes.
- Updated provider registration to handle both OpenAI and Azure with a unified approach for chat variants.
- Refactored the `buildOpenAIProviderOptions` function to streamline parameter handling and removed unnecessary parameters.
- Added configuration options for Azure to manage API versions and deployment URLs effectively.

* feat(aiCore): 添加对智谱AI模型的支持

在getReasoningEffort函数中新增对智谱AI模型的支持逻辑,包括判断模型类型及设置对应的推理参数

* feat(翻译): 添加对Qwen MT模型翻译选项的特殊处理

添加对Qwen MT模型的支持,当助手类型为翻译时设置翻译选项。若目标语言不支持则抛出错误

* refactor(aiCore): 将console.log替换为logger.debug以改进日志记录

* refactor(aiCore): 提取 ModernAiProviderConfig 类型以复用

将多处重复的配置类型定义提取为 ModernAiProviderConfig 类型,提高代码可维护性

* refactor(services): 提取 FetchChatCompletionOptions 类型以提升代码可维护性

* refactor(ApiService): 重构 fetchChatCompletion 参数类型定义

将参数类型提取为独立的类型定义,支持 messages 或 prompt 两种参数形式

* fix(ApiService): 使FetchChatCompletionOptions参数变为可选

为了增加接口的灵活性,将options参数改为可选

* fix(ApiService): 修复prompt参数类型并添加消息转换逻辑

根据vercel/ai#8363问题,将prompt参数类型从StreamTextParams['prompt']改为string
当传入prompt参数时自动转换为messages格式

* refactor(translate): 重构语言检测功能,使用通用聊天接口替代专用接口

- 移除专用的语言检测接口 fetchLanguageDetection
- 使用现有的 fetchChatCompletion 接口实现语言检测功能
- 处理 QwenMT 模型的特殊逻辑
- 优化语言检测的提示词和参数处理

* refactor(TranslateService): 重构翻译服务使用新的API接口

移除旧的翻译逻辑,改用新的fetchChatCompletion接口实现翻译功能
简化代码结构并移除不再需要的依赖

* feat(abortController): 添加readyToAbort函数用于创建并注册AbortController

提供便捷方法创建AbortController并自动注册到全局映射中,简化取消操作的流程

* feat(aiCore): 添加对文本增量累积的可配置支持

根据模型是否支持文本增量来决定是否累积文本内容,新增accumulate参数控制行为

* fix(translate): 修复翻译过程中中止控制器的错误处理

修正abortController.ts中中止控制器的回调函数调用方式
统一翻译错误消息的格式化处理
改进翻译服务的中止逻辑和错误传播

* docs(i18n): 添加请求路径的国际化翻译

* fix(api): 修复API检查时的错误处理和取消逻辑

添加对API检查过程中错误的处理,并支持通过abortController取消请求
避免空系统提示词导致的报错,优化检查流程的健壮性

* feat(mini窗口): 添加ErrorBoundary组件包裹内容

为mini窗口内容添加错误边界组件,防止未捕获错误导致整个应用崩溃

* feat(release): 更新版本至1.6.0-beta.3并添加新功能和优化

- 新增多个功能,包括ErrorBoundary组件、智谱AI模型支持、系统OCR功能、文件页面批量删除等
- 重构翻译服务和语言检测功能,提升扩展性和类型安全
- 修复多个问题,改善API检查和翻译过程中的错误处理
- 优化构建配置和性能,提升初始化效率

* test: 更新ApiService测试中ApiClientFactory的模拟路径

* refactor(vertexai): 将VertexAI配置检查从ApiClientFactory移至VertexAPIClient

重构VertexAI客户端的配置检查逻辑,将其从工厂类移动到具体的客户端实现类中

* test(aiCore): 更新客户端兼容性测试的mock数据

添加isOpenAIModel等mock函数用于测试
修复AnthropicVertexClient的mock路径
添加注释说明服务层不应调用React Hook

* fix: 添加注释说明redux设置状态不应被服务层使用

* test(ActionUtils): 添加对ConversationService和models模块的mock测试

* test(Spinner): 更新快照

* test(ThinkingBlock): 移除不再需要的实时计时测试用例

* refactor(ThinkingBlock): 优化条件渲染逻辑以提高可读性

* test(streamCallback): 添加serializeError的mock实现

* fix(registry): enhance provider config validation and update error handling in tests

- Added a check for null or undefined config in registerProviderConfig function.
- Updated tests to ensure proper error messages are thrown when no providers are registered.
- Adjusted mock implementations in generateImage tests to reflect changes in provider model identifiers.

* refactor(aiCore): consolidate StreamTextParams type imports and enhance type definitions

- Moved StreamTextParams type definition to a new file for better organization.
- Updated imports across multiple files to reference the new location.
- Adjusted related type definitions in ApiService and ConversationService for consistency.

* feat(providerConfig): add AWS Bedrock support in provider configuration

- Implemented AWS Bedrock integration by adding access key, region, and secret access key retrieval.
- Enhanced providerToAiSdkConfig function to handle Bedrock-specific options.

* fix(providerConfig): update Google Vertex AI import and creator function name

- Changed the import path for Google Vertex AI to '@ai-sdk/google-vertex/edge'.
- Updated the creator function name from 'createGoogleVertex' to 'createVertex' for consistency.

* feat(providerConfig): implement API key rotation logic for providers

- Added a new function to handle rotating API keys for providers, reusing legacy multi-key logic.
- Updated the providerToAiSdkConfig function to utilize the new key rotation mechanism.
- Enhanced TranslateService imports for better organization.

* fix(aiCore): update file data and mediaType extraction in convertFileBlockToFilePart function

- Modified the conversion logic to correctly extract base64 data and MIME type from the API response.
- Ensured that the filename remains unchanged during the conversion process.

* feat(aiCore): enhance file processing logic in convertFileBlockToFilePart function

- Added support for handling image files and document types beyond PDF.
- Implemented file size limit checks and fallback mechanisms for text extraction.
- Improved logging for file processing outcomes, including success and failure cases.
- Introduced functions to check model support for image input and large file uploads.

* chore(release): update version to 1.6.0-beta.4 and enhance release notes

- Updated version in package.json to 1.6.0-beta.4.
- Revised release notes in electron-builder.yml to reflect new features, improvements, and bug fixes, including API key rotation, AWS Bedrock support, and enhanced file processing capabilities.

* refactor(aiCore): restructure parameter handling and file processing modules

- Removed the transformParameters module and replaced it with a more organized structure under prepareParams.
- Introduced new modules for file processing, model capabilities, and parameter handling to improve code maintainability and clarity.
- Updated relevant imports across services to reflect the new module structure.
- Enhanced logging and error handling in file processing functions.

* refactor(providerConfig): improve provider handling and configuration logic

- Introduced formatPrivateKey utility for better private key management.
- Updated handleSpecialProviders function to streamline provider type checks and error handling.
- Enhanced providerToAiSdkConfig function to include Google Vertex AI configuration with private key formatting.
- Removed commented-out code for clarity and maintainability.

* chore(dependencies): update ai package version and enhance aiCore functionality

- Updated the 'ai' package version from 5.0.26 to 5.0.29 in package.json and yarn.lock.
- Refactored aiCore's provider schemas to introduce new provider types and improve type safety.
- Enhanced the RuntimeExecutor class to streamline model handling and plugin execution for various AI tasks.
- Updated tests to reflect changes in parameter handling and ensure compatibility with new provider configurations.

* refactor(AiSdkToChunkAdapter): streamline web search result handling

- Replaced the switch statement with a source mapping object for improved readability and maintainability.
- Enhanced handling of various web search providers by mapping them to their respective sources.
- Simplified the logic for processing web search results in the onChunk method.

* chore: update release notes and version to 1.6.0-beta.5

- Enhanced web search functionality and optimized knowledge base settings for better performance.
- Added automatic image generation for Gemini 2.5 Flash Image model and improved OCR service compatibility on Linux.
- Refactored AI core architecture and improved message retry mechanisms.
- Fixed various UI component issues and improved overall stability.

* feat: enhance provider configuration and stream text parameters

- Added maxRetries option to buildStreamTextParams for improved error handling.
- Implemented custom fetch logic for 'cherryin' provider to include signature generation in requests.

* chore: update release notes and version to 1.6.0-beta.6

- Refined performance optimizations for AI services and improved compatibility with various providers.
- Enhanced error handling and stability in the ModernAiProvider class.
- Updated version in package.json to 1.6.0-beta.6.

* refactor(ErrorBlock): simplify CodeViewer component usage

- Removed unnecessary props from CodeViewer in ErrorBlock for cleaner code and improved readability.

* refactor(CodeViewer): change children prop type to React.ReactNode

- Updated the children prop type in CodeViewer from string to React.ReactNode for improved flexibility in rendering various content types.

* chore: update migration version and add migration logic for version 147

- Incremented migration version from 146 to 147.
- Implemented migration logic to trim trailing slashes from the apiHost of the anthropic provider.

* feat(错误处理): 增强AI SDK错误处理并添加国际化支持

添加AiSdkErrorUnion类型用于统一处理AI SDK错误
实现safeToString函数安全转换任意值为字符串
添加formatAiSdkError和formatError函数格式化错误信息
在ErrorBlock中重构错误详情展示逻辑,支持多种错误类型
补充国际化字段用于错误信息展示

* feat(i18n): 添加错误相关的多语言翻译字段

* feat(错误处理): 统一使用SerializedError类型处理错误并增强类型安全

重构错误处理逻辑,使用@reduxjs/toolkit的SerializedError类型统一处理错误
新增错误类型定义文件并扩展SerializedError接口
更新相关组件和工具函数以适配新的错误类型

* refactor(CodeViewer): make children prop optional for improved flexibility

* feat(ErrorBlock): add success message on clipboard copy action

* refactor(types): 重构错误类型定义并添加序列化功能

- 将 SerializedError 从 @reduxjs/toolkit 移至本地定义
- 添加 Serializable 类型和序列化工具函数
- 更新相关文件中的错误类型引用
- 实现安全序列化功能用于错误处理

* fix(错误处理): 修复错误块中显示错误信息的问题

修正错误块组件中错误信息的显示问题:
1. 修复BuiltinError组件中错误名称显示为消息的问题
2. 修复AiSdkError组件中原因显示为消息的问题
3. 修复AiApiCallError组件中各种字段显示为消息的问题
4. 使用CodeViewer组件正确显示JSON格式数据
5. 移除CodeViewer组件中不必要的children属性
6. 设置serialize默认pretty为true

* feat(错误处理): 添加序列化错误类型检查函数并更新错误格式化逻辑

添加 isSerializedError 类型检查函数用于判断序列化错误
更新 formatError 和 formatAiSdkError 函数以处理序列化错误类型
修改 ErrorBlock 组件使用新的类型检查函数替代 instanceof 检查

* fix: 使用 SerializedError 类型替换 errorData 的 Record 类型

* fix: 为错误对象添加name和stack字段

在工具执行失败和数据库升级时,为错误对象补充name和stack字段以提供更完整的错误信息

* fix(错误处理): 使用JSON.stringify格式化响应头信息以提高可读性

* fix(ErrorBlock): 修复错误状态码检查逻辑以支持statusCode字段

* fix(ErrorBlock): 修复错误状态码检查逻辑以支持statusCode字段

* feat(AI Provider): Update image generation handling to use legacy implementation for enhanced features

- Refactor image generation logic to utilize legacy completions for better support of image editing.
- Introduce uiMessages as a required parameter for image generation endpoints.
- Update related types and middleware configurations to accommodate new message structures.
- Adjust ConversationService and OrchestrateService to handle model and UI messages separately.

* docs(types): 添加prompt类型的注释

* feat: Update message handling to include optional uiMessages parameter

- Modify BaseParams type to make uiMessages optional.
- Refactor message preparation in HomeWindow and ActionUtils to handle modelMessages and uiMessages separately.
- Ensure compatibility with updated message structures in fetchChatCompletion calls.

* refactor(types): Enhance Serializable type to include SerializableValue for improved serialization handling

- Updated Serializable type to use SerializableValue for better clarity and structure.
- Ensured compatibility with nested objects and arrays in serialization logic.

* refactor(serialize): 重命名 SerializableValue 为 SerializablePrimitive 以提高可读性

将 SerializableValue 类型重命名为 SerializablePrimitive 以更准确地反映其用途,仅包含基本类型

* Revert "refactor(serialize): 重命名 SerializableValue 为 SerializablePrimitive 以提高可读性"

This reverts commit 8408a8d359.

* docs(serialize): 添加注释

* feat(tests): Mock ConversationService to return modelMessages and uiMessages for improved test coverage

- Updated the mock implementation of ConversationService in ActionUtils.test.ts to return structured modelMessages and uiMessages.
- This change enhances the test setup by providing realistic message data for testing purposes.

---------

Co-authored-by: suyao <sy20010504@gmail.com>
Co-authored-by: one <wangan.cs@gmail.com>
Co-authored-by: icarus <eurfelux@gmail.com>
2025-09-04 14:03:04 +08:00
Konv Suu
9ff4acf092 fix: regex pattern error when update manual blacklist (#9871) 2025-09-04 13:15:02 +08:00
Phantom
128b1fe9bc fix(translate): wrong copy button state (#9867) 2025-09-04 13:01:39 +08:00
Phantom
9a92372c3e refactor(mcp): use includes http to detect streamable http type mcp server (#9865)
refactor(mcp): 简化 McpServerTypeSchema 的类型校验逻辑

将联合类型替换为字符串校验并优化 http 相关类型的转换
2025-09-04 11:41:26 +08:00
Pleasure1234
0a36869b3c fix: NavigationService initialization timing issue and add tab drag reordering (#9700)
* fix: NavigationService initialization timing issue and add tab drag reordering

- Fix NavigationService timing issue in TabsService by adding fallback navigation with setTimeout
- Add drag and drop functionality for tab reordering with visual feedback
- Remove unused MessageSquareDiff icon from Navbar
- Add reorderTabs action to tabs store

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Update tabs.ts

* Update TabContainer.tsx

* Update TabContainer.tsx

* fix(dnd): horizontal sortable (#9827)

* refactor(CodeViewer): improve props, aligned to CodeEditor (#9786)

* refactor(CodeViewer): improve props, aligned to CodeEditor

* refactor: simplify internal variables

* refactor: remove default lineNumbers

* fix: shiki theme container style

* revert: use ReactMarkdown for prompt editing

* fix: draggable list id type (#9809)

* refactor(dnd): rename idKey to itemKey for clarity

* refactor: key and id type for draggable lists

* chore: update yarn lock

* fix: type error

* refactor: improve getId fallbacks

* feat: integrate file selection and upload functionality in KnowledgeFiles component (#9815)

* feat: integrate file selection and upload functionality in KnowledgeFiles component

- Added useFiles hook to manage file selection.
- Updated handleAddFile to utilize the new file selection logic, allowing multiple file uploads.
- Improved user experience by handling file uploads asynchronously and logging the results.

* feat: enhance file upload interaction in KnowledgeFiles component

- Wrapped Dragger component in a div to allow for custom click handling.
- Prevented default click behavior to improve user experience when adding files.
- Maintained existing file upload functionality while enhancing the UI interaction.

* refactor(KnowledgeFiles): 提取文件处理逻辑到独立函数

将重复的文件上传和处理逻辑提取到独立的processFiles函数中,提高代码复用性和可维护性

---------

Co-authored-by: icarus <eurfelux@gmail.com>

* fix(Sortable): correct gap and horizontal style

* feat: make tabs sortable (example)

* refactor: improve sortable direction and gap

* refactor: update example

* fix: remove useless states

---------

Co-authored-by: beyondkmp <beyondkmp@gmail.com>
Co-authored-by: icarus <eurfelux@gmail.com>
Co-authored-by: Pleasure1234 <3196812536@qq.com>

* fix: syntax error

* refactor: remove useless styles

* fix: tabs overflow, add scrollbar

* fix: button gap

* fix: app region drag

* refactor: remove scrollbar, add space for app dragging

* Revert "refactor: remove scrollbar, add space for app dragging"

This reverts commit f6ebeb143e.

* refactor: update style

* refactor: add a scroll-to-right button

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: one <wangan.cs@gmail.com>
Co-authored-by: beyondkmp <beyondkmp@gmail.com>
Co-authored-by: icarus <eurfelux@gmail.com>
2025-09-04 02:42:42 +08:00
Phantom
a9a38f88bb fix: transform parameters when adding mcp by json (#9850)
* fix(mcp): 添加 streamable_http 类型并转换为 streamableHttp

为了兼容其他配置,添加 streamable_http 类型并在解析时自动转换为 streamableHttp

* feat(mcp): 根据URL自动推断服务器类型

当未显式指定服务器类型时,通过检查URL后缀自动设置合适的类型(mcp或sse)

* feat(mcp): 添加http类型支持并映射到streamableHttp
2025-09-03 21:30:26 +08:00
SuYao
aca1fcad18 feat: enhance RichEditor with logging and improve NotesPage editor synchronization (#9817)
* feat: enhance RichEditor with logging and improve NotesPage editor synchronization

- Added logging for enhanced link setting failures in RichEditor.
- Improved content synchronization logic in NotesPage to prevent unnecessary updates and ensure cleaner state management during file switches.
- Updated markdown conversion to handle task list structures more robustly, including support for div formats in task items.
- Added tests to verify task list structure preservation during HTML to Markdown conversions.

* feat: enhance Markdown preview interaction in AssistantPromptSettings

- Added double-click functionality to toggle preview mode in the Markdown container, preserving scroll position for a smoother user experience.
2025-09-03 20:02:04 +08:00
LiuVaayne
24bc878c27 fix: correct provider URL formatting in syncModelScopeServers function (#9852) 2025-09-03 19:34:48 +08:00
one
b1a9fbc6fd refactor: tooltip icons (#9841)
* refactor: add HelpTooltip, group tooltip icons

* refactor: add a tip for preview tools

* refactor: use HelpTooltip in SettingsTab
2025-09-03 18:02:53 +08:00
Pleasure1234
8a4c635c97 refactor: migrate showWorkspace setting from global settings to notes module (#9814)
* refactor: migrate showWorkspace setting from global settings to notes module

- Move showWorkspace state from settings store to notes store for better module cohesion
- Add useShowWorkspace hook in useNotesSettings for consistent access pattern
- Add smooth animation for workspace panel show/hide transition
- Relocate save to notes action to message toolbar for better accessibility
- Add migration v146 to handle state migration for existing users

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: cli lint error

* feat: add open outside menu

* fix: hooks error

* Update useShowWorkspace.ts

* fix: update icon import in NotesSidebarHeader component

- Replaced FilePlus icon with FilePlus2 in the NotesSidebarHeader for consistency with the latest icon set.

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: suyao <sy20010504@gmail.com>
2025-09-03 17:02:24 +08:00
one
16d5f5c299 fix: capture animations and fonts in iframe (#9800)
* fix: capture animations in iframe

* fix: font urls

* fix: yarn lock

* refactor: inline fonts persistence
2025-09-03 15:11:13 +08:00
Ricardo
69a5a0434a fix: enhance Obsidian vault detection for multiple installation methods (#9821) 2025-09-03 15:08:59 +08:00
one
6d1f3a5729 fix(Markdown): regex for style (#9839) 2025-09-03 14:19:06 +08:00
co63oc
b725400428 fix typos (#9831) 2025-09-03 13:14:06 +08:00
beyondkmp
9f7d2be463 refactor(electron.vite.config.ts): streamline external dependencies and improve build configuration (#9835)
- Removed hardcoded external dependencies and replaced them with dynamic extraction from package.json.
- Cleaned up the configuration for better maintainability and flexibility in managing dependencies.
2025-09-03 12:45:33 +08:00
kangfenmao
fdee510c8c feat: add 'invalid_model' translation key across multiple languages
- Introduced a new translation key 'invalid_model' in English, Japanese, Russian, Chinese (Simplified and Traditional), Greek, Spanish, French, and Portuguese.
- Updated the SelectModelButton component to display an error tag when no valid provider is found, enhancing user feedback.
2025-09-03 11:56:57 +08:00
kangfenmao
76ac1bd8f7 fix: enhance provider selection logic in AssistantService
- Updated getProviderByModel function to improve provider selection.
- Added fallback logic to return a default or cherryin provider if the specified model provider is not found.
- Ensured that the first provider is returned as a last resort, enhancing robustness in provider retrieval.
2025-09-03 11:43:42 +08:00
beyondkmp
362658339a feat: integrate file selection and upload functionality in KnowledgeFiles component (#9815)
* feat: integrate file selection and upload functionality in KnowledgeFiles component

- Added useFiles hook to manage file selection.
- Updated handleAddFile to utilize the new file selection logic, allowing multiple file uploads.
- Improved user experience by handling file uploads asynchronously and logging the results.

* feat: enhance file upload interaction in KnowledgeFiles component

- Wrapped Dragger component in a div to allow for custom click handling.
- Prevented default click behavior to improve user experience when adding files.
- Maintained existing file upload functionality while enhancing the UI interaction.

* refactor(KnowledgeFiles): 提取文件处理逻辑到独立函数

将重复的文件上传和处理逻辑提取到独立的processFiles函数中,提高代码复用性和可维护性

---------

Co-authored-by: icarus <eurfelux@gmail.com>
2025-09-02 23:34:08 +08:00
one
925d7e2a25 fix: draggable list id type (#9809)
* refactor(dnd): rename idKey to itemKey for clarity

* refactor: key and id type for draggable lists

* chore: update yarn lock

* fix: type error

* refactor: improve getId fallbacks
2025-09-02 23:28:29 +08:00
one
089477eb1e refactor(CodeViewer): improve props, aligned to CodeEditor (#9786)
* refactor(CodeViewer): improve props, aligned to CodeEditor

* refactor: simplify internal variables

* refactor: remove default lineNumbers

* fix: shiki theme container style

* revert: use ReactMarkdown for prompt editing
2025-09-02 21:52:14 +08:00
108 changed files with 2246 additions and 485 deletions

View File

@@ -4,6 +4,8 @@ import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
import { resolve } from 'path'
import { visualizer } from 'rollup-plugin-visualizer'
import pkg from './package.json' assert { type: 'json' }
const visualizerPlugin = (type: 'renderer' | 'main') => {
return process.env[`VISUALIZER_${type.toUpperCase()}`] ? [visualizer({ open: true })] : []
}
@@ -21,25 +23,15 @@ export default defineConfig({
'@shared': resolve('packages/shared'),
'@logger': resolve('src/main/services/LoggerService'),
'@mcp-trace/trace-core': resolve('packages/mcp-trace/trace-core'),
'@mcp-trace/trace-node': resolve('packages/mcp-trace/trace-node')
'@mcp-trace/trace-node': resolve('packages/mcp-trace/trace-node'),
'@cherrystudio/ai-core/provider': resolve('packages/aiCore/src/core/providers'),
'@cherrystudio/ai-core/built-in/plugins': resolve('packages/aiCore/src/core/plugins/built-in'),
'@cherrystudio/ai-core': resolve('packages/aiCore/src')
}
},
build: {
rollupOptions: {
external: [
'@libsql/client',
'bufferutil',
'utf-8-validate',
'jsdom',
'electron',
'graceful-fs',
'selection-hook',
'@napi-rs/system-ocr',
'@strongtz/win32-arm64-msvc',
'os-proxy-config',
'sharp',
'turndown'
],
external: ['bufferutil', 'utf-8-validate', 'electron', ...Object.keys(pkg.dependencies)],
output: {
manualChunks: undefined, // 彻底禁用代码分割 - 返回 null 强制单文件打包
inlineDynamicImports: true // 内联所有动态导入,这是关键配置

View File

@@ -74,8 +74,9 @@
"@libsql/win32-x64-msvc": "^0.4.7",
"@napi-rs/system-ocr": "patch:@napi-rs/system-ocr@npm%3A1.0.2#~/.yarn/patches/@napi-rs-system-ocr-npm-1.0.2-59e7a78e8b.patch",
"@strongtz/win32-arm64-msvc": "^0.4.7",
"ai-sdk-provider-claude-code": "^1.1.3",
"express": "^5.1.0",
"graceful-fs": "^4.2.11",
"htmlparser2": "^10.0.0",
"jsdom": "26.1.0",
"node-stream-zip": "^1.15.0",
"officeparser": "^4.2.0",
@@ -170,6 +171,7 @@
"@truto/turndown-plugin-gfm": "^1.0.2",
"@tryfabric/martian": "^1.2.4",
"@types/cli-progress": "^3",
"@types/express": "^5.0.3",
"@types/fs-extra": "^11",
"@types/he": "^1",
"@types/lodash": "^4.17.5",
@@ -336,7 +338,13 @@
"pkce-challenge@npm:^4.1.0": "patch:pkce-challenge@npm%3A4.1.0#~/.yarn/patches/pkce-challenge-npm-4.1.0-fbc51695a3.patch",
"undici": "6.21.2",
"vite": "npm:rolldown-vite@latest",
"tesseract.js@npm:*": "patch:tesseract.js@npm%3A6.0.1#~/.yarn/patches/tesseract.js-npm-6.0.1-2562a7e46d.patch"
"tesseract.js@npm:*": "patch:tesseract.js@npm%3A6.0.1#~/.yarn/patches/tesseract.js-npm-6.0.1-2562a7e46d.patch",
"@img/sharp-darwin-arm64": "0.34.3",
"@img/sharp-darwin-x64": "0.34.3",
"@img/sharp-linux-arm": "0.34.3",
"@img/sharp-linux-arm64": "0.34.3",
"@img/sharp-linux-x64": "0.34.3",
"@img/sharp-win32-x64": "0.34.3"
},
"packageManager": "yarn@4.9.1",
"lint-staged": {

View File

@@ -250,6 +250,7 @@ export enum IpcChannel {
// Provider
Provider_AddKey = 'provider:add-key',
Provider_GetClaudeCodePort = 'provider:get-claude-code-port',
//Selection Assistant
Selection_TextSelected = 'selection:text-selected',

View File

@@ -2089,7 +2089,7 @@
"Design",
"Education"
],
"prompt": "I want you to act as a Graphviz DOT generator, an expert to create meaningful diagrams. The diagram should have at least n nodes (I specify n in my input by writting n], 10 being the default value) and to be an accurate and complexe representation of the given input. Each node is indexed by a number to reduce the size of the output, should not include any styling, and with layout=neato, overlap=false, node shape=rectangle] as parameters. The code should be valid, bugless and returned on a single line, without any explanation. Provide a clear and organized diagram, the relationships between the nodes have to make sense for an expert of that input. My first diagram is: \"The water cycle 8]\".\n\n",
"prompt": "I want you to act as a Graphviz DOT generator, an expert to create meaningful diagrams. The diagram should have at least n nodes (I specify n in my input by writing n], 10 being the default value) and to be an accurate and complex representation of the given input. Each node is indexed by a number to reduce the size of the output, should not include any styling, and with layout=neato, overlap=false, node shape=rectangle] as parameters. The code should be valid, bugless and returned on a single line, without any explanation. Provide a clear and organized diagram, the relationships between the nodes have to make sense for an expert of that input. My first diagram is: \"The water cycle 8]\".\n\n",
"description": "Generate meaningful charts."
},
{
@@ -2148,7 +2148,7 @@
"Career",
"Business"
],
"prompt": "Please acknowledge my following request. Please respond to me as a product manager. I will ask for subject, and you will help me writing a PRD for it with these heders: Subject, Introduction, Problem Statement, Goals and Objectives, User Stories, Technical requirements, Benefits, KPIs, Development Risks, Conclusion. Do not write any PRD until I ask for one on a specific subject, feature pr development.\n\n",
"prompt": "Please acknowledge my following request. Please respond to me as a product manager. I will ask for subject, and you will help me writing a PRD for it with these headers: Subject, Introduction, Problem Statement, Goals and Objectives, User Stories, Technical requirements, Benefits, KPIs, Development Risks, Conclusion. Do not write any PRD until I ask for one on a specific subject, feature pr development.\n\n",
"description": "Help draft the Product Requirements Document."
},
{
@@ -2159,7 +2159,7 @@
"Entertainment",
"General"
],
"prompt": "I want you to act as a drunk person. You will only answer like a very drunk person texting and nothing else. Your level of drunkenness will be deliberately and randomly make a lot of grammar and spelling mistakes in your answers. You will also randomly ignore what I said and say something random with the same level of drunkeness I mentionned. Do not write explanations on replies. My first sentence is \"how are you?",
"prompt": "I want you to act as a drunk person. You will only answer like a very drunk person texting and nothing else. Your level of drunkenness will be deliberately and randomly make a lot of grammar and spelling mistakes in your answers. You will also randomly ignore what I said and say something random with the same level of drunkenness I mentioned. Do not write explanations on replies. My first sentence is \"how are you?",
"description": "Mimic the speech pattern of a drunk person."
},
{
@@ -3517,7 +3517,7 @@
"Tools",
"Copywriting"
],
"prompt": "I want you to act as a scientific manuscript matcher. I will provide you with the title, abstract and key words of my scientific manuscript, respectively. Your task is analyzing my title, abstract and key words synthetically to find the most related, reputable journals for potential publication of my research based on an analysis of tens of millions of citation connections in database, such as Web of Science, Pubmed, Scopus, ScienceDirect and so on. You only need to provide me with the 15 most suitable journals. Your reply should include the name of journal, the cooresponding match score (The full score is ten). I want you to reply in text-based excel sheet and sort by matching scores in reverse order.\nMy title is \"XXX\" My abstract is \"XXX\" My key words are \"XXX\"\n\n",
"prompt": "I want you to act as a scientific manuscript matcher. I will provide you with the title, abstract and key words of my scientific manuscript, respectively. Your task is analyzing my title, abstract and key words synthetically to find the most related, reputable journals for potential publication of my research based on an analysis of tens of millions of citation connections in database, such as Web of Science, Pubmed, Scopus, ScienceDirect and so on. You only need to provide me with the 15 most suitable journals. Your reply should include the name of journal, the corresponding match score (The full score is ten). I want you to reply in text-based excel sheet and sort by matching scores in reverse order.\nMy title is \"XXX\" My abstract is \"XXX\" My key words are \"XXX\"\n\n",
"description": ""
},
{

View File

@@ -13,6 +13,7 @@ import installExtension, { REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS } from 'electro
import { isDev, isLinux, isWin } from './constant'
import { registerIpc } from './ipc'
import { claudeCodeService } from './services/ClaudeCodeService'
import { configManager } from './services/ConfigManager'
import mcpService from './services/MCPService'
import { nodeTraceService } from './services/NodeTraceService'
@@ -119,6 +120,14 @@ if (!app.requestSingleInstanceLock()) {
nodeTraceService.init()
// Start Claude-code HTTP service
try {
await claudeCodeService.start()
logger.info('Claude-code HTTP service started successfully')
} catch (error) {
logger.error('Failed to start Claude-code HTTP service:', error as Error)
}
app.on('activate', function () {
const mainWindow = windowService.getMainWindow()
if (!mainWindow || mainWindow.isDestroyed()) {
@@ -193,6 +202,15 @@ if (!app.requestSingleInstanceLock()) {
} catch (error) {
logger.warn('Error cleaning up MCP service:', error as Error)
}
// Stop Claude-code HTTP service
try {
await claudeCodeService.stop()
logger.info('Claude-code HTTP service stopped')
} catch (error) {
logger.warn('Error stopping Claude-code HTTP service:', error as Error)
}
// finish the logger
logger.finish()
})

View File

@@ -11,6 +11,7 @@ import { SpanEntity, TokenUsage } from '@mcp-trace/trace-core'
import { MIN_WINDOW_HEIGHT, MIN_WINDOW_WIDTH, UpgradeChannel } from '@shared/config/constant'
import { IpcChannel } from '@shared/IpcChannel'
import { FileMetadata, Provider, Shortcut, ThemeMode } from '@types'
import { claudeCodeService } from './services/ClaudeCodeService'
import { BrowserWindow, dialog, ipcMain, ProxyConfig, session, shell, systemPreferences, webContents } from 'electron'
import { Notification } from 'src/renderer/src/types/notification'
@@ -755,4 +756,9 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
// CherryIN
ipcMain.handle(IpcChannel.Cherryin_GetSignature, (_, params) => generateSignature(params))
// Provider
ipcMain.handle(IpcChannel.Provider_GetClaudeCodePort, () => {
return claudeCodeService.getPort()
})
}

View File

@@ -0,0 +1,158 @@
import { createExecutor } from '@cherrystudio/ai-core'
import { loggerService } from '@logger'
import { createClaudeCode } from 'ai-sdk-provider-claude-code'
import express, { Request, Response } from 'express'
import { Server } from 'http'
const logger = loggerService.withContext('ClaudeCodeService')
export class ClaudeCodeService {
private app: express.Application
private server: Server | null = null
private port: number = 0
private claudeCodeProvider: any = null
constructor() {
this.app = express()
this.setupMiddleware()
this.setupRoutes()
}
private setupMiddleware() {
this.app.use(express.json())
this.app.use(express.text())
}
private setupRoutes() {
// Health check endpoint
this.app.get('/health', (_req: Request, res: Response) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() })
})
// Initialize claude-code provider
this.app.post('/init', async (req: Request, res: Response) => {
try {
const config = req.body
logger.info('Initializing claude-code provider with config', config)
this.claudeCodeProvider = createClaudeCode()
res.json({
success: true,
message: 'Claude-code provider initialized successfully'
})
} catch (error) {
logger.error('Failed to initialize claude-code provider', error as Error)
res.status(500).json({
success: false,
error: (error as Error).message
})
}
})
// Stream text completion endpoint
this.app.post('/completions', async (req: Request, res: Response): Promise<void> => {
try {
if (!this.claudeCodeProvider) {
res.status(400).json({
success: false,
error: 'Claude-code provider not initialized. Call /init first.'
})
return
}
const { modelId, params, options } = req.body
logger.info('Processing completions request', { modelId, hasParams: !!params })
// 创建执行器
const executor = createExecutor('claude-code', options || {}, [])
const model = this.claudeCodeProvider.languageModel('opus')
// 执行流式文本生成
const result = await executor.streamText({
...params,
model,
abortSignal: new AbortController().signal
})
console.log('result', result)
// 使用 AI SDK 提供的便捷函数处理流式响应
result.pipeUIMessageStreamToResponse(res)
logger.info('Completions request completed successfully')
} catch (error) {
logger.error('Error in completions endpoint', error as Error)
if (!res.headersSent) {
res.status(500).json({
success: false,
error: (error as Error).message
})
}
}
})
}
public async start(): Promise<number> {
return new Promise((resolve, reject) => {
// 尝试使用固定端口,如果失败则使用系统分配端口
const preferredPort = 23456
this.server = this.app.listen(preferredPort, 'localhost', () => {
if (this.server?.address()) {
this.port = (this.server.address() as any)?.port || 0
logger.info(`Claude-code HTTP service started on port ${this.port}`)
resolve(this.port)
} else {
reject(new Error('Failed to start server'))
}
})
this.server.on('error', (error: any) => {
if (error.code === 'EADDRINUSE') {
logger.warn(`Port ${preferredPort} is in use, trying with dynamic port`)
// 如果固定端口被占用,使用动态端口
this.server = this.app.listen(0, 'localhost', () => {
if (this.server?.address()) {
this.port = (this.server.address() as any)?.port || 0
logger.info(`Claude-code HTTP service started on dynamic port ${this.port}`)
resolve(this.port)
} else {
reject(new Error('Failed to start server'))
}
})
this.server.on('error', (dynamicError) => {
logger.error('Server error on dynamic port', dynamicError)
reject(dynamicError)
})
} else {
logger.error('Server error', error)
reject(error)
}
})
})
}
public async stop(): Promise<void> {
return new Promise((resolve) => {
if (this.server) {
this.server.close(() => {
logger.info('Claude-code HTTP service stopped')
resolve()
})
} else {
resolve()
}
})
}
public getPort(): number {
return this.port
}
public isRunning(): boolean {
return this.server !== null && this.server.listening
}
}
// 单例实例
export const claudeCodeService = new ClaudeCodeService()

View File

@@ -32,7 +32,8 @@ class ObsidianVaultService {
)
} else {
// Linux
this.obsidianConfigPath = path.join(app.getPath('home'), '.config', 'obsidian', 'obsidian.json')
this.obsidianConfigPath = this.resolveLinuxObsidianConfigPath()
logger.debug(`Resolved Obsidian config path (linux): ${this.obsidianConfigPath}`)
}
}
@@ -164,6 +165,57 @@ class ObsidianVaultService {
return []
}
}
/**
* 在 Linux 下解析 Obsidian 配置文件路径,兼容多种安装方式。
* 优先返回第一个存在的路径;若均不存在,则返回 XDG 默认路径。
*/
private resolveLinuxObsidianConfigPath(): string {
const home = app.getPath('home')
const xdgConfigHome = process.env.XDG_CONFIG_HOME || path.join(home, '.config')
// 常见目录名与文件名大小写差异做兼容
const configDirs = ['obsidian', 'Obsidian']
const fileNames = ['obsidian.json', 'Obsidian.json']
const candidates: string[] = []
// 1) AppImage/DEBXDG 标准路径)
for (const dir of configDirs) {
for (const file of fileNames) {
candidates.push(path.join(xdgConfigHome, dir, file))
}
}
// 2) Snap 安装:
// - 常见:~/snap/obsidian/current/.config/obsidian/obsidian.json
// - 兼容:~/snap/obsidian/common/.config/obsidian/obsidian.json
for (const dir of configDirs) {
for (const file of fileNames) {
candidates.push(path.join(home, 'snap', 'obsidian', 'current', '.config', dir, file))
candidates.push(path.join(home, 'snap', 'obsidian', 'common', '.config', dir, file))
}
}
// 3) Flatpak 安装:~/.var/app/md.obsidian.Obsidian/config/obsidian/obsidian.json
for (const dir of configDirs) {
for (const file of fileNames) {
candidates.push(path.join(home, '.var', 'app', 'md.obsidian.Obsidian', 'config', dir, file))
}
}
const existing = candidates.find((p) => {
try {
return fs.existsSync(p)
} catch {
return false
}
})
if (existing) return existing
return path.join(xdgConfigHome, 'obsidian', 'obsidian.json')
}
}
export default ObsidianVaultService

View File

@@ -437,6 +437,9 @@ const api = {
cherryin: {
generateSignature: (params: { method: string; path: string; query: string; body: Record<string, any> }) =>
ipcRenderer.invoke(IpcChannel.Cherryin_GetSignature, params)
},
provider: {
getClaudeCodePort: () => ipcRenderer.invoke(IpcChannel.Provider_GetClaudeCodePort)
}
}

View File

@@ -53,6 +53,54 @@ export class AiSdkToChunkAdapter {
return await aiSdkResult.text
}
/**
* 直接处理单个 chunk 数据
* @param chunk AI SDK 的 chunk 数据
*/
async processChunk(response: ReadableStream<TextStreamPart<any>>): Promise<void> {
const reader = response.getReader()
const final = {
text: '',
reasoningContent: '',
webSearchResults: [],
reasoningId: ''
}
try {
let buffer = ''
const decoder = new TextDecoder()
while (true) {
const { done, value } = await reader.read()
if (done) break
const chunk = decoder.decode(value, { stream: true })
buffer += chunk
// 按行处理 SSE 数据
const lines = buffer.split('\n')
buffer = lines.pop() || '' // 保留最后一行(可能不完整)
for (const line of lines) {
if (line.startsWith('data: ')) {
const dataStr = line.slice(6) // 移除 "data: " 前缀
if (dataStr === '[DONE]') {
break
}
try {
const data = JSON.parse(dataStr)
this.convertAndEmitChunk(data, final)
} catch (parseError) {
// 忽略无法解析的数据
// logger.debug('Failed to parse streamed data:', parseError as Error, line)
}
}
}
}
} finally {
reader.releaseLock()
}
}
/**
* 读取 fullStream 并转换为 Cherry Studio chunks
* @param fullStream AI SDK 的 fullStream (ReadableStream)
@@ -90,6 +138,7 @@ export class AiSdkToChunkAdapter {
final: { text: string; reasoningContent: string; webSearchResults: any[]; reasoningId: string }
) {
logger.info(`AI SDK chunk type: ${chunk.type}`, chunk)
console.log('final', final)
switch (chunk.type) {
// === 文本相关事件 ===
case 'text-start':
@@ -99,7 +148,7 @@ export class AiSdkToChunkAdapter {
break
case 'text-delta':
if (this.accumulate) {
final.text += chunk.text || ''
final.text += chunk.delta || ''
} else {
final.text = chunk.text || ''
}
@@ -232,13 +281,13 @@ export class AiSdkToChunkAdapter {
text: final.text || '',
reasoning_content: final.reasoningContent || '',
usage: {
completion_tokens: chunk.totalUsage.outputTokens || 0,
prompt_tokens: chunk.totalUsage.inputTokens || 0,
total_tokens: chunk.totalUsage.totalTokens || 0
completion_tokens: chunk?.totalUsage?.outputTokens || 0,
prompt_tokens: chunk?.totalUsage?.inputTokens || 0,
total_tokens: chunk?.totalUsage?.totalTokens || 0
},
metrics: chunk.totalUsage
metrics: chunk?.totalUsage
? {
completion_tokens: chunk.totalUsage.outputTokens || 0,
completion_tokens: chunk?.totalUsage?.outputTokens || 0,
time_completion_millsec: 0
}
: undefined
@@ -250,13 +299,13 @@ export class AiSdkToChunkAdapter {
text: final.text || '',
reasoning_content: final.reasoningContent || '',
usage: {
completion_tokens: chunk.totalUsage.outputTokens || 0,
prompt_tokens: chunk.totalUsage.inputTokens || 0,
total_tokens: chunk.totalUsage.totalTokens || 0
completion_tokens: chunk?.totalUsage?.outputTokens || 0,
prompt_tokens: chunk?.totalUsage?.inputTokens || 0,
total_tokens: chunk?.totalUsage?.totalTokens || 0
},
metrics: chunk.totalUsage
metrics: chunk?.totalUsage
? {
completion_tokens: chunk.totalUsage.outputTokens || 0,
completion_tokens: chunk?.totalUsage?.outputTokens || 0,
time_completion_millsec: 0
}
: undefined

View File

@@ -9,18 +9,16 @@
import { createExecutor } from '@cherrystudio/ai-core'
import { loggerService } from '@logger'
import { isNotSupportedImageSizeModel } from '@renderer/config/models'
import { getEnableDeveloperMode } from '@renderer/hooks/useSettings'
import { addSpan, endSpan } from '@renderer/services/SpanManagerService'
import { StartSpanParams } from '@renderer/trace/types/ModelSpanEntity'
import type { Assistant, GenerateImageParams, Model, Provider } from '@renderer/types'
import type { AiSdkModel, StreamTextParams } from '@renderer/types/aiCoreTypes'
import { ChunkType } from '@renderer/types/chunk'
import { type ImageModel, type LanguageModel, type Provider as AiSdkProvider, wrapLanguageModel } from 'ai'
import AiSdkToChunkAdapter from './chunk/AiSdkToChunkAdapter'
import LegacyAiProvider from './legacy/index'
import { CompletionsResult } from './legacy/middleware/schemas'
import { CompletionsParams, CompletionsResult } from './legacy/middleware/schemas'
import { AiSdkMiddlewareConfig, buildAiSdkMiddlewares } from './middleware/AiSdkMiddlewareBuilder'
import { buildPlugins } from './plugins/PluginBuilder'
import { createAiSdkProvider } from './provider/factory'
@@ -92,6 +90,11 @@ export default class ModernAiProvider {
// 准备特殊配置
await prepareSpecialProviderConfig(this.actualProvider, this.config)
// 特殊处理 claude-code provider通过本地 HTTP 服务器
// if (this.config.providerId === 'claude-code') {
return await this._completionsViaHttpService(modelId, params, config)
// }
// 提前创建本地 provider 实例
if (!this.localProvider) {
this.localProvider = await createAiSdkProvider(this.config)
@@ -140,7 +143,24 @@ export default class ModernAiProvider {
config: ModernAiProviderConfig
): Promise<CompletionsResult> {
if (config.isImageGenerationEndpoint) {
return await this.modernImageGeneration(model as ImageModel, params, config)
// 使用 legacy 实现处理图像生成(支持图片编辑等高级功能)
if (!config.uiMessages) {
throw new Error('uiMessages is required for image generation endpoint')
}
const legacyParams: CompletionsParams = {
callType: 'chat',
messages: config.uiMessages, // 使用原始的 UI 消息格式
assistant: config.assistant,
streamOutput: config.streamOutput ?? true,
onChunk: config.onChunk,
topicId: config.topicId,
mcpTools: config.mcpTools,
enableWebSearch: config.enableWebSearch
}
// 调用 legacy 的 completions会自动使用 ImageGenerationMiddleware
return await this.legacyProvider.completions(legacyParams)
}
return await this.modernCompletions(model as LanguageModel, params, config)
@@ -231,6 +251,79 @@ export default class ModernAiProvider {
}
}
/**
* 通过本地 HTTP 服务器处理 claude-code completions
*/
private async _completionsViaHttpService(
modelId: string,
params: StreamTextParams,
config: ModernAiProviderConfig
): Promise<CompletionsResult> {
logger.info('Starting claude-code completions via HTTP service', {
modelId,
providerId: this.config!.providerId,
topicId: config.topicId,
hasOnChunk: !!config.onChunk
})
try {
// 初始化 claude-code provider
const initResponse = await fetch('http://localhost:' + (await this.getClaudeCodePort()) + '/init', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(this.config!.options)
})
if (!initResponse.ok) {
throw new Error(`Failed to initialize claude-code provider: ${initResponse.statusText}`)
}
// 发送 completions 请求
const completionsResponse = await fetch('http://localhost:' + (await this.getClaudeCodePort()) + '/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
modelId,
params,
options: this.config!.options
})
})
if (!completionsResponse.ok) {
throw new Error(`Failed to get completions: ${completionsResponse.statusText}`)
}
let finalText = ''
if (config.onChunk && completionsResponse.body) {
// 创建 adapter 来处理 chunk 数据
const accumulate = this.model!.supported_text_delta !== false
const adapter = new AiSdkToChunkAdapter(config.onChunk, config.mcpTools, accumulate)
await adapter.processChunk(completionsResponse.body)
} else {
finalText = await completionsResponse.text()
}
return {
getText: () => finalText
}
} catch (error) {
logger.error('Error in claude-code HTTP service completions', error as Error)
throw error
}
}
/**
* 获取 Claude-code HTTP 服务端口
*/
private async getClaudeCodePort(): Promise<number> {
return await window.api.provider.getClaudeCodePort()
}
/**
* 使用现代化AI SDK的completions实现
*/
@@ -290,7 +383,9 @@ export default class ModernAiProvider {
/**
* 使用现代化 AI SDK 的图像生成实现,支持流式输出
* @deprecated 已改为使用 legacy 实现以支持图片编辑等高级功能
*/
/*
private async modernImageGeneration(
model: ImageModel,
params: StreamTextParams,
@@ -407,6 +502,7 @@ export default class ModernAiProvider {
throw error
}
}
*/
// 代理其他方法到原有实现
public async models() {

View File

@@ -1,5 +1,5 @@
import { loggerService } from '@logger'
import type { MCPTool, Model, Provider } from '@renderer/types'
import type { MCPTool, Message, Model, Provider } from '@renderer/types'
import type { Chunk } from '@renderer/types/chunk'
import { extractReasoningMiddleware, LanguageModelMiddleware, simulateStreamingMiddleware } from 'ai'
@@ -23,6 +23,7 @@ export interface AiSdkMiddlewareConfig {
enableWebSearch: boolean
enableGenerateImage: boolean
mcpTools?: MCPTool[]
uiMessages?: Message[]
}
/**

View File

@@ -6,7 +6,6 @@
import { loggerService } from '@logger'
import { isVisionModel } from '@renderer/config/models'
import type { Message, Model } from '@renderer/types'
import type { StreamTextParams } from '@renderer/types/aiCoreTypes'
import { FileMessageBlock, ImageMessageBlock, ThinkingMessageBlock } from '@renderer/types/newMessage'
import {
findFileBlocks,
@@ -154,11 +153,8 @@ async function convertMessageToAssistantModelMessage(
/**
* 转换 Cherry Studio 消息数组为 AI SDK 消息数组
*/
export async function convertMessagesToSdkMessages(
messages: Message[],
model: Model
): Promise<StreamTextParams['messages']> {
const sdkMessages: StreamTextParams['messages'] = []
export async function convertMessagesToSdkMessages(messages: Message[], model: Model): Promise<ModelMessage[]> {
const sdkMessages: ModelMessage[] = []
const isVision = isVisionModel(model)
for (const message of messages) {

View File

@@ -76,6 +76,17 @@ export function getAiSdkProviderId(provider: Provider): ProviderId | 'openai-com
export async function createAiSdkProvider(config) {
let localProvider: Awaited<AiSdkProvider> | null = null
try {
// 特殊处理 claude-code provider通过 IPC 在主线程中创建
// if (config.providerId === 'claude-code') {
localProvider = await window.api.provider.createClaudeCode()
logger.debug('Claude-code provider created via IPC', {
providerId: config.providerId,
hasOptions: !!config.options
})
console.log('localProvider', localProvider)
return localProvider
// }
if (config.providerId === 'openai' && config.options?.mode === 'chat') {
config.providerId = `${config.providerId}-chat`
} else if (config.providerId === 'azure' && config.options?.mode === 'responses') {

View File

@@ -92,7 +92,6 @@ function formatProviderApiHost(provider: Provider): Provider {
*/
export function getActualProvider(model: Model): Provider {
const baseProvider = getProviderByModel(model)
// 按顺序处理各种转换
let actualProvider = cloneDeep(baseProvider)
actualProvider = handleSpecialProviders(model, actualProvider)

View File

@@ -257,12 +257,13 @@ export const CodeBlockView: React.FC<Props> = memo(({ children, language, onSave
) : (
<CodeViewer
className="source-view"
value={children}
language={language}
onHeightChange={handleHeightChange}
expanded={shouldExpand}
wrapped={shouldWrap}
onHeightChange={handleHeightChange}>
{children}
</CodeViewer>
maxHeight={`${MAX_COLLAPSED_CODE_HEIGHT}px`}
/>
),
[children, codeEditor.enabled, handleHeightChange, language, onSave, shouldExpand, shouldWrap]
)

View File

@@ -48,8 +48,6 @@ export interface CodeEditorProps {
maxHeight?: string
/** Minimum editor height. */
minHeight?: string
/** Font size that overrides the app setting. */
fontSize?: string
/** Editor options that extend BasicSetupOptions. */
options?: {
/**
@@ -70,6 +68,8 @@ export interface CodeEditorProps {
} & BasicSetupOptions
/** Additional extensions for CodeMirror. */
extensions?: Extension[]
/** Font size that overrides the app setting. */
fontSize?: number
/** Style overrides for the editor, passed directly to CodeMirror's style property. */
style?: React.CSSProperties
/** CSS class name appended to the default `code-editor` class. */
@@ -108,9 +108,9 @@ const CodeEditor = ({
height,
maxHeight,
minHeight,
fontSize,
options,
extensions,
fontSize: customFontSize,
style,
className,
editable = true,
@@ -121,7 +121,7 @@ const CodeEditor = ({
const enableKeymap = useMemo(() => options?.keymap ?? codeEditor.keymap, [options?.keymap, codeEditor.keymap])
// 合并 codeEditor 和 options 的 basicSetupoptions 优先
const customBasicSetup = useMemo(() => {
const basicSetup = useMemo(() => {
return {
lineNumbers: _lineNumbers,
...(codeEditor as BasicSetupOptions),
@@ -129,7 +129,7 @@ const CodeEditor = ({
}
}, [codeEditor, _lineNumbers, options])
const customFontSize = useMemo(() => fontSize ?? `${_fontSize - 1}px`, [fontSize, _fontSize])
const fontSize = useMemo(() => customFontSize ?? _fontSize - 1, [customFontSize, _fontSize])
const { activeCmTheme } = useCodeStyle()
const initialContent = useRef(options?.stream ? (value ?? '').trimEnd() : (value ?? ''))
@@ -214,10 +214,10 @@ const CodeEditor = ({
foldKeymap: enableKeymap,
completionKeymap: enableKeymap,
lintKeymap: enableKeymap,
...customBasicSetup // override basicSetup
...basicSetup // override basicSetup
}}
style={{
fontSize: customFontSize,
fontSize,
marginTop: 0,
borderRadius: 'inherit',
...style

View File

@@ -1,4 +1,3 @@
import { MAX_COLLAPSED_CODE_HEIGHT } from '@renderer/config/constant'
import { useCodeStyle } from '@renderer/context/CodeStyleProvider'
import { useCodeHighlight } from '@renderer/hooks/useCodeHighlight'
import { useSettings } from '@renderer/hooks/useSettings'
@@ -11,13 +10,48 @@ import { ThemedToken } from 'shiki/core'
import styled from 'styled-components'
interface CodeViewerProps {
/** Code string value. */
value: string
/**
* Code language string.
* - Case-insensitive.
* - Supports common names: javascript, json, python, etc.
* - Supports shiki aliases: c#/csharp, objective-c++/obj-c++/objc++, etc.
*/
language: string
children: React.ReactNode
expanded?: boolean
wrapped?: boolean
onHeightChange?: (scrollHeight: number) => void
className?: string
/**
* Height of the scroll container.
* Only works when expanded is false.
*/
height?: string | number
/**
* Maximum height of the scroll container.
* Only works when expanded is false.
*/
maxHeight?: string | number
/** Viewer options. */
options?: {
/**
* Whether to show line numbers.
*/
lineNumbers?: boolean
}
/** Font size that overrides the app setting. */
fontSize?: number
/** CSS class name appended to the default `code-viewer` class. */
className?: string
/**
* Whether the editor is expanded.
* If true, the height and maxHeight props are ignored.
* @default true
*/
expanded?: boolean
/**
* Whether the code lines are wrapped.
* @default true
*/
wrapped?: boolean
}
/**
@@ -26,19 +60,33 @@ interface CodeViewerProps {
* - 使用虚拟滚动和按需高亮,改善页面内有大量长代码块时的响应
* - 并发安全
*/
const CodeViewer = ({ children, language, expanded, wrapped, onHeightChange, className, height }: CodeViewerProps) => {
const { codeShowLineNumbers, fontSize } = useSettings()
const CodeViewer = ({
value,
language,
height,
maxHeight,
onHeightChange,
options,
fontSize: customFontSize,
className,
expanded = true,
wrapped = true
}: CodeViewerProps) => {
const { codeShowLineNumbers: _lineNumbers, fontSize: _fontSize } = useSettings()
const { getShikiPreProperties, isShikiThemeDark } = useCodeStyle()
const shikiThemeRef = useRef<HTMLDivElement>(null)
const scrollerRef = useRef<HTMLDivElement>(null)
const callerId = useRef(`${Date.now()}-${uuid()}`).current
const rawLines = useMemo(() => (typeof children === 'string' ? children.trimEnd().split('\n') : []), [children])
const fontSize = useMemo(() => customFontSize ?? _fontSize - 1, [customFontSize, _fontSize])
const lineNumbers = useMemo(() => options?.lineNumbers ?? _lineNumbers, [options?.lineNumbers, _lineNumbers])
const rawLines = useMemo(() => (typeof value === 'string' ? value.trimEnd().split('\n') : []), [value])
// 计算行号数字位数
const gutterDigits = useMemo(
() => (codeShowLineNumbers ? Math.max(rawLines.length.toString().length, 1) : 0),
[codeShowLineNumbers, rawLines.length]
() => (lineNumbers ? Math.max(rawLines.length.toString().length, 1) : 0),
[lineNumbers, rawLines.length]
)
// 设置 pre 标签属性
@@ -68,7 +116,7 @@ const CodeViewer = ({ children, language, expanded, wrapped, onHeightChange, cla
const getScrollElement = useCallback(() => scrollerRef.current, [])
const getItemKey = useCallback((index: number) => `${callerId}-${index}`, [callerId])
// `line-height: 1.6` 为全局样式,但是为了避免测量误差在这里取整
const estimateSize = useCallback(() => Math.round((fontSize - 1) * 1.6), [fontSize])
const estimateSize = useCallback(() => Math.round(fontSize * 1.6), [fontSize])
// 创建 virtualizer 实例
const virtualizer = useVirtualizer({
@@ -105,20 +153,19 @@ const CodeViewer = ({ children, language, expanded, wrapped, onHeightChange, cla
}, [rawLines.length, onHeightChange])
return (
<div ref={shikiThemeRef} style={height ? { height } : undefined}>
<div ref={shikiThemeRef} style={expanded ? undefined : { height }}>
<ScrollContainer
ref={scrollerRef}
className="shiki-scroller"
$wrap={wrapped}
$expanded={expanded}
$expand={expanded}
$lineHeight={estimateSize()}
$height={height}
style={
{
'--gutter-width': `${gutterDigits}ch`,
fontSize: `${fontSize - 1}px`,
maxHeight: expanded ? undefined : height ? undefined : MAX_COLLAPSED_CODE_HEIGHT,
height: height,
fontSize,
height: expanded ? undefined : height,
maxHeight: expanded ? undefined : maxHeight,
overflowY: expanded ? 'hidden' : 'auto'
} as React.CSSProperties
}>
@@ -142,7 +189,7 @@ const CodeViewer = ({ children, language, expanded, wrapped, onHeightChange, cla
<VirtualizedRow
rawLine={rawLines[virtualItem.index]}
tokenLine={tokenLines[virtualItem.index]}
showLineNumbers={codeShowLineNumbers}
showLineNumbers={lineNumbers}
index={virtualItem.index}
/>
</div>
@@ -226,9 +273,8 @@ VirtualizedRow.displayName = 'VirtualizedRow'
const ScrollContainer = styled.div<{
$wrap?: boolean
$expanded?: boolean
$expand?: boolean
$lineHeight?: number
$height?: string | number
}>`
display: block;
overflow-x: auto;
@@ -244,7 +290,7 @@ const ScrollContainer = styled.div<{
line-height: ${(props) => props.$lineHeight}px;
/* contain 优化 wrap 时滚动性能will-change 优化 unwrap 时滚动性能 */
contain: ${(props) => (props.$wrap ? 'content' : 'none')};
will-change: ${(props) => (!props.$wrap && !props.$expanded ? 'transform' : 'auto')};
will-change: ${(props) => (!props.$wrap && !props.$expand ? 'transform' : 'auto')};
.line-number {
width: var(--gutter-width, 1.2ch);

View File

@@ -71,8 +71,9 @@ describe('DraggableList', () => {
})
it('should render nothing when list is empty', () => {
const emptyList: Array<{ id: string; name: string }> = []
render(
<DraggableList list={[]} onUpdate={() => {}}>
<DraggableList list={emptyList} onUpdate={() => {}}>
{(item) => <div data-testid="item">{item.name}</div>}
</DraggableList>
)

View File

@@ -33,7 +33,7 @@ describe('useDraggableReorder', () => {
originalList: mockOriginalList,
filteredList: mockOriginalList, // 列表未过滤
onUpdate,
idKey: 'id'
itemKey: 'id'
})
)
@@ -61,7 +61,7 @@ describe('useDraggableReorder', () => {
originalList: mockOriginalList,
filteredList,
onUpdate,
idKey: 'id'
itemKey: 'id'
})
)
@@ -89,7 +89,7 @@ describe('useDraggableReorder', () => {
originalList: mockOriginalList,
filteredList: mockOriginalList,
onUpdate,
idKey: 'id'
itemKey: 'id'
})
)
@@ -110,7 +110,7 @@ describe('useDraggableReorder', () => {
originalList: mockOriginalList,
filteredList: mockOriginalList,
onUpdate,
idKey: 'id'
itemKey: 'id'
})
)
@@ -136,7 +136,7 @@ describe('useDraggableReorder', () => {
originalList: mockOriginalList,
filteredList,
onUpdate,
idKey: 'id'
itemKey: 'id'
})
)

View File

@@ -9,7 +9,7 @@ import {
ResponderProvided
} from '@hello-pangea/dnd'
import { droppableReorder } from '@renderer/utils'
import { FC, HTMLAttributes } from 'react'
import { HTMLAttributes, Key, useCallback } from 'react'
interface Props<T> {
list: T[]
@@ -17,23 +17,25 @@ interface Props<T> {
listStyle?: React.CSSProperties
listProps?: HTMLAttributes<HTMLDivElement>
children: (item: T, index: number) => React.ReactNode
itemKey?: keyof T | ((item: T) => Key)
onUpdate: (list: T[]) => void
onDragStart?: OnDragStartResponder
onDragEnd?: OnDragEndResponder
droppableProps?: Partial<DroppableProps>
}
const DraggableList: FC<Props<any>> = ({
function DraggableList<T>({
children,
list,
style,
listStyle,
listProps,
itemKey,
droppableProps,
onDragStart,
onUpdate,
onDragEnd
}) => {
}: Props<T>) {
const _onDragEnd = (result: DropResult, provided: ResponderProvided) => {
onDragEnd?.(result, provided)
if (result.destination) {
@@ -46,6 +48,17 @@ const DraggableList: FC<Props<any>> = ({
}
}
const getId = useCallback(
(item: T) => {
if (typeof itemKey === 'function') return itemKey(item)
if (itemKey) return item[itemKey] as Key
if (typeof item === 'string') return item as Key
if (item && typeof item === 'object' && 'id' in item) return item.id as Key
return undefined
},
[itemKey]
)
return (
<DragDropContext onDragStart={onDragStart} onDragEnd={_onDragEnd}>
<Droppable droppableId="droppable" {...droppableProps}>
@@ -53,9 +66,9 @@ const DraggableList: FC<Props<any>> = ({
<div {...provided.droppableProps} ref={provided.innerRef} style={style}>
<div {...listProps} className="draggable-list-container">
{list.map((item, index) => {
const id = item.id || item
const draggableId = String(getId(item) ?? index)
return (
<Draggable key={`draggable_${id}_${index}`} draggableId={id} index={index}>
<Draggable key={`draggable_${draggableId}`} draggableId={draggableId} index={index}>
{(provided) => (
<div
ref={provided.innerRef}

View File

@@ -9,7 +9,7 @@ interface UseDraggableReorderParams<T> {
/** 用于更新原始列表状态的函数 */
onUpdate: (newList: T[]) => void
/** 用于从列表项中获取唯一ID的属性名或函数 */
idKey: keyof T | ((item: T) => Key)
itemKey: keyof T | ((item: T) => Key)
}
/**
@@ -19,8 +19,16 @@ interface UseDraggableReorderParams<T> {
* @param params - { originalList, filteredList, onUpdate, idKey }
* @returns 返回可以直接传递给 DraggableVirtualList 的 props: { onDragEnd, itemKey }
*/
export function useDraggableReorder<T>({ originalList, filteredList, onUpdate, idKey }: UseDraggableReorderParams<T>) {
const getId = useCallback((item: T) => (typeof idKey === 'function' ? idKey(item) : (item[idKey] as Key)), [idKey])
export function useDraggableReorder<T>({
originalList,
filteredList,
onUpdate,
itemKey
}: UseDraggableReorderParams<T>) {
const getId = useCallback(
(item: T) => (typeof itemKey === 'function' ? itemKey(item) : (item[itemKey] as Key)),
[itemKey]
)
// 创建从 item ID 到其在 *原始列表* 中索引的映射
const itemIndexMap = useMemo(() => {

View File

@@ -208,7 +208,7 @@ const VirtualRow = memo(
const draggableId = String(virtualItem.key)
return (
<Draggable
key={`draggable_${draggableId}_${virtualItem.index}`}
key={`draggable_${draggableId}`}
draggableId={draggableId}
isDragDisabled={disabled}
index={virtualItem.index}>

View File

@@ -56,6 +56,7 @@ const MermaidPreview = ({
document.body.removeChild(measureEl)
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[diagramId, mermaid, forceRenderKey]
)

View File

@@ -1,3 +1,4 @@
import { loggerService } from '@logger'
import { ContentSearch, type ContentSearchRef } from '@renderer/components/ContentSearch'
import DragHandle from '@tiptap/extension-drag-handle-react'
import { EditorContent } from '@tiptap/react'
@@ -26,6 +27,7 @@ import { ToC } from './TableOfContent'
import { Toolbar } from './toolbar'
import type { FormattingCommand, RichEditorProps, RichEditorRef } from './types'
import { useRichEditor } from './useRichEditor'
const logger = loggerService.withContext('RichEditor')
const RichEditor = ({
ref,
@@ -290,6 +292,7 @@ const RichEditor = ({
const end = $from.end()
editor.chain().focus().setTextSelection({ from: start, to: end }).setEnhancedLink({ href: url }).run()
} catch (error) {
logger.warn('Failed to set enhanced link:', error as Error)
editor.chain().focus().toggleEnhancedLink({ href: '' }).run()
}
} else {

View File

@@ -1,4 +1,7 @@
import { PlusOutlined } from '@ant-design/icons'
import { TopNavbarOpenedMinappTabs } from '@renderer/components/app/PinnedMinapps'
import { Sortable, useDndReorder } from '@renderer/components/dnd'
import Scrollbar from '@renderer/components/Scrollbar'
import { isLinux, isMac, isWin } from '@renderer/config/constant'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useFullscreen } from '@renderer/hooks/useFullscreen'
@@ -7,11 +10,12 @@ import { getThemeModeLabel, getTitleLabel } from '@renderer/i18n/label'
import tabsService from '@renderer/services/TabsService'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import type { Tab } from '@renderer/store/tabs'
import { addTab, removeTab, setActiveTab } from '@renderer/store/tabs'
import { addTab, removeTab, setActiveTab, setTabs } from '@renderer/store/tabs'
import { ThemeMode } from '@renderer/types'
import { classNames } from '@renderer/utils'
import { Tooltip } from 'antd'
import { Button, Tooltip } from 'antd'
import {
ChevronRight,
FileSearch,
Folder,
Hammer,
@@ -28,13 +32,11 @@ import {
Terminal,
X
} from 'lucide-react'
import { useCallback, useEffect } from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useLocation, useNavigate } from 'react-router-dom'
import styled from 'styled-components'
import { TopNavbarOpenedMinappTabs } from '../app/PinnedMinapps'
interface TabsContainerProps {
children: React.ReactNode
}
@@ -81,6 +83,8 @@ const TabsContainer: React.FC<TabsContainerProps> = ({ children }) => {
const { settedTheme, toggleTheme } = useTheme()
const { hideMinappPopup } = useMinappPopup()
const { t } = useTranslation()
const scrollRef = useRef<HTMLDivElement>(null)
const [canScroll, setCanScroll] = useState(false)
const getTabId = (path: string): string => {
if (path === '/') return 'home'
@@ -142,34 +146,83 @@ const TabsContainer: React.FC<TabsContainerProps> = ({ children }) => {
navigate(tab.path)
}
const handleScrollRight = () => {
scrollRef.current?.scrollBy({ left: 200, behavior: 'smooth' })
}
useEffect(() => {
const scrollElement = scrollRef.current
if (!scrollElement) return
const checkScrollability = () => {
setCanScroll(scrollElement.scrollWidth > scrollElement.clientWidth)
}
checkScrollability()
const resizeObserver = new ResizeObserver(checkScrollability)
resizeObserver.observe(scrollElement)
window.addEventListener('resize', checkScrollability)
return () => {
resizeObserver.disconnect()
window.removeEventListener('resize', checkScrollability)
}
}, [tabs])
const visibleTabs = useMemo(() => tabs.filter((tab) => !specialTabs.includes(tab.id)), [tabs])
const { onSortEnd } = useDndReorder<Tab>({
originalList: tabs,
filteredList: visibleTabs,
onUpdate: (newTabs) => dispatch(setTabs(newTabs)),
itemKey: 'id'
})
return (
<Container>
<TabsBar $isFullscreen={isFullscreen}>
{tabs
.filter((tab) => !specialTabs.includes(tab.id))
.map((tab) => {
return (
<Tab key={tab.id} active={tab.id === activeTabId} onClick={() => handleTabClick(tab)}>
<TabHeader>
{tab.id && <TabIcon>{getTabIcon(tab.id)}</TabIcon>}
<TabTitle>{getTitleLabel(tab.id)}</TabTitle>
</TabHeader>
{tab.id !== 'home' && (
<CloseButton
className="close-button"
onClick={(e) => {
e.stopPropagation()
closeTab(tab.id)
}}>
<X size={12} />
</CloseButton>
)}
</Tab>
)
})}
<AddTabButton onClick={handleAddTab} className={classNames({ active: activeTabId === 'launchpad' })}>
<PlusOutlined />
</AddTabButton>
<TabsArea>
<TabsScroll ref={scrollRef}>
<Sortable
items={visibleTabs}
itemKey="id"
layout="list"
horizontal
gap={'6px'}
onSortEnd={onSortEnd}
className="tabs-sortable"
renderItem={(tab) => (
<Tab key={tab.id} active={tab.id === activeTabId} onClick={() => handleTabClick(tab)}>
<TabHeader>
{tab.id && <TabIcon>{getTabIcon(tab.id)}</TabIcon>}
<TabTitle>{getTitleLabel(tab.id)}</TabTitle>
</TabHeader>
{tab.id !== 'home' && (
<CloseButton
className="close-button"
data-no-dnd
onClick={(e) => {
e.stopPropagation()
closeTab(tab.id)
}}>
<X size={12} />
</CloseButton>
)}
</Tab>
)}
/>
</TabsScroll>
{canScroll && (
<ScrollButton onClick={handleScrollRight} className="scroll-right-button" shape="circle" size="small">
<ChevronRight size={16} />
</ScrollButton>
)}
<AddTabButton onClick={handleAddTab} className={classNames({ active: activeTabId === 'launchpad' })}>
<PlusOutlined />
</AddTabButton>
</TabsArea>
<RightButtonsContainer>
<TopNavbarOpenedMinappTabs />
<Tooltip
@@ -200,6 +253,7 @@ const Container = styled.div`
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
`
const TabsBar = styled.div<{ $isFullscreen: boolean }>`
@@ -221,6 +275,34 @@ const TabsBar = styled.div<{ $isFullscreen: boolean }>`
}
`
const TabsArea = styled.div`
display: flex;
align-items: center;
flex: 1 1 auto;
min-width: 0;
gap: 6px;
padding-right: 2rem;
position: relative;
-webkit-app-region: drag;
> * {
-webkit-app-region: no-drag;
}
&:hover {
.scroll-right-button {
opacity: 1;
}
}
`
const TabsScroll = styled(Scrollbar)`
&::-webkit-scrollbar {
display: none;
}
`
const Tab = styled.div<{ active?: boolean }>`
display: flex;
align-items: center;
@@ -228,12 +310,12 @@ const Tab = styled.div<{ active?: boolean }>`
padding: 4px 10px;
padding-right: 8px;
background: ${(props) => (props.active ? 'var(--color-list-item)' : 'transparent')};
transition: background 0.2s;
border-radius: var(--list-item-border-radius);
cursor: pointer;
user-select: none;
height: 30px;
min-width: 90px;
transition: background 0.2s;
.close-button {
opacity: 0;
transition: opacity 0.2s;
@@ -251,12 +333,15 @@ const TabHeader = styled.div`
display: flex;
align-items: center;
gap: 6px;
min-width: 0;
flex: 1;
`
const TabIcon = styled.span`
display: flex;
align-items: center;
color: var(--color-text-2);
flex-shrink: 0;
`
const TabTitle = styled.span`
@@ -265,6 +350,8 @@ const TabTitle = styled.span`
display: flex;
align-items: center;
margin-right: 4px;
overflow: hidden;
white-space: nowrap;
`
const CloseButton = styled.span`
@@ -284,6 +371,7 @@ const AddTabButton = styled.div`
cursor: pointer;
color: var(--color-text-2);
border-radius: var(--list-item-border-radius);
flex-shrink: 0;
&.active {
background: var(--color-list-item);
}
@@ -292,11 +380,28 @@ const AddTabButton = styled.div`
}
`
const ScrollButton = styled(Button)`
position: absolute;
right: 4rem;
top: 50%;
transform: translateY(-50%);
z-index: 1;
opacity: 0;
transition: opacity 0.2s ease-in-out;
border: none;
box-shadow:
0 6px 16px 0 rgba(0, 0, 0, 0.08),
0 3px 6px -4px rgba(0, 0, 0, 0.12),
0 9px 28px 8px rgba(0, 0, 0, 0.05);
`
const RightButtonsContainer = styled.div`
display: flex;
align-items: center;
gap: 6px;
margin-left: auto;
flex-shrink: 0;
`
const ThemeButton = styled.div`

View File

@@ -0,0 +1,20 @@
import { Tooltip, TooltipProps } from 'antd'
import { HelpCircle } from 'lucide-react'
type InheritedTooltipProps = Omit<TooltipProps, 'children'>
interface HelpTooltipProps extends InheritedTooltipProps {
iconColor?: string
iconSize?: string | number
iconStyle?: React.CSSProperties
}
const HelpTooltip = ({ iconColor = 'var(--color-text-2)', iconSize = 14, iconStyle, ...rest }: HelpTooltipProps) => {
return (
<Tooltip {...rest}>
<HelpCircle size={iconSize} color={iconColor} style={{ ...iconStyle }} role="img" aria-label="Help" />
</Tooltip>
)
}
export default HelpTooltip

View File

@@ -9,7 +9,7 @@ interface InfoTooltipProps extends InheritedTooltipProps {
iconStyle?: React.CSSProperties
}
const InfoTooltip = ({ iconColor = 'var(--color-text-3)', iconSize = 14, iconStyle, ...rest }: InfoTooltipProps) => {
const InfoTooltip = ({ iconColor = 'var(--color-text-2)', iconSize = 14, iconStyle, ...rest }: InfoTooltipProps) => {
return (
<Tooltip {...rest}>
<Info size={iconSize} color={iconColor} style={{ ...iconStyle }} role="img" aria-label="Information" />

View File

@@ -0,0 +1,3 @@
export { default as HelpTooltip } from './HelpTooltip'
export { default as InfoTooltip } from './InfoTooltip'
export { default as WarnTooltip } from './WarnTooltip'

View File

@@ -56,6 +56,8 @@ interface SortableProps<T> {
listStyle?: React.CSSProperties
/** Ghost item style */
ghostItemStyle?: React.CSSProperties
/** Item gap */
gap?: number | string
}
function Sortable<T>({
@@ -70,7 +72,8 @@ function Sortable<T>({
useDragOverlay = true,
showGhost = false,
className,
listStyle
listStyle,
gap
}: SortableProps<T>) {
const sensors = useSensors(
useSensor(PortalSafePointerSensor, {
@@ -150,7 +153,12 @@ function Sortable<T>({
onDragCancel={handleDragCancel}
modifiers={modifiers}>
<SortableContext items={itemIds} strategy={strategy}>
<ListWrapper className={className} data-layout={layout} style={listStyle}>
<ListWrapper
className={className}
data-layout={layout}
data-direction={horizontal ? 'horizontal' : 'vertical'}
$gap={gap}
style={listStyle}>
{items.map((item, index) => (
<SortableItem
key={itemIds[index]}
@@ -176,17 +184,31 @@ function Sortable<T>({
)
}
const ListWrapper = styled.div`
const ListWrapper = styled.div<{ $gap?: number | string }>`
gap: ${({ $gap }) => $gap};
&[data-layout='grid'] {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
width: 100%;
gap: 12px;
@media (max-width: 768px) {
grid-template-columns: 1fr;
}
}
&[data-layout='list'] {
display: flex;
align-items: center;
[data-direction='horizontal'] {
flex-direction: row;
}
[data-direction='vertical'] {
flex-direction: column;
}
}
`
export default Sortable

View File

@@ -8,7 +8,7 @@ interface UseDndReorderParams<T> {
/** 用于更新原始列表状态的函数 */
onUpdate: (newList: T[]) => void
/** 用于从列表项中获取唯一ID的属性名或函数 */
idKey: keyof T | ((item: T) => Key)
itemKey: keyof T | ((item: T) => Key)
}
/**
@@ -18,8 +18,11 @@ interface UseDndReorderParams<T> {
* @param params - { originalList, filteredList, onUpdate, idKey }
* @returns 返回可以直接传递给 Sortable 的 onSortEnd 回调
*/
export function useDndReorder<T>({ originalList, filteredList, onUpdate, idKey }: UseDndReorderParams<T>) {
const getId = useCallback((item: T) => (typeof idKey === 'function' ? idKey(item) : (item[idKey] as Key)), [idKey])
export function useDndReorder<T>({ originalList, filteredList, onUpdate, itemKey }: UseDndReorderParams<T>) {
const getId = useCallback(
(item: T) => (typeof itemKey === 'function' ? itemKey(item) : (item[itemKey] as Key)),
[itemKey]
)
// 创建从 item ID 到其在 *原始列表* 中索引的映射
const itemIndexMap = useMemo(() => {

View File

@@ -136,7 +136,7 @@ export async function upgradeToV7(tx: Transaction): Promise<void> {
content: mcpTool.response,
error:
mcpTool.status !== 'done'
? { message: 'MCP Tool did not complete', originalStatus: mcpTool.status }
? { message: 'MCP Tool did not complete', originalStatus: mcpTool.status, name: null, stack: null }
: undefined,
createdAt: oldMessage.createdAt,
metadata: { rawMcpToolResponse: mcpTool }
@@ -263,10 +263,18 @@ export async function upgradeToV7(tx: Transaction): Promise<void> {
// 10. Error Block (Status is ERROR)
if (oldMessage.error && typeof oldMessage.error === 'object' && Object.keys(oldMessage.error).length > 0) {
if (isEmpty(oldMessage.content)) {
const block = createErrorBlock(oldMessage.id, oldMessage.error, {
createdAt: oldMessage.createdAt,
status: MessageBlockStatus.ERROR // Error block status is ERROR
})
const block = createErrorBlock(
oldMessage.id,
{
message: oldMessage.error?.message ?? null,
name: oldMessage.error?.name ?? null,
stack: oldMessage.error?.stack ?? null
},
{
createdAt: oldMessage.createdAt,
status: MessageBlockStatus.ERROR // Error block status is ERROR
}
)
blocksToCreate.push(block)
messageBlockIds.push(block.id)
}

View File

@@ -0,0 +1,14 @@
import { useAppDispatch, useAppSelector } from '@renderer/store'
import { selectNotesSettings, updateNotesSettings } from '@renderer/store/note'
export function useShowWorkspace() {
const dispatch = useAppDispatch()
const settings = useAppSelector(selectNotesSettings)
const showWorkspace = settings.showWorkspace
return {
showWorkspace,
setShowWorkspace: (show: boolean) => dispatch(updateNotesSettings({ showWorkspace: show })),
toggleShowWorkspace: () => dispatch(updateNotesSettings({ showWorkspace: !showWorkspace }))
}
}

View File

@@ -3,10 +3,8 @@ import {
setAssistantsTabSortType,
setShowAssistants,
setShowTopics,
setShowWorkspace,
toggleShowAssistants,
toggleShowTopics,
toggleShowWorkspace
toggleShowTopics
} from '@renderer/store/settings'
import { AssistantsSortType } from '@renderer/types'
@@ -41,14 +39,3 @@ export function useAssistantsTabSortType() {
setAssistantsTabSortType: (sortType: AssistantsSortType) => dispatch(setAssistantsTabSortType(sortType))
}
}
export function useShowWorkspace() {
const showWorkspace = useAppSelector((state) => state.settings.showWorkspace)
const dispatch = useAppDispatch()
return {
showWorkspace,
setShowWorkspace: (show: boolean) => dispatch(setShowWorkspace(show)),
toggleShowWorkspace: () => dispatch(toggleShowWorkspace())
}
}

View File

@@ -538,7 +538,10 @@
"tip": "The run button will be displayed in the toolbar of executable code blocks, please do not execute dangerous code!",
"title": "Code Execution"
},
"code_image_tools": "Enable preview tools",
"code_image_tools": {
"label": "Enable preview tools",
"tip": "Enable preview tools for images rendered from code blocks such as mermaid"
},
"code_wrappable": "Code block wrappable",
"context_count": {
"label": "Context",
@@ -828,6 +831,7 @@
"invalid": "Invalid MCP server"
}
},
"cause": "Error cause",
"chat": {
"chunk": {
"non_json": "Returned an invalid data format"
@@ -837,6 +841,7 @@
"quota_exceeded": "Your daily {{quota}} free quota has been exhausted. Please go to the <provider>{{provider}}</provider> to obtain an API key and configure the API key to continue using.",
"response": "Something went wrong. Please check if you have set your API key in the Settings > Providers"
},
"data": "data",
"detail": "Error Details",
"details": "Details",
"http": {
@@ -856,6 +861,7 @@
"exists": "Model already exists",
"not_exists": "Model does not exist"
},
"name": "Error name",
"no_api_key": "API key is not configured",
"pause_placeholder": "Paused",
"provider_disabled": "Model provider is not enabled",
@@ -864,9 +870,13 @@
"title": "Render Error"
},
"requestBody": "Request Body",
"requestBodyValues": "Request Body Values",
"requestUrl": "Request URL",
"responseBody": "Response Body",
"responseHeaders": "Response Header",
"stack": "Stack Trace",
"status": "Status Code",
"statusCode": "Status code",
"unknown": "Unknown error",
"user_message_not_found": "Cannot find original user message to resend"
},
@@ -1560,6 +1570,7 @@
"selected": "Selected tags"
},
"function_calling": "Function Calling",
"invalid_model": "Invalid Model",
"no_matches": "No models available",
"parameter_name": "Parameter Name",
"parameter_type": {
@@ -1633,6 +1644,7 @@
"only_markdown": "Only Markdown files are supported",
"only_one_file_allowed": "Only one file can be uploaded",
"open_folder": "Open an external folder",
"open_outside": "Open from external",
"rename": "Rename",
"rename_changed": "Due to security policies, the filename has been changed from {{original}} to {{final}}",
"save": "Save to Notes",
@@ -4152,7 +4164,7 @@
"aborted": "Translation aborted"
},
"input": {
"placeholder": "Text, files, or images (OCR supported) can be pasted or dragged in"
"placeholder": "Text, text files, or images (with OCR support) can be pasted or dragged in"
},
"language": {
"not_pair": "Source language is different from the set language",

View File

@@ -538,7 +538,10 @@
"tip": "実行可能なコードブロックのツールバーには実行ボタンが表示されます。危険なコードを実行しないでください!",
"title": "コード実行"
},
"code_image_tools": "プレビューツールを有効にする",
"code_image_tools": {
"label": "プレビューツールを有効にする",
"tip": "mermaid などのコードブロックから生成された画像に対してプレビューツールを有効にする"
},
"code_wrappable": "コードブロック折り返し",
"context_count": {
"label": "コンテキスト",
@@ -828,6 +831,7 @@
"invalid": "無効なMCPサーバー"
}
},
"cause": "エラーの原因",
"chat": {
"chunk": {
"non_json": "無効なデータ形式が返されました"
@@ -837,6 +841,7 @@
"quota_exceeded": "本日の{{quota}}無料クォータが使い果たされました。<provider>{{provider}}</provider>でAPIキーを取得し、APIキーを設定して使用を続けてください。",
"response": "エラーが発生しました。APIキーが設定されていない場合は、設定 > プロバイダーでキーを設定してください"
},
"data": "データ",
"detail": "エラーの詳細",
"details": "詳細",
"http": {
@@ -856,6 +861,7 @@
"exists": "モデルが既に存在します",
"not_exists": "モデルが存在しません"
},
"name": "エラー名",
"no_api_key": "APIキーが設定されていません",
"pause_placeholder": "応答を一時停止しました",
"provider_disabled": "モデルプロバイダーが有効になっていません",
@@ -864,9 +870,13 @@
"title": "レンダリングエラー"
},
"requestBody": "要求されたコンテンツ",
"requestBodyValues": "リクエストボディ",
"requestUrl": "リクエストパス",
"responseBody": "レスポンス内容",
"responseHeaders": "レスポンスヘッダー",
"stack": "スタック情報",
"status": "ステータスコード",
"statusCode": "ステータスコード",
"unknown": "不明なエラー",
"user_message_not_found": "元のユーザーメッセージを見つけることができませんでした"
},
@@ -1560,6 +1570,7 @@
"selected": "選択済みのタグ"
},
"function_calling": "関数呼び出し",
"invalid_model": "無効なモデル",
"no_matches": "利用可能なモデルがありません",
"parameter_name": "パラメータ名",
"parameter_type": {
@@ -1633,6 +1644,7 @@
"only_markdown": "Markdown ファイルのみをアップロードできます",
"only_one_file_allowed": "アップロードできるファイルは1つだけです",
"open_folder": "外部フォルダーを開きます",
"open_outside": "外部から開く",
"rename": "名前の変更",
"rename_changed": "セキュリティポリシーにより、ファイル名は{{original}}から{{final}}に変更されました",
"save": "メモに保存する",
@@ -4152,7 +4164,7 @@
"aborted": "翻訳中止"
},
"input": {
"placeholder": "テキスト、ファイル、画像OCR対応を貼り付けたりドラッグアンドドロップしたりできます"
"placeholder": "テキスト、テキストファイル、画像OCR対応を貼り付けたりドラッグして挿入したりできます"
},
"language": {
"not_pair": "ソース言語が設定された言語と異なります",

View File

@@ -538,7 +538,10 @@
"tip": "Выполнение кода в блоке кода возможно, но не рекомендуется выполнять опасный код!",
"title": "Выполнение кода"
},
"code_image_tools": "Включить инструменты предпросмотра",
"code_image_tools": {
"label": "Включить инструменты предпросмотра",
"tip": "Включить инструменты предпросмотра для изображений, сгенерированных из блоков кода (например mermaid)"
},
"code_wrappable": "Блок кода можно переносить",
"context_count": {
"label": "Контекст",
@@ -828,6 +831,7 @@
"invalid": "Недействительный сервер MCP"
}
},
"cause": "Ошибка произошла по следующей причине",
"chat": {
"chunk": {
"non_json": "Вернулся недопустимый формат данных"
@@ -837,6 +841,7 @@
"quota_exceeded": "Ваша ежедневная {{quota}} бесплатная квота исчерпана. Пожалуйста, перейдите в <provider>{{provider}}</provider> для получения ключа API и настройте ключ API для продолжения использования.",
"response": "Что-то пошло не так. Пожалуйста, проверьте, установлен ли ваш ключ API в Настройки > Провайдеры"
},
"data": "данные",
"detail": "Детали ошибки",
"details": "Подробности",
"http": {
@@ -856,6 +861,7 @@
"exists": "Модель уже существует",
"not_exists": "Модель не существует"
},
"name": "错误名称",
"no_api_key": "Ключ API не настроен",
"pause_placeholder": "Получение ответа приостановлено",
"provider_disabled": "Провайдер моделей не включен",
@@ -864,9 +870,13 @@
"title": "Ошибка рендеринга"
},
"requestBody": "Запрашиваемый контент",
"requestBodyValues": "Тело запроса",
"requestUrl": "Путь запроса",
"responseBody": "Содержание ответа",
"responseHeaders": "Заголовки ответа",
"stack": "Информация стека",
"status": "Код статуса",
"statusCode": "Код состояния",
"unknown": "Неизвестная ошибка",
"user_message_not_found": "Не удалось найти исходное сообщение пользователя"
},
@@ -1560,6 +1570,7 @@
"selected": "Выбранные теги"
},
"function_calling": "Вызов функции",
"invalid_model": "Недействительная модель",
"no_matches": "Нет доступных моделей",
"parameter_name": "Имя параметра",
"parameter_type": {
@@ -1633,6 +1644,7 @@
"only_markdown": "Только Markdown",
"only_one_file_allowed": "Можно загрузить только один файл",
"open_folder": "Откройте внешнюю папку",
"open_outside": "открыть снаружи",
"rename": "переименовать",
"rename_changed": "В связи с политикой безопасности имя файла было изменено с {{Original}} на {{final}}",
"save": "Сохранить в заметки",
@@ -4152,7 +4164,7 @@
"aborted": "Перевод прерван"
},
"input": {
"placeholder": "Можно вставить или перетащить текст, файлы, изображения (поддержка OCR)"
"placeholder": "Можно вставить или перетащить текст, текстовые файлы, изображения (с поддержкой OCR)"
},
"language": {
"not_pair": "Исходный язык отличается от настроенного",

View File

@@ -538,7 +538,10 @@
"tip": "可执行的代码块工具栏中会显示运行按钮,注意不要执行危险代码!",
"title": "代码执行"
},
"code_image_tools": "启用预览工具",
"code_image_tools": {
"label": "启用预览工具",
"tip": "为 mermaid 等代码块渲染后的图像启用预览工具"
},
"code_wrappable": "代码块可换行",
"context_count": {
"label": "上下文数",
@@ -828,6 +831,7 @@
"invalid": "无效的MCP服务器"
}
},
"cause": "错误原因",
"chat": {
"chunk": {
"non_json": "返回了无效的数据格式"
@@ -837,6 +841,7 @@
"quota_exceeded": "您今日免费配额已用尽,请前往 <provider>{{provider}}</provider> 获取API密钥配置API密钥后继续使用",
"response": "出错了,如果没有配置 API 密钥,请前往设置 > 模型提供商中配置密钥"
},
"data": "数据",
"detail": "错误详情",
"details": "详细信息",
"http": {
@@ -856,6 +861,7 @@
"exists": "模型已存在",
"not_exists": "模型不存在"
},
"name": "错误名称",
"no_api_key": "API 密钥未配置",
"pause_placeholder": "已中断",
"provider_disabled": "模型提供商未启用",
@@ -864,9 +870,13 @@
"title": "渲染错误"
},
"requestBody": "请求内容",
"requestBodyValues": "请求体",
"requestUrl": "请求路径",
"responseBody": "响应内容",
"responseHeaders": "响应首部",
"stack": "堆栈信息",
"status": "状态码",
"statusCode": "状态码",
"unknown": "未知错误",
"user_message_not_found": "无法找到原始用户消息"
},
@@ -1560,6 +1570,7 @@
"selected": "已选标签"
},
"function_calling": "函数调用",
"invalid_model": "无效模型",
"no_matches": "无可用模型",
"parameter_name": "参数名称",
"parameter_type": {
@@ -1633,6 +1644,7 @@
"only_markdown": "仅支持 Markdown 格式",
"only_one_file_allowed": "只能上传一个文件",
"open_folder": "打开外部文件夹",
"open_outside": "从外部打开",
"rename": "重命名",
"rename_changed": "由于安全策略,文件名已从 {{original}} 更改为 {{final}}",
"save": "保存到笔记",
@@ -4152,7 +4164,7 @@
"aborted": "翻译中止"
},
"input": {
"placeholder": "可粘贴或拖入文本、文件、图片支持OCR"
"placeholder": "可粘贴或拖入文本、文本文件、图片支持OCR"
},
"language": {
"not_pair": "源语言与设置的语言不同",

View File

@@ -538,7 +538,10 @@
"tip": "可執行的程式碼塊工具欄中會顯示運行按鈕,注意不要執行危險程式碼!",
"title": "程式碼執行"
},
"code_image_tools": "啟用預覽工具",
"code_image_tools": {
"label": "啟用預覽工具",
"tip": "為 mermaid 等程式碼區塊渲染後的圖像啟用預覽工具"
},
"code_wrappable": "程式碼區塊可自動換行",
"context_count": {
"label": "上下文",
@@ -828,6 +831,7 @@
"invalid": "無效的MCP伺服器"
}
},
"cause": "錯誤原因",
"chat": {
"chunk": {
"non_json": "返回了無效的資料格式"
@@ -837,6 +841,7 @@
"quota_exceeded": "您今日{{quota}}免费配额已用尽,请前往 <provider>{{provider}}</provider> 获取API密钥配置API密钥后继续使用",
"response": "出現錯誤。如果尚未設定 API 金鑰,請前往設定 > 模型提供者中設定金鑰"
},
"data": "数据",
"detail": "錯誤詳情",
"details": "詳細信息",
"http": {
@@ -856,6 +861,7 @@
"exists": "模型已存在",
"not_exists": "模型不存在"
},
"name": "錯誤名稱",
"no_api_key": "API 金鑰未設定",
"pause_placeholder": "回應已暫停",
"provider_disabled": "模型供應商未啟用",
@@ -864,9 +870,13 @@
"title": "渲染錯誤"
},
"requestBody": "請求內容",
"requestBodyValues": "请求体",
"requestUrl": "請求路徑",
"responseBody": "响应内容",
"responseHeaders": "响应首部",
"stack": "堆棧信息",
"status": "狀態碼",
"statusCode": "狀態碼",
"unknown": "未知錯誤",
"user_message_not_found": "無法找到原始用戶訊息"
},
@@ -1560,6 +1570,7 @@
"selected": "已選標籤"
},
"function_calling": "函數調用",
"invalid_model": "無效模型",
"no_matches": "無可用模型",
"parameter_name": "參數名稱",
"parameter_type": {
@@ -1633,6 +1644,7 @@
"only_markdown": "僅支援 Markdown 格式",
"only_one_file_allowed": "只能上傳一個文件",
"open_folder": "打開外部文件夾",
"open_outside": "從外部打開",
"rename": "重命名",
"rename_changed": "由於安全策略,文件名已從 {{original}} 更改為 {{final}}",
"save": "儲存到筆記",
@@ -4152,7 +4164,7 @@
"aborted": "翻譯中止"
},
"input": {
"placeholder": "可粘貼或拖入文字、檔案、圖片支援OCR"
"placeholder": "可粘貼或拖入文字、文字檔案、圖片支援OCR"
},
"language": {
"not_pair": "源語言與設定的語言不同",

View File

@@ -538,7 +538,10 @@
"tip": "Στη γραμμή εργαλείων των εκτελέσιμων blocks κώδικα θα εμφανίζεται το κουμπί εκτέλεσης· προσέξτε να μην εκτελέσετε επικίνδυνο κώδικα!",
"title": "Εκτέλεση Κώδικα"
},
"code_image_tools": "Ενεργοποίηση εργαλείου προεπισκόπησης",
"code_image_tools": {
"label": "Ενεργοποίηση εργαλείου προεπισκόπησης",
"tip": "Ενεργοποίηση εργαλείου προεπισκόπησης για εικόνες που αποδίδονται από blocks κώδικα όπως το mermaid"
},
"code_wrappable": "Οι κώδικες μπορούν να γράφονται σε διαφορετική γραμμή",
"context_count": {
"label": "Πλήθος ενδιάμεσων",
@@ -677,6 +680,7 @@
"model_placeholder": "Επιλέξτε το μοντέλο που θα χρησιμοποιήσετε",
"model_required": "Επιλέξτε μοντέλο",
"select_folder": "Επιλογή φακέλου",
"supported_providers": "υποστηριζόμενοι πάροχοι",
"title": "Εργαλεία κώδικα",
"update_options": "Ενημέρωση επιλογών",
"working_directory": "κατάλογος εργασίας"
@@ -743,6 +747,7 @@
"delete": "Διαγραφή",
"delete_confirm": "Είστε βέβαιοι ότι θέλετε να διαγράψετε;",
"description": "Περιγραφή",
"detail": "Λεπτομέρειες",
"disabled": "Απενεργοποιημένο",
"docs": "Έγγραφα",
"download": "Λήψη",
@@ -826,6 +831,7 @@
"invalid": "Μη έγκυρος διακομιστής MCP"
}
},
"cause": "Αιτία σφάλματος",
"chat": {
"chunk": {
"non_json": "Επέστρεψε μη έγκυρη μορφή δεδομένων"
@@ -835,6 +841,9 @@
"quota_exceeded": "Η ημερήσια δωρεάν ποσόστωση {{quota}} tokens σας έχει εξαντληθεί. Παρακαλώ μεταβείτε στο <provider>{{provider}}</provider> για να λάβετε ένα κλειδί API και να ρυθμίσετε το κλειδί API για να συνεχίσετε τη χρήση.",
"response": "Σφάλμα. Εάν δεν έχετε ρυθμίσει το κλειδί API, πηγαίνετε στο ρυθμισμένα > παρέχοντας το πρόσωπο του μοντέλου"
},
"data": "δεδομένα",
"detail": "Λεπτομέρειες σφάλματος",
"details": "Λεπτομέρειες",
"http": {
"400": "Σφάλμα ζητήματος, παρακαλώ ελέγξτε αν τα παράμετρα του ζητήματος είναι σωστά. Εάν έχετε αλλάξει τις ρυθμίσεις του μοντέλου, επαναφέρετε τις προεπιλεγμένες ρυθμίσεις.",
"401": "Αποτυχία επιβεβαίωσης ταυτότητας, παρακαλώ ελέγξτε αν η κλειδί API είναι σωστή",
@@ -846,11 +855,13 @@
"503": "Η υπηρεσία δεν είναι διαθέσιμη, παρακαλώ δοκιμάστε ξανά",
"504": "Υπερχρονισμός φάρων, παρακαλώ δοκιμάστε ξανά"
},
"message": "Μήνυμα σφάλματος",
"missing_user_message": "Αδυναμία εναλλαγής απάντησης μοντέλου: το αρχικό μήνυμα χρήστη έχει διαγραφεί. Παρακαλούμε στείλτε ένα νέο μήνυμα για να λάβετε απάντηση από αυτό το μοντέλο",
"model": {
"exists": "Το μοντέλο υπάρχει ήδη",
"not_exists": "Το μοντέλο δεν υπάρχει"
},
"name": "Λάθος όνομα",
"no_api_key": "Δεν έχετε ρυθμίσει το κλειδί API",
"pause_placeholder": "Διακόπηκε",
"provider_disabled": "Ο παρεχόμενος παροχός του μοντέλου δεν είναι ενεργοποιημένος",
@@ -858,6 +869,14 @@
"description": "Απέτυχε η ώθηση της εξίσωσης, παρακαλώ ελέγξτε το σωστό μορφάτι της",
"title": "Σφάλμα Παρασκήνιου"
},
"requestBody": "Περιεχόμενο αιτήματος",
"requestBodyValues": "Σώμα αιτήματος",
"requestUrl": "Μονοπάτι αιτήματος",
"responseBody": "απάντηση περιεχομένου",
"responseHeaders": "Επικεφαλίδες απόκρισης",
"stack": "Πληροφορίες στοίβας",
"status": "Κωδικός κατάστασης",
"statusCode": "Κωδικός κατάστασης",
"unknown": "Άγνωστο σφάλμα",
"user_message_not_found": "Αδυναμία εύρεσης της αρχικής μηνύματος χρήστη"
},
@@ -1319,7 +1338,8 @@
"delete": {
"content": "Η διαγραφή της ομάδας θα διαγράψει τις ερωτήσεις των χρηστών και όλες τις απαντήσεις του αστρόναυτη",
"title": "Διαγραφή ομάδας"
}
},
"retry_failed": "Αποτυχημένο μήνυμα επανάληψης"
},
"ignore": {
"knowledge": {
@@ -1550,6 +1570,7 @@
"selected": "Επιλεγμένη ετικέτα"
},
"function_calling": "Ξεχωριστική Κλήση Συναρτήσεων",
"invalid_model": "Μη έγκυρο μοντέλο",
"no_matches": "Δεν υπάρχουν διαθέσιμα μοντέλα",
"parameter_name": "Όνομα παραμέτρου",
"parameter_type": {
@@ -1619,9 +1640,13 @@
"new_folder": "Νέος φάκελος",
"new_note": "Δημιουργία νέας σημείωσης",
"no_content_to_copy": "Δεν υπάρχει περιεχόμενο προς αντιγραφή",
"no_file_selected": "Επιλέξτε το αρχείο για μεταφόρτωση",
"only_markdown": "Υποστηρίζεται μόνο η μορφή Markdown",
"only_one_file_allowed": "Μπορείτε να ανεβάσετε μόνο ένα αρχείο",
"open_folder": "Άνοιγμα εξωτερικού φακέλου",
"open_outside": "Από το εξωτερικό",
"rename": "μετονομασία",
"rename_changed": "Λόγω πολιτικής ασφάλειας, το όνομα του αρχείου έχει αλλάξει από {{original}} σε {{final}}",
"save": "αποθήκευση στις σημειώσεις",
"settings": {
"data": {
@@ -3343,6 +3368,8 @@
"label": "Καταγραφή στοιχείων στο grid"
},
"input": {
"confirm_delete_message": "Επιβεβαίωση πριν τη διαγραφή μηνύματος",
"confirm_regenerate_message": "Επιβεβαίωση πριν από την επαναδημιουργία του μηνύματος",
"enable_quick_triggers": "Ενεργοποίηση των '/' και '@' για γρήγορη πρόσβαση σε μενού",
"paste_long_text_as_file": "Επικόλληση μεγάλου κειμένου ως αρχείο",
"paste_long_text_threshold": "Όριο μεγάλου κειμένου",
@@ -4137,7 +4164,7 @@
"aborted": "Η μετάφραση διακόπηκε"
},
"input": {
"placeholder": "Μπορείτε να επικολλήσετε ή να σύρετε κείμενο, αρχεία, εικόνες (με υποστήριξη OCR)"
"placeholder": "Μπορείτε να επικολλήσετε ή να σύρετε κείμενο, αρχεία κειμένου, εικόνες (υποστηρίζεται η OCR)"
},
"language": {
"not_pair": "Η γλώσσα πηγής διαφέρει από την οριζόμενη γλώσσα",

View File

@@ -538,7 +538,10 @@
"tip": "En la barra de herramientas de bloques de código ejecutables se mostrará un botón de ejecución. ¡Tenga cuidado en no ejecutar código peligroso!",
"title": "Ejecución de Código"
},
"code_image_tools": "Activar herramientas de vista previa",
"code_image_tools": {
"label": "Habilitar herramienta de vista previa",
"tip": "Habilitar herramientas de vista previa para imágenes renderizadas de bloques de código como mermaid"
},
"code_wrappable": "Bloques de código reemplazables",
"context_count": {
"label": "Número de contextos",
@@ -677,6 +680,7 @@
"model_placeholder": "Seleccionar el modelo que se va a utilizar",
"model_required": "Seleccione el modelo",
"select_folder": "Seleccionar carpeta",
"supported_providers": "Proveedores de servicios compatibles",
"title": "Herramientas de código",
"update_options": "Opciones de actualización",
"working_directory": "directorio de trabajo"
@@ -743,6 +747,7 @@
"delete": "Eliminar",
"delete_confirm": "¿Está seguro de que desea eliminarlo?",
"description": "Descripción",
"detail": "Detalles",
"disabled": "Desactivado",
"docs": "Documentos",
"download": "Descargar",
@@ -826,6 +831,7 @@
"invalid": "Servidor MCP no válido"
}
},
"cause": "Error原因",
"chat": {
"chunk": {
"non_json": "Devuelve un formato de datos no válido"
@@ -835,6 +841,9 @@
"quota_exceeded": "Su cuota gratuita diaria de {{quota}} tokens se ha agotado. Por favor, vaya a <provider>{{provider}}</provider> para obtener una clave API y configurar la clave API para continuar usando.",
"response": "Ha ocurrido un error, si no ha configurado la clave API, vaya a Configuración > Proveedor de modelos para configurar la clave"
},
"data": "datos",
"detail": "Detalles del error",
"details": "Detalles",
"http": {
"400": "Error en la solicitud, revise si los parámetros de la solicitud son correctos. Si modificó la configuración del modelo, restablezca a la configuración predeterminada",
"401": "Fallo en la autenticación, revise si la clave API es correcta",
@@ -846,11 +855,13 @@
"503": "Servicio no disponible, inténtelo de nuevo más tarde",
"504": "Tiempo de espera de la puerta de enlace, inténtelo de nuevo más tarde"
},
"message": "错误信息",
"missing_user_message": "No se puede cambiar la respuesta del modelo: el mensaje original del usuario ha sido eliminado. Envíe un nuevo mensaje para obtener la respuesta de este modelo",
"model": {
"exists": "El modelo ya existe",
"not_exists": "El modelo no existe"
},
"name": "Nombre de error",
"no_api_key": "La clave API no está configurada",
"pause_placeholder": "Interrumpido",
"provider_disabled": "El proveedor de modelos no está habilitado",
@@ -858,6 +869,14 @@
"description": "Error al renderizar la fórmula, por favor, compruebe si el formato de la fórmula es correcto",
"title": "Error de renderizado"
},
"requestBody": "Contenido de la solicitud",
"requestBodyValues": "Cuerpo de la solicitud",
"requestUrl": "Ruta de solicitud",
"responseBody": "Contenido de la respuesta",
"responseHeaders": "Encabezados de respuesta",
"stack": "Información de la pila",
"status": "código de estado",
"statusCode": "código de estado",
"unknown": "Error desconocido",
"user_message_not_found": "No se pudo encontrar el mensaje original del usuario"
},
@@ -1319,7 +1338,8 @@
"delete": {
"content": "Eliminar el mensaje del grupo eliminará la pregunta del usuario y todas las respuestas del asistente",
"title": "Eliminar mensaje del grupo"
}
},
"retry_failed": "Reintentar el mensaje con error"
},
"ignore": {
"knowledge": {
@@ -1550,6 +1570,7 @@
"selected": "Etiquetas seleccionadas"
},
"function_calling": "Llamada a función",
"invalid_model": "Modelo inválido",
"no_matches": "No hay modelos disponibles",
"parameter_name": "Nombre del parámetro",
"parameter_type": {
@@ -1619,9 +1640,13 @@
"new_folder": "Nueva carpeta",
"new_note": "Crear nota nueva",
"no_content_to_copy": "No hay contenido para copiar",
"no_file_selected": "Por favor, seleccione el archivo a subir",
"only_markdown": "Solo se admite el formato Markdown",
"only_one_file_allowed": "solo se puede subir un archivo",
"open_folder": "abrir carpeta externa",
"open_outside": "Abrir desde el exterior",
"rename": "renombrar",
"rename_changed": "Debido a políticas de seguridad, el nombre del archivo ha cambiado de {{original}} a {{final}}",
"save": "Guardar en notas",
"settings": {
"data": {
@@ -3343,6 +3368,8 @@
"label": "Desencadenante de detalles de cuadrícula"
},
"input": {
"confirm_delete_message": "Confirmar antes de eliminar mensaje",
"confirm_regenerate_message": "confirmar antes de regenerar el mensaje",
"enable_quick_triggers": "Habilitar menú rápido con '/' y '@'",
"paste_long_text_as_file": "Pegar texto largo como archivo",
"paste_long_text_threshold": "Límite de longitud de texto largo",
@@ -4137,7 +4164,7 @@
"aborted": "Traducción cancelada"
},
"input": {
"placeholder": "Se puede pegar o arrastrar texto, archivos e imágenes (compatible con OCR)"
"placeholder": "Puede pegar o arrastrar texto, archivos de texto o imágenes (compatible con OCR)"
},
"language": {
"not_pair": "El idioma de origen es diferente al idioma configurado",

View File

@@ -538,7 +538,10 @@
"tip": "Une bouton d'exécution s'affichera dans la barre d'outils des blocs de code exécutables. Attention à ne pas exécuter de code dangereux !",
"title": "Exécution de code"
},
"code_image_tools": "Activer l'outil d'aperçu",
"code_image_tools": {
"label": "Activer l'outil d'aperçu",
"tip": "Activer les outils de prévisualisation pour les images rendues des blocs de code tels que mermaid"
},
"code_wrappable": "Blocs de code avec retours à la ligne",
"context_count": {
"label": "Nombre de contextes",
@@ -677,6 +680,7 @@
"model_placeholder": "Sélectionnez le modèle à utiliser",
"model_required": "Veuillez sélectionner le modèle",
"select_folder": "Sélectionner le dossier",
"supported_providers": "fournisseurs pris en charge",
"title": "Outils de code",
"update_options": "Options de mise à jour",
"working_directory": "répertoire de travail"
@@ -743,6 +747,7 @@
"delete": "Supprimer",
"delete_confirm": "Êtes-vous sûr de vouloir supprimer ?",
"description": "Description",
"detail": "détails",
"disabled": "Désactivé",
"docs": "Documents",
"download": "Télécharger",
@@ -826,6 +831,7 @@
"invalid": "Serveur MCP invalide"
}
},
"cause": "Erreur causée par",
"chat": {
"chunk": {
"non_json": "a renvoyé un format de données invalide"
@@ -835,6 +841,9 @@
"quota_exceeded": "Votre quota gratuit quotidien de {{quota}} tokens a été épuisé. Veuillez vous rendre sur <provider>{{provider}}</provider> pour obtenir une clé API et configurer la clé API pour continuer à utiliser.",
"response": "Une erreur s'est produite, si l'API n'est pas configurée, veuillez aller dans Paramètres > Fournisseurs de modèles pour configurer la clé"
},
"data": "données",
"detail": "Détails de l'erreur",
"details": "Informations détaillées",
"http": {
"400": "Erreur de requête, veuillez vérifier si les paramètres de la requête sont corrects. Si vous avez modifié les paramètres du modèle, réinitialisez-les aux paramètres par défaut.",
"401": "Échec de l'authentification, veuillez vérifier que votre clé API est correcte.",
@@ -846,11 +855,13 @@
"503": "Service indisponible, veuillez réessayer plus tard.",
"504": "Délai d'expiration de la passerelle, veuillez réessayer plus tard."
},
"message": "Erreur message",
"missing_user_message": "Impossible de changer de modèle de réponse : le message utilisateur d'origine a été supprimé. Veuillez envoyer un nouveau message pour obtenir une réponse de ce modèle.",
"model": {
"exists": "Le modèle existe déjà",
"not_exists": "Le modèle n'existe pas"
},
"name": "Nom d'erreur",
"no_api_key": "La clé API n'est pas configurée",
"pause_placeholder": "Прервано",
"provider_disabled": "Le fournisseur de modèles n'est pas activé",
@@ -858,6 +869,14 @@
"description": "La formule n'a pas été rendue avec succès, veuillez vérifier si le format de la formule est correct",
"title": "Erreur de rendu"
},
"requestBody": "Contenu de la demande",
"requestBodyValues": "Corps de la requête",
"requestUrl": "Chemin de la requête",
"responseBody": "Contenu de la réponse",
"responseHeaders": "En-têtes de réponse",
"stack": "Informations de la pile",
"status": "Code d'état",
"statusCode": "Code d'état",
"unknown": "Неизвестная ошибка",
"user_message_not_found": "Impossible de trouver le message d'utilisateur original"
},
@@ -1319,7 +1338,8 @@
"delete": {
"content": "La suppression du groupe de messages supprimera les questions des utilisateurs et toutes les réponses des assistants",
"title": "Supprimer le groupe de messages"
}
},
"retry_failed": "message d'erreur de nouvelle tentative"
},
"ignore": {
"knowledge": {
@@ -1550,6 +1570,7 @@
"selected": "Étiquette sélectionnée"
},
"function_calling": "Appel de fonction",
"invalid_model": "Modèle invalide",
"no_matches": "Aucun modèle disponible",
"parameter_name": "Nom du paramètre",
"parameter_type": {
@@ -1619,9 +1640,13 @@
"new_folder": "Nouveau dossier",
"new_note": "Nouvelle note",
"no_content_to_copy": "Aucun contenu à copier",
"no_file_selected": "Veuillez sélectionner le fichier à télécharger",
"only_markdown": "uniquement le format Markdown est pris en charge",
"only_one_file_allowed": "On ne peut télécharger qu'un seul fichier",
"open_folder": "ouvrir le dossier externe",
"open_outside": "Ouvrir depuis l'extérieur",
"rename": "renommer",
"rename_changed": "En raison de la politique de sécurité, le nom du fichier a été changé de {{original}} à {{final}}",
"save": "sauvegarder dans les notes",
"settings": {
"data": {
@@ -3343,6 +3368,8 @@
"label": "Déclencheur de popover de la grille"
},
"input": {
"confirm_delete_message": "Confirmer avant de supprimer le message",
"confirm_regenerate_message": "Confirmer avant de régénérer le message",
"enable_quick_triggers": "Activer les menus rapides avec '/' et '@'",
"paste_long_text_as_file": "Coller le texte long sous forme de fichier",
"paste_long_text_threshold": "Seuil de longueur de texte",
@@ -4137,7 +4164,7 @@
"aborted": "Traduction annulée"
},
"input": {
"placeholder": "Peut coller ou glisser du texte, des fichiers, des images (avec reconnaissance optique de caractères)"
"placeholder": "Peut coller ou glisser du texte, des fichiers texte ou des images (avec prise en charge de l'OCR)"
},
"language": {
"not_pair": "La langue source est différente de la langue définie",

View File

@@ -538,7 +538,10 @@
"tip": "A barra de ferramentas de blocos de código executáveis exibirá um botão de execução; atenção para não executar códigos perigosos!",
"title": "Execução de Código"
},
"code_image_tools": "Ativar ferramenta de pré-visualização",
"code_image_tools": {
"label": "Habilitar ferramenta de visualização",
"tip": "Ativar ferramentas de visualização para imagens renderizadas de blocos de código como mermaid"
},
"code_wrappable": "Bloco de código com quebra de linha",
"context_count": {
"label": "Número de contexto",
@@ -677,6 +680,7 @@
"model_placeholder": "Selecione o modelo a ser utilizado",
"model_required": "Selecione o modelo",
"select_folder": "Selecionar pasta",
"supported_providers": "Provedores de serviço suportados",
"title": "Ferramenta de código",
"update_options": "Opções de atualização",
"working_directory": "diretório de trabalho"
@@ -743,6 +747,7 @@
"delete": "Excluir",
"delete_confirm": "Tem certeza de que deseja excluir?",
"description": "Descrição",
"detail": "detalhes",
"disabled": "Desativado",
"docs": "Documentos",
"download": "Baixar",
@@ -826,6 +831,7 @@
"invalid": "Servidor MCP inválido"
}
},
"cause": "Causa do erro",
"chat": {
"chunk": {
"non_json": "Devolveu um formato de dados inválido"
@@ -835,6 +841,9 @@
"quota_exceeded": "Sua cota gratuita diária de {{quota}} tokens foi esgotada. Por favor, vá para <provider>{{provider}}</provider> para obter uma chave API e configurar a chave API para continuar usando.",
"response": "Ocorreu um erro, se a chave da API não foi configurada, por favor vá para Configurações > Provedores de Modelo para configurar a chave"
},
"data": "dados",
"detail": "Detalhes do erro",
"details": "Detalhes",
"http": {
"400": "Erro na solicitação, por favor verifique se os parâmetros da solicitação estão corretos. Se você alterou as configurações do modelo, redefina para as configurações padrão",
"401": "Falha na autenticação, por favor verifique se a chave da API está correta",
@@ -846,11 +855,13 @@
"503": "Serviço indisponível, por favor tente novamente mais tarde",
"504": "Tempo de espera do gateway excedido, por favor tente novamente mais tarde"
},
"message": "Mensagem de erro",
"missing_user_message": "Não é possível alternar a resposta do modelo: a mensagem original do usuário foi excluída. Envie uma nova mensagem para obter a resposta deste modelo",
"model": {
"exists": "O modelo já existe",
"not_exists": "O modelo não existe"
},
"name": "Nome do erro",
"no_api_key": "A chave da API não foi configurada",
"pause_placeholder": "Interrompido",
"provider_disabled": "O provedor de modelos está desativado",
@@ -858,6 +869,14 @@
"description": "Falha ao renderizar a fórmula, por favor verifique se o formato da fórmula está correto",
"title": "Erro de Renderização"
},
"requestBody": "Conteúdo da solicitação",
"requestBodyValues": "Corpo da solicitação",
"requestUrl": "Caminho da solicitação",
"responseBody": "Conteúdo da resposta",
"responseHeaders": "Cabeçalho de resposta",
"stack": "Informações da pilha",
"status": "Código de status",
"statusCode": "Código de status",
"unknown": "Erro desconhecido",
"user_message_not_found": "Não foi possível encontrar a mensagem original do usuário"
},
@@ -1319,7 +1338,8 @@
"delete": {
"content": "Excluir mensagens de grupo removerá as perguntas dos usuários e todas as respostas do assistente",
"title": "Excluir mensagens de grupo"
}
},
"retry_failed": "Repetir mensagem com erro"
},
"ignore": {
"knowledge": {
@@ -1550,6 +1570,7 @@
"selected": "Etiqueta selecionada"
},
"function_calling": "Chamada de função",
"invalid_model": "Modelo inválido",
"no_matches": "Nenhum modelo disponível",
"parameter_name": "Nome do parâmetro",
"parameter_type": {
@@ -1619,9 +1640,13 @@
"new_folder": "Nova pasta",
"new_note": "Nova nota",
"no_content_to_copy": "Não há conteúdo para copiar",
"no_file_selected": "Selecione o arquivo a ser enviado",
"only_markdown": "Apenas o formato Markdown é suportado",
"only_one_file_allowed": "só é possível enviar um arquivo",
"open_folder": "Abrir pasta externa",
"open_outside": "Abrir externamente",
"rename": "renomear",
"rename_changed": "Devido às políticas de segurança, o nome do arquivo foi alterado de {{original}} para {{final}}",
"save": "salvar em notas",
"settings": {
"data": {
@@ -3343,6 +3368,8 @@
"label": "Disparador de detalhes da grade"
},
"input": {
"confirm_delete_message": "confirmar antes de excluir a mensagem",
"confirm_regenerate_message": "Confirmar antes de regenerar a mensagem",
"enable_quick_triggers": "Ativar menu rápido com '/' e '@'",
"paste_long_text_as_file": "Colar texto longo como arquivo",
"paste_long_text_threshold": "Limite de texto longo",
@@ -4137,7 +4164,7 @@
"aborted": "Tradução interrompida"
},
"input": {
"placeholder": "Pode colar ou arrastar e soltar texto, arquivos e imagens (suporte a OCR)"
"placeholder": "Pode colar ou arrastar texto, arquivos de texto ou imagens (com suporte a OCR)"
},
"language": {
"not_pair": "O idioma de origem é diferente do idioma definido",

View File

@@ -139,7 +139,7 @@ const Markdown: FC<Props> = ({ block, postProcess }) => {
} as Partial<Components>
}, [block.id])
if (messageContent.includes('<style>')) {
if (/<style\b[^>]*>/i.test(messageContent)) {
components.style = MarkdownShadowDOMRenderer as any
}

View File

@@ -4,7 +4,16 @@ import { getHttpMessageLabel, getProviderLabel } from '@renderer/i18n/label'
import { getProviderById } from '@renderer/services/ProviderService'
import { useAppDispatch } from '@renderer/store'
import { removeBlocksThunk } from '@renderer/store/thunk/messageThunk'
import {
isSerializedAiSdkAPICallError,
isSerializedAiSdkError,
isSerializedError,
SerializedAiSdkAPICallError,
SerializedAiSdkError,
SerializedError
} from '@renderer/types/error'
import type { ErrorMessageBlock, Message } from '@renderer/types/newMessage'
import { formatAiSdkError, formatError, safeToString } from '@renderer/utils/error'
import { Alert as AntdAlert, Button, Modal } from 'antd'
import React, { useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
@@ -25,13 +34,16 @@ const ErrorBlock: React.FC<Props> = ({ block, message }) => {
const ErrorMessage: React.FC<{ block: ErrorMessageBlock }> = ({ block }) => {
const { t, i18n } = useTranslation()
const i18nKey = `error.${block.error?.i18nKey}`
const i18nKey = block.error && 'i18nKey' in block.error ? `error.${block.error?.i18nKey}` : ''
const errorKey = `error.${block.error?.message}`
const errorStatus = block.error?.status
const errorStatus =
block.error && ('status' in block.error || 'statusCode' in block.error)
? block.error?.status || block.error?.statusCode
: undefined
if (i18n.exists(i18nKey)) {
const providerId = block.error?.providerId
if (providerId) {
const providerId = block.error && 'providerId' in block.error ? block.error?.providerId : undefined
if (providerId && typeof providerId === 'string') {
return (
<Trans
i18nKey={i18nKey}
@@ -54,10 +66,10 @@ const ErrorMessage: React.FC<{ block: ErrorMessageBlock }> = ({ block }) => {
return t(errorKey)
}
if (HTTP_ERROR_CODES.includes(errorStatus)) {
if (typeof errorStatus === 'number' && HTTP_ERROR_CODES.includes(errorStatus)) {
return (
<h5>
{getHttpMessageLabel(errorStatus)} {block.error?.message}
{getHttpMessageLabel(errorStatus.toString())} {block.error?.message}
</h5>
)
}
@@ -80,15 +92,23 @@ const MessageErrorInfo: React.FC<{ block: ErrorMessageBlock; message: Message }>
}
const getAlertMessage = () => {
if (block.error && HTTP_ERROR_CODES.includes(block.error?.status)) {
const status =
block.error && ('status' in block.error || 'statusCode' in block.error)
? block.error?.status || block.error?.statusCode
: undefined
if (block.error && typeof status === 'number' && HTTP_ERROR_CODES.includes(status)) {
return block.error.message
}
return null
}
const getAlertDescription = () => {
if (block.error && HTTP_ERROR_CODES.includes(block.error?.status)) {
return getHttpMessageLabel(block.error.status)
const status =
block.error && ('status' in block.error || 'statusCode' in block.error)
? block.error?.status || block.error?.statusCode
: undefined
if (block.error && typeof status === 'number' && HTTP_ERROR_CODES.includes(status)) {
return getHttpMessageLabel(status.toString())
}
return <ErrorMessage block={block} />
}
@@ -123,7 +143,7 @@ const MessageErrorInfo: React.FC<{ block: ErrorMessageBlock; message: Message }>
interface ErrorDetailModalProps {
open: boolean
onClose: () => void
error?: Record<string, any>
error?: SerializedError
}
const ErrorDetailModal: React.FC<ErrorDetailModalProps> = ({ open, onClose, error }) => {
@@ -131,53 +151,31 @@ const ErrorDetailModal: React.FC<ErrorDetailModalProps> = ({ open, onClose, erro
const copyErrorDetails = () => {
if (!error) return
const errorText = `
${t('error.message')}: ${error.message || 'N/A'}
${t('error.requestUrl')}: ${error.url || 'N/A'}
${t('error.requestBody')}: ${error.requestBody ? JSON.stringify(error.requestBody, null, 2) : 'N/A'}
${t('error.stack')}: ${error.stack || 'N/A'}
`.trim()
let errorText: string
if (isSerializedAiSdkError(error)) {
errorText = formatAiSdkError(error)
} else if (isSerializedError(error)) {
errorText = formatError(error)
} else {
// fallback
errorText = safeToString(error)
}
navigator.clipboard.writeText(errorText)
window.message.success(t('message.copied'))
}
const renderErrorDetails = (error: any) => {
const renderErrorDetails = (error?: SerializedError) => {
if (!error) return <div>{t('error.unknown')}</div>
if (isSerializedAiSdkAPICallError(error)) {
return <AiApiCallError error={error} />
}
if (isSerializedAiSdkError(error)) {
return <AiSdkError error={error} />
}
return (
<ErrorDetailList>
{error.message && (
<ErrorDetailItem>
<ErrorDetailLabel>{t('error.message')}:</ErrorDetailLabel>
<ErrorDetailValue>{error.message}</ErrorDetailValue>
</ErrorDetailItem>
)}
{error.url && (
<ErrorDetailItem>
<ErrorDetailLabel>{t('error.requestUrl')}:</ErrorDetailLabel>
<ErrorDetailValue>{error.url}</ErrorDetailValue>
</ErrorDetailItem>
)}
{error.requestBody && (
<ErrorDetailItem>
<ErrorDetailLabel>{t('error.requestBody')}:</ErrorDetailLabel>
<CodeViewer className="source-view" language="json" expanded>
{JSON.stringify(error.requestBody, null, 2)}
</CodeViewer>
</ErrorDetailItem>
)}
{error.stack && (
<ErrorDetailItem>
<ErrorDetailLabel>{t('error.stack')}:</ErrorDetailLabel>
<StackTrace>
<pre>{error.stack}</pre>
</StackTrace>
</ErrorDetailItem>
)}
<BuiltinError error={error} />
</ErrorDetailList>
)
}
@@ -262,4 +260,110 @@ const Alert = styled(AntdAlert)`
}
`
// 作为 base渲染公共字段应当在 ErrorDetailList 中渲染
const BuiltinError = ({ error }: { error: SerializedError }) => {
const { t } = useTranslation()
return (
<>
{error.name && (
<ErrorDetailItem>
<ErrorDetailLabel>{t('error.name')}:</ErrorDetailLabel>
<ErrorDetailValue>{error.name}</ErrorDetailValue>
</ErrorDetailItem>
)}
{error.message && (
<ErrorDetailItem>
<ErrorDetailLabel>{t('error.message')}:</ErrorDetailLabel>
<ErrorDetailValue>{error.message}</ErrorDetailValue>
</ErrorDetailItem>
)}
{error.stack && (
<ErrorDetailItem>
<ErrorDetailLabel>{t('error.stack')}:</ErrorDetailLabel>
<StackTrace>
<pre>{error.stack}</pre>
</StackTrace>
</ErrorDetailItem>
)}
</>
)
}
// 作为 base渲染公共字段应当在 ErrorDetailList 中渲染
const AiSdkError = ({ error }: { error: SerializedAiSdkError }) => {
const { t } = useTranslation()
const cause = error.cause
return (
<>
<BuiltinError error={error} />
{cause && (
<ErrorDetailItem>
<ErrorDetailLabel>{t('error.cause')}:</ErrorDetailLabel>
<ErrorDetailValue>{error.cause}</ErrorDetailValue>
</ErrorDetailItem>
)}
</>
)
}
const AiApiCallError = ({ error }: { error: SerializedAiSdkAPICallError }) => {
const { t } = useTranslation()
// 这些字段是 unknown 类型,暂且不清楚都可能是什么类型,总之先覆盖下大部分场景
const requestBodyValues = safeToString(error.requestBodyValues)
const data = safeToString(error.data)
return (
<ErrorDetailList>
<AiSdkError error={error} />
{error.url && (
<ErrorDetailItem>
<ErrorDetailLabel>{t('error.requestUrl')}:</ErrorDetailLabel>
<ErrorDetailValue>{error.url}</ErrorDetailValue>
</ErrorDetailItem>
)}
{requestBodyValues && (
<ErrorDetailItem>
<ErrorDetailLabel>{t('error.requestBodyValues')}:</ErrorDetailLabel>
<CodeViewer value={safeToString(error.requestBodyValues)} className="source-view" language="json" expanded />
</ErrorDetailItem>
)}
{error.statusCode && (
<ErrorDetailItem>
<ErrorDetailLabel>{t('error.statusCode')}:</ErrorDetailLabel>
<ErrorDetailValue>{error.statusCode}</ErrorDetailValue>
</ErrorDetailItem>
)}
{error.responseHeaders && (
<ErrorDetailItem>
<ErrorDetailLabel>{t('error.responseHeaders')}:</ErrorDetailLabel>
<CodeViewer
value={JSON.stringify(error.responseHeaders, null, 2)}
className="source-view"
language="json"
expanded
/>
</ErrorDetailItem>
)}
{error.responseBody && (
<ErrorDetailItem>
<ErrorDetailLabel>{t('error.responseBody')}:</ErrorDetailLabel>
<CodeViewer value={error.responseBody} className="source-view" language="json" expanded />
</ErrorDetailItem>
)}
{data && (
<ErrorDetailItem>
<ErrorDetailLabel>{t('error.data')}:</ErrorDetailLabel>
<CodeViewer value={safeToString(error.data)} className="source-view" language="json" expanded />
</ErrorDetailItem>
)}
</ErrorDetailList>
)
}
export default React.memo(ErrorBlock)

View File

@@ -42,7 +42,19 @@ import {
} from '@renderer/utils/messageUtils/find'
import { Dropdown, Popconfirm, Tooltip } from 'antd'
import dayjs from 'dayjs'
import { AtSign, Check, FilePenLine, Languages, ListChecks, Menu, Save, Split, ThumbsUp, Upload } from 'lucide-react'
import {
AtSign,
Check,
FilePenLine,
Languages,
ListChecks,
Menu,
NotebookPen,
Save,
Split,
ThumbsUp,
Upload
} from 'lucide-react'
import { FC, memo, useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
@@ -255,15 +267,6 @@ const MessageMenubar: FC<Props> = (props) => {
onClick: () => {
SaveToKnowledgePopup.showForMessage(message)
}
},
{
label: t('notes.save'),
key: 'clipboard',
onClick: async () => {
const title = await getMessageTitle(message)
const markdown = messageToMarkdown(message)
exportMessageToNotes(title, markdown, notesPath)
}
}
]
},
@@ -382,7 +385,6 @@ const MessageMenubar: FC<Props> = (props) => {
toggleMultiSelectMode,
message,
mainTextContent,
notesPath,
messageContainerRef,
topic.name
]
@@ -620,6 +622,21 @@ const MessageMenubar: FC<Props> = (props) => {
</ActionButton>
</Tooltip>
)}
{isAssistantMessage && (
<Tooltip title={t('notes.save')} mouseEnterDelay={0.8}>
<ActionButton
className="message-action-button"
onClick={async (e) => {
e.stopPropagation()
const title = await getMessageTitle(message)
const markdown = messageToMarkdown(message)
exportMessageToNotes(title, markdown, notesPath)
}}
$softHoverBg={softHoverBg}>
<NotebookPen size={15} />
</ActionButton>
</Tooltip>
)}
{confirmDeleteMessage ? (
<Popconfirm
title={t('message.message.delete.content')}

View File

@@ -14,7 +14,7 @@ import { setNarrowMode } from '@renderer/store/settings'
import { Assistant, Topic } from '@renderer/types'
import { Tooltip } from 'antd'
import { t } from 'i18next'
import { Menu, MessageSquareDiff, PanelLeftClose, PanelRightClose, Search } from 'lucide-react'
import { Menu, PanelLeftClose, PanelRightClose, Search } from 'lucide-react'
import { AnimatePresence, motion } from 'motion/react'
import { FC } from 'react'
import styled from 'styled-components'
@@ -83,11 +83,6 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant, activeTo
<PanelLeftClose size={18} />
</NavbarIcon>
</Tooltip>
<Tooltip title={t('settings.shortcuts.new_topic')} mouseEnterDelay={0.8}>
<NavbarIcon onClick={() => EventEmitter.emit(EVENT_NAMES.ADD_NEW_TOPIC)} style={{ marginRight: 5 }}>
<MessageSquareDiff size={18} />
</NavbarIcon>
</Tooltip>
</NavbarLeft>
</motion.div>
)}

View File

@@ -2,6 +2,7 @@ import EditableNumber from '@renderer/components/EditableNumber'
import { HStack } from '@renderer/components/Layout'
import Scrollbar from '@renderer/components/Scrollbar'
import Selector from '@renderer/components/Selector'
import { HelpTooltip } from '@renderer/components/TooltipIcons'
import { DEFAULT_CONTEXTCOUNT, DEFAULT_MAX_TOKENS, DEFAULT_TEMPERATURE } from '@renderer/config/constant'
import { isOpenAIModel } from '@renderer/config/models'
import { UNKNOWN } from '@renderer/config/translate'
@@ -48,8 +49,8 @@ import {
import { Assistant, AssistantSettings, CodeStyleVarious, MathEngine, ThemeMode } from '@renderer/types'
import { modalConfirm } from '@renderer/utils'
import { getSendMessageShortcutLabel } from '@renderer/utils/input'
import { Button, Col, InputNumber, Row, Slider, Switch, Tooltip } from 'antd'
import { CircleHelp, Settings2 } from 'lucide-react'
import { Button, Col, InputNumber, Row, Slider, Switch } from 'antd'
import { Settings2 } from 'lucide-react'
import { FC, useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
@@ -193,10 +194,10 @@ const SettingsTab: FC<Props> = (props) => {
}>
<SettingGroup style={{ marginTop: 5 }}>
<Row align="middle">
<SettingRowTitleSmall>{t('chat.settings.temperature.label')}</SettingRowTitleSmall>
<Tooltip title={t('chat.settings.temperature.tip')}>
<CircleHelp size={14} style={{ marginLeft: 4 }} color="var(--color-text-2)" />
</Tooltip>
<SettingRowTitleSmall>
{t('chat.settings.temperature.label')}
<HelpTooltip title={t('chat.settings.temperature.tip')} />
</SettingRowTitleSmall>
<Switch
size="small"
style={{ marginLeft: 'auto' }}
@@ -224,10 +225,10 @@ const SettingsTab: FC<Props> = (props) => {
<SettingDivider />
)}
<Row align="middle">
<SettingRowTitleSmall>{t('chat.settings.context_count.label')}</SettingRowTitleSmall>
<Tooltip title={t('chat.settings.context_count.tip')}>
<CircleHelp size={14} style={{ marginLeft: 4 }} color="var(--color-text-2)" />
</Tooltip>
<SettingRowTitleSmall>
{t('chat.settings.context_count.label')}
<HelpTooltip title={t('chat.settings.context_count.tip')} />
</SettingRowTitleSmall>
</Row>
<Row align="middle" gutter={10}>
<Col span={23}>
@@ -256,10 +257,10 @@ const SettingsTab: FC<Props> = (props) => {
<SettingDivider />
<SettingRow>
<Row align="middle">
<SettingRowTitleSmall>{t('chat.settings.max_tokens.label')}</SettingRowTitleSmall>
<Tooltip title={t('chat.settings.max_tokens.tip')}>
<CircleHelp size={14} style={{ marginLeft: 4 }} color="var(--color-text-2)" />
</Tooltip>
<SettingRowTitleSmall>
{t('chat.settings.max_tokens.label')}
<HelpTooltip title={t('chat.settings.max_tokens.tip')} />
</SettingRowTitleSmall>
</Row>
<Switch
size="small"
@@ -327,9 +328,7 @@ const SettingsTab: FC<Props> = (props) => {
<SettingRow>
<SettingRowTitleSmall>
{t('chat.settings.thought_auto_collapse.label')}
<Tooltip title={t('chat.settings.thought_auto_collapse.tip')}>
<CircleHelp size={14} style={{ marginLeft: 4 }} color="var(--color-text-2)" />
</Tooltip>
<HelpTooltip title={t('chat.settings.thought_auto_collapse.tip')} />
</SettingRowTitleSmall>
<Switch
size="small"
@@ -426,10 +425,8 @@ const SettingsTab: FC<Props> = (props) => {
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>
{t('settings.math.single_dollar.label')}{' '}
<Tooltip title={t('settings.math.single_dollar.tip')}>
<CircleHelp size={14} style={{ marginLeft: 4 }} color="var(--color-text-2)" />
</Tooltip>
{t('settings.math.single_dollar.label')}
<HelpTooltip title={t('settings.math.single_dollar.tip')} />
</SettingRowTitleSmall>
<Switch
size="small"
@@ -457,9 +454,7 @@ const SettingsTab: FC<Props> = (props) => {
<SettingRow>
<SettingRowTitleSmall>
{t('chat.settings.code_execution.title')}
<Tooltip title={t('chat.settings.code_execution.tip')}>
<CircleHelp size={14} style={{ marginLeft: 4 }} color="var(--color-text-2)" />
</Tooltip>
<HelpTooltip title={t('chat.settings.code_execution.tip')} />
</SettingRowTitleSmall>
<Switch
size="small"
@@ -473,9 +468,7 @@ const SettingsTab: FC<Props> = (props) => {
<SettingRow style={{ paddingLeft: 8 }}>
<SettingRowTitleSmall>
{t('chat.settings.code_execution.timeout_minutes.label')}
<Tooltip title={t('chat.settings.code_execution.timeout_minutes.tip')}>
<CircleHelp size={14} style={{ marginLeft: 4 }} color="var(--color-text-2)" />
</Tooltip>
<HelpTooltip title={t('chat.settings.code_execution.timeout_minutes.tip')} />
</SettingRowTitleSmall>
<EditableNumber
size="small"
@@ -563,7 +556,10 @@ const SettingsTab: FC<Props> = (props) => {
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('chat.settings.code_image_tools')}</SettingRowTitleSmall>
<SettingRowTitleSmall>
{t('chat.settings.code_image_tools.label')}
<HelpTooltip title={t('chat.settings.code_image_tools.tip')} />
</SettingRowTitleSmall>
<Switch
size="small"
checked={codeImageTools}
@@ -713,6 +709,7 @@ const Container = styled(Scrollbar)`
const SettingRowTitleSmall = styled(SettingRowTitle)`
font-size: 13px;
gap: 4px;
`
const SettingGroup = styled.div<{ theme?: ThemeMode }>`

View File

@@ -37,6 +37,7 @@ import {
FolderOpen,
HelpCircle,
MenuIcon,
NotebookPen,
PackagePlus,
PinIcon,
PinOffIcon,
@@ -276,6 +277,14 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic,
onPinTopic(topic)
}
},
{
label: t('notes.save'),
key: 'notes',
icon: <NotebookPen size={14} />,
onClick: async () => {
exportTopicToNotes(topic, notesPath)
}
},
{
label: t('chat.topics.clear.title'),
key: 'clear-messages',
@@ -345,13 +354,6 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic,
window.message.error(t('chat.save.topic.knowledge.error.save_failed'))
}
}
},
{
label: t('notes.save'),
key: 'notes',
onClick: async () => {
exportTopicToNotes(topic, notesPath)
}
}
]
},

View File

@@ -4,8 +4,9 @@ import { isLocalAi } from '@renderer/config/env'
import { isEmbeddingModel, isRerankModel, isWebSearchModel } from '@renderer/config/models'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { getProviderName } from '@renderer/services/ProviderService'
import { useAppSelector } from '@renderer/store'
import { Assistant, Model } from '@renderer/types'
import { Button } from 'antd'
import { Button, Tag } from 'antd'
import { ChevronsUpDown } from 'lucide-react'
import { FC, useEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next'
@@ -19,6 +20,7 @@ const SelectModelButton: FC<Props> = ({ assistant }) => {
const { model, updateAssistant } = useAssistant(assistant.id)
const { t } = useTranslation()
const timerRef = useRef<NodeJS.Timeout>(undefined)
const provider = useAppSelector((state) => state.llm.providers.find((p) => p.id === model?.provider))
const modelFilter = (model: Model) => !isEmbeddingModel(model) && !isRerankModel(model)
@@ -60,6 +62,7 @@ const SelectModelButton: FC<Props> = ({ assistant }) => {
</ModelName>
</ButtonContent>
<ChevronsUpDown size={14} color="var(--color-icon)" />
{!provider && <Tag color="error">{t('models.invalid_model')}</Tag>}
</DropdownButton>
)
}

View File

@@ -25,8 +25,8 @@ const mocks = vi.hoisted(() => {
}
})
vi.mock('@renderer/components/InfoTooltip', () => ({
default: ({ title }: { title: string }) => <div>{mocks.i18n.t(title)}</div>
vi.mock('@renderer/components/TooltipIcons', () => ({
InfoTooltip: ({ title }: { title: string }) => <div>{mocks.i18n.t(title)}</div>
}))
vi.mock('react-i18next', () => ({

View File

@@ -31,8 +31,8 @@ const mocks = vi.hoisted(() => ({
}))
// Mock InfoTooltip component
vi.mock('@renderer/components/InfoTooltip', () => ({
default: ({ title, placement }: { title: string; placement: string }) => (
vi.mock('@renderer/components/TooltipIcons', () => ({
InfoTooltip: ({ title, placement }: { title: string; placement: string }) => (
<span data-testid="info-tooltip" title={title} data-placement={placement}>
</span>

View File

@@ -1,4 +1,4 @@
import InfoTooltip from '@renderer/components/InfoTooltip'
import { InfoTooltip } from '@renderer/components/TooltipIcons'
import { KnowledgeBase } from '@renderer/types'
import { Alert, InputNumber } from 'antd'
import { TriangleAlert } from 'lucide-react'

View File

@@ -1,6 +1,6 @@
import InfoTooltip from '@renderer/components/InfoTooltip'
import InputEmbeddingDimension from '@renderer/components/InputEmbeddingDimension'
import ModelSelector from '@renderer/components/ModelSelector'
import { InfoTooltip } from '@renderer/components/TooltipIcons'
import { DEFAULT_KNOWLEDGE_DOCUMENT_COUNT } from '@renderer/config/constant'
import { isEmbeddingModel, isRerankModel } from '@renderer/config/models'
import { useProviders } from '@renderer/hooks/useProvider'

View File

@@ -1,5 +1,6 @@
import { loggerService } from '@logger'
import Ellipsis from '@renderer/components/Ellipsis'
import { useFiles } from '@renderer/hooks/useFiles'
import { useKnowledge } from '@renderer/hooks/useKnowledge'
import FileItem from '@renderer/pages/files/FileItem'
import StatusIcon from '@renderer/pages/knowledge/components/StatusIcon'
@@ -48,6 +49,7 @@ const getDisplayTime = (item: KnowledgeItem) => {
const KnowledgeFiles: FC<KnowledgeContentProps> = ({ selectedBase, progressMap, preprocessMap }) => {
const { t } = useTranslation()
const [windowHeight, setWindowHeight] = useState(window.innerHeight)
const { onSelectFile, selecting } = useFiles({ extensions: fileTypes })
const { base, fileItems, addFiles, refreshItem, removeItem, getProcessingStatus } = useKnowledge(
selectedBase.id || ''
@@ -71,19 +73,12 @@ const KnowledgeFiles: FC<KnowledgeContentProps> = ({ selectedBase, progressMap,
return null
}
const handleAddFile = () => {
if (disabled) {
const handleAddFile = async () => {
if (disabled || selecting) {
return
}
const input = document.createElement('input')
input.type = 'file'
input.multiple = true
input.accept = fileTypes.join(',')
input.onchange = (e) => {
const files = (e.target as HTMLInputElement).files
files && handleDrop(Array.from(files))
}
input.click()
const selectedFiles = await onSelectFile({ multipleSelections: true })
processFiles(selectedFiles)
}
const handleDrop = async (files: File[]) => {
@@ -118,8 +113,14 @@ const KnowledgeFiles: FC<KnowledgeContentProps> = ({ selectedBase, progressMap,
}
})
.filter(({ ext }) => fileTypes.includes(ext))
const uploadedFiles = await FileManager.uploadFiles(_files)
logger.debug('uploadedFiles', uploadedFiles)
processFiles(_files)
}
}
const processFiles = async (files: FileMetadata[]) => {
logger.debug('processFiles', files)
if (files.length > 0) {
const uploadedFiles = await FileManager.uploadFiles(files)
addFiles(uploadedFiles)
}
}
@@ -150,16 +151,23 @@ const KnowledgeFiles: FC<KnowledgeContentProps> = ({ selectedBase, progressMap,
</ItemHeader>
<ItemFlexColumn>
<Dragger
showUploadList={false}
customRequest={({ file }) => handleDrop([file as File])}
multiple={true}
accept={fileTypes.join(',')}>
<p className="ant-upload-text">{t('knowledge.drag_file')}</p>
<p className="ant-upload-hint">
{t('knowledge.file_hint', { file_types: 'TXT, MD, HTML, PDF, DOCX, PPTX, XLSX, EPUB...' })}
</p>
</Dragger>
<div
onClick={(e) => {
e.stopPropagation()
handleAddFile()
}}>
<Dragger
showUploadList={false}
customRequest={({ file }) => handleDrop([file as File])}
multiple={true}
accept={fileTypes.join(',')}
openFileDialogOnClick={false}>
<p className="ant-upload-text">{t('knowledge.drag_file')}</p>
<p className="ant-upload-hint">
{t('knowledge.file_hint', { file_types: 'TXT, MD, HTML, PDF, DOCX, PPTX, XLSX, EPUB...' })}
</p>
</Dragger>
</div>
{fileItems.length === 0 ? (
<KnowledgeEmptyView />
) : (

View File

@@ -1,8 +1,8 @@
import { loggerService } from '@logger'
import AiProvider from '@renderer/aiCore'
import InfoTooltip from '@renderer/components/InfoTooltip'
import InputEmbeddingDimension from '@renderer/components/InputEmbeddingDimension'
import ModelSelector from '@renderer/components/ModelSelector'
import { InfoTooltip } from '@renderer/components/TooltipIcons'
import { isEmbeddingModel, isRerankModel } from '@renderer/config/models'
import { useModel } from '@renderer/hooks/useModel'
import { useProviders } from '@renderer/hooks/useProvider'

View File

@@ -3,7 +3,7 @@ import { NavbarCenter, NavbarHeader, NavbarRight } from '@renderer/components/ap
import { HStack } from '@renderer/components/Layout'
import { useActiveNode } from '@renderer/hooks/useNotesQuery'
import { useNotesSettings } from '@renderer/hooks/useNotesSettings'
import { useShowWorkspace } from '@renderer/hooks/useStore'
import { useShowWorkspace } from '@renderer/hooks/useShowWorkspace'
import { findNodeInTree } from '@renderer/services/NotesTreeService'
import { Breadcrumb, BreadcrumbProps, Dropdown, Tooltip } from 'antd'
import { t } from 'i18next'

View File

@@ -2,7 +2,7 @@ import { Navbar, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar
import { HStack } from '@renderer/components/Layout'
import { isMac } from '@renderer/config/constant'
import { useFullscreen } from '@renderer/hooks/useFullscreen'
import { useShowWorkspace } from '@renderer/hooks/useStore'
import { useShowWorkspace } from '@renderer/hooks/useShowWorkspace'
import { Tooltip } from 'antd'
import { PanelLeftClose, PanelRightClose } from 'lucide-react'
import { useCallback } from 'react'

View File

@@ -3,7 +3,7 @@ import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
import { RichEditorRef } from '@renderer/components/RichEditor/types'
import { useActiveNode, useFileContent, useFileContentSync } from '@renderer/hooks/useNotesQuery'
import { useNotesSettings } from '@renderer/hooks/useNotesSettings'
import { useSettings } from '@renderer/hooks/useSettings'
import { useShowWorkspace } from '@renderer/hooks/useShowWorkspace'
import {
createFolder,
createNote,
@@ -20,6 +20,7 @@ import { selectActiveFilePath, selectSortType, setActiveFilePath, setSortType }
import { NotesSortType, NotesTreeNode } from '@renderer/types/note'
import { FileChangeEvent } from '@shared/config/types'
import { useLiveQuery } from 'dexie-react-hooks'
import { AnimatePresence, motion } from 'framer-motion'
import { debounce } from 'lodash'
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
@@ -34,7 +35,7 @@ const logger = loggerService.withContext('NotesPage')
const NotesPage: FC = () => {
const editorRef = useRef<RichEditorRef>(null)
const { t } = useTranslation()
const { showWorkspace } = useSettings()
const { showWorkspace } = useShowWorkspace()
const dispatch = useAppDispatch()
const activeFilePath = useAppSelector(selectActiveFilePath)
const sortType = useAppSelector(selectSortType)
@@ -51,7 +52,6 @@ const NotesPage: FC = () => {
const [selectedFolderId, setSelectedFolderId] = useState<string | null>(null)
const watcherRef = useRef<(() => void) | null>(null)
const isSyncingTreeRef = useRef(false)
const isEditorInitialized = useRef(false)
const lastContentRef = useRef<string>('')
const lastFilePathRef = useRef<string | undefined>(undefined)
const isInitialSortApplied = useRef(false)
@@ -85,7 +85,7 @@ const NotesPage: FC = () => {
const saveCurrentNote = useCallback(
async (content: string, filePath?: string) => {
const targetPath = filePath || activeFilePath
if (!targetPath || content === currentContent) return
if (!targetPath || content.trim() === currentContent.trim()) return
try {
await window.api.file.write(targetPath, content)
@@ -113,8 +113,7 @@ const NotesPage: FC = () => {
lastContentRef.current = newMarkdown
lastFilePathRef.current = activeFilePath
// 捕获当前文件路径,避免在防抖执行时文件路径已改变的竞态条件
const currentFilePath = activeFilePath
debouncedSave(newMarkdown, currentFilePath)
debouncedSave(newMarkdown, activeFilePath)
},
[debouncedSave, activeFilePath]
)
@@ -284,26 +283,35 @@ const NotesPage: FC = () => {
])
useEffect(() => {
if (currentContent && editorRef.current) {
editorRef.current.setMarkdown(currentContent)
// 标记编辑器已初始化
isEditorInitialized.current = true
const editor = editorRef.current
if (!editor || !currentContent) return
// 获取编辑器当前内容
const editorMarkdown = editor.getMarkdown()
// 只有当编辑器内容与期望内容不一致时才更新
// 这样既能处理初始化,也能处理后续的内容同步,还能避免光标跳动
if (editorMarkdown !== currentContent) {
editor.setMarkdown(currentContent)
}
}, [currentContent, activeFilePath])
// 切换文件时重置编辑器初始化状态并兜底保存
// 切换文件时的清理工作
useEffect(() => {
if (lastContentRef.current && lastContentRef.current !== currentContent && lastFilePathRef.current) {
saveCurrentNote(lastContentRef.current, lastFilePathRef.current).catch((error) => {
logger.error('Emergency save before file switch failed:', error as Error)
})
}
return () => {
// 保存之前文件的内容
if (lastContentRef.current && lastFilePathRef.current) {
saveCurrentNote(lastContentRef.current, lastFilePathRef.current).catch((error) => {
logger.error('Emergency save before file switch failed:', error as Error)
})
}
// 重置状态
isEditorInitialized.current = false
lastContentRef.current = ''
lastFilePathRef.current = undefined
}, [activeFilePath, currentContent, saveCurrentNote])
// 取消防抖保存并清理状态
debouncedSave.cancel()
lastContentRef.current = ''
lastFilePathRef.current = undefined
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeFilePath])
// 获取目标文件夹路径(选中文件夹或根目录)
const getTargetFolderPath = useCallback(() => {
@@ -593,22 +601,31 @@ const NotesPage: FC = () => {
<NavbarCenter style={{ borderRight: 'none' }}>{t('notes.title')}</NavbarCenter>
</Navbar>
<ContentContainer id="content-container">
{showWorkspace && (
<NotesSidebar
notesTree={notesTree}
selectedFolderId={selectedFolderId}
onSelectNode={handleSelectNode}
onCreateFolder={handleCreateFolder}
onCreateNote={handleCreateNote}
onDeleteNode={handleDeleteNode}
onRenameNode={handleRenameNode}
onToggleExpanded={handleToggleExpanded}
onToggleStar={handleToggleStar}
onMoveNode={handleMoveNode}
onSortNodes={handleSortNodes}
onUploadFiles={handleUploadFiles}
/>
)}
<AnimatePresence initial={false}>
{showWorkspace && (
<motion.div
initial={{ width: 0, opacity: 0 }}
animate={{ width: 250, opacity: 1 }}
exit={{ width: 0, opacity: 0 }}
transition={{ duration: 0.3, ease: 'easeInOut' }}
style={{ overflow: 'hidden' }}>
<NotesSidebar
notesTree={notesTree}
selectedFolderId={selectedFolderId}
onSelectNode={handleSelectNode}
onCreateFolder={handleCreateFolder}
onCreateNote={handleCreateNote}
onDeleteNode={handleDeleteNode}
onRenameNode={handleRenameNode}
onToggleExpanded={handleToggleExpanded}
onToggleStar={handleToggleStar}
onMoveNode={handleMoveNode}
onSortNodes={handleSortNodes}
onUploadFiles={handleUploadFiles}
/>
</motion.div>
)}
</AnimatePresence>
<EditorWrapper>
<HeaderNavbar notesTree={notesTree} getCurrentNoteContent={getCurrentNoteContent} />
<NotesEditor

View File

@@ -303,6 +303,14 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
onClick: () => {
handleStartEdit(node)
}
},
{
label: t('notes.open_outside'),
key: 'open_outside',
icon: <FolderOpen size={14} />,
onClick: () => {
window.api.openPath(node.externalPath)
}
}
]
if (node.type !== 'folder') {
@@ -520,6 +528,7 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
const SidebarContainer = styled.div`
width: 250px;
min-width: 250px;
height: 100vh;
background-color: var(--color-background);
border-right: 0.5px solid var(--color-border);

View File

@@ -1,7 +1,7 @@
import { CheckOutlined } from '@ant-design/icons'
import { NotesSortType } from '@renderer/types/note'
import { Dropdown, Input, MenuProps, Tooltip } from 'antd'
import { ArrowLeft, ArrowUpNarrowWide, FilePlus, FolderPlus, Search, Star } from 'lucide-react'
import { ArrowLeft, ArrowUpNarrowWide, FilePlus2, FolderPlus, Search, Star } from 'lucide-react'
import { FC, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
@@ -77,7 +77,7 @@ const NotesSidebarHeader: FC<NotesSidebarHeaderProps> = ({
<Tooltip title={t('notes.new_note')} mouseEnterDelay={0.8}>
<ActionButton onClick={onCreateNote}>
<FilePlus size={18} />
<FilePlus2 size={18} />
</ActionButton>
</Tooltip>

View File

@@ -1,6 +1,6 @@
import { PlusOutlined, RedoOutlined } from '@ant-design/icons'
import { loggerService } from '@logger'
import AiProviderNew from '@renderer/aiCore/index_new'
import AiProvider from '@renderer/aiCore'
import IcImageUp from '@renderer/assets/images/paintings/ic_ImageUp.svg'
import { Navbar, NavbarCenter, NavbarRight } from '@renderer/components/app/Navbar'
import { HStack } from '@renderer/components/Layout'
@@ -203,12 +203,7 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
try {
if (mode === 'aihubmix_image_generate') {
if (painting.model.startsWith('imagen-')) {
const AI = new AiProviderNew({
id: painting.model,
provider: 'aihubmix',
name: painting.model,
group: 'imagen'
})
const AI = new AiProvider(aihubmixProvider)
const base64s = await AI.generateImage({
prompt,
model: painting.model,

View File

@@ -2,7 +2,6 @@ import 'emoji-picker-element'
import { CloseCircleFilled } from '@ant-design/icons'
import CodeEditor from '@renderer/components/CodeEditor'
import CodeViewer from '@renderer/components/CodeViewer'
import EmojiPicker from '@renderer/components/EmojiPicker'
import { Box, HSpaceBetweenStack, HStack } from '@renderer/components/Layout'
import { RichEditorRef } from '@renderer/components/RichEditor/types'
@@ -14,6 +13,7 @@ import { Button, Input, Popover } from 'antd'
import { Edit, HelpCircle, Save } from 'lucide-react'
import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import ReactMarkdown from 'react-markdown'
import styled from 'styled-components'
import { SettingDivider } from '..'
@@ -122,7 +122,14 @@ const AssistantPromptSettings: React.FC<Props> = ({ assistant, updateAssistant }
<TextAreaContainer>
<RichEditorContainer>
{showPreview ? (
<CodeViewer children={processedPrompt} language="markdown" expanded={true} height="100%" />
<MarkdownContainer
onDoubleClick={() => {
const currentScrollTop = editorRef.current?.getScrollTop?.() || 0
setShowPreview(false)
requestAnimationFrame(() => editorRef.current?.setScrollTop?.(currentScrollTop))
}}>
<ReactMarkdown>{processedPrompt || prompt}</ReactMarkdown>
</MarkdownContainer>
) : (
<CodeEditor
value={prompt}
@@ -214,4 +221,10 @@ const RichEditorContainer = styled.div`
}
`
const MarkdownContainer = styled.div.attrs({ className: 'markdown' })`
height: 100%;
padding: 0.5em;
overflow: auto;
`
export default AssistantPromptSettings

View File

@@ -1,6 +1,6 @@
// import { loggerService } from '@logger'
import InfoTooltip from '@renderer/components/InfoTooltip'
import { SuccessTag } from '@renderer/components/Tags/SuccessTag'
import { InfoTooltip } from '@renderer/components/TooltipIcons'
import { isMac, isWin } from '@renderer/config/constant'
import { useOcrProvider } from '@renderer/hooks/useOcrProvider'
import useTranslate from '@renderer/hooks/useTranslate'

View File

@@ -1,6 +1,6 @@
// import { loggerService } from '@logger'
import InfoTooltip from '@renderer/components/InfoTooltip'
import CustomTag from '@renderer/components/Tags/CustomTag'
import { InfoTooltip } from '@renderer/components/TooltipIcons'
import { TESSERACT_LANG_MAP } from '@renderer/config/ocr'
import { useOcrProvider } from '@renderer/hooks/useOcrProvider'
import useTranslate from '@renderer/hooks/useTranslate'

View File

@@ -1,7 +1,7 @@
import { InfoCircleOutlined } from '@ant-design/icons'
import InfoTooltip from '@renderer/components/InfoTooltip'
import { HStack } from '@renderer/components/Layout'
import Selector from '@renderer/components/Selector'
import { InfoTooltip } from '@renderer/components/TooltipIcons'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useEnableDeveloperMode, useSettings } from '@renderer/hooks/useSettings'
import { useTimer } from '@renderer/hooks/useTimer'

View File

@@ -55,7 +55,7 @@ const McpServersList: FC = () => {
originalList: mcpServers,
filteredList: filteredMcpServers,
onUpdate: updateMcpServers,
idKey: 'id'
itemKey: 'id'
})
const scrollRef = useRef<HTMLDivElement>(null)
@@ -251,6 +251,7 @@ const McpServersList: FC = () => {
itemKey="id"
onSortEnd={onSortEnd}
layout="grid"
gap={'12px'}
useDragOverlay
showGhost
renderItem={(server) => (

View File

@@ -116,7 +116,7 @@ export const syncModelScopeServers = async (
env: {},
isActive: true,
provider: 'ModelScope',
providerUrl: `${MODELSCOPE_HOST}/mcp/servers/@${server.id}`,
providerUrl: `${MODELSCOPE_HOST}/mcp/servers/${server.id}`,
logoUrl: server.logo_url || '',
tags: server.tags || []
}

View File

@@ -1,7 +1,7 @@
import { RedoOutlined } from '@ant-design/icons'
import InfoTooltip from '@renderer/components/InfoTooltip'
import { HStack } from '@renderer/components/Layout'
import ModelSelector from '@renderer/components/ModelSelector'
import { InfoTooltip } from '@renderer/components/TooltipIcons'
import { isEmbeddingModel, isRerankModel, isTextToImageModel } from '@renderer/config/models'
import { TRANSLATE_PROMPT } from '@renderer/config/prompts'
import { useTheme } from '@renderer/context/ThemeProvider'

View File

@@ -1,5 +1,5 @@
import InfoTooltip from '@renderer/components/InfoTooltip'
import { HStack } from '@renderer/components/Layout'
import { InfoTooltip } from '@renderer/components/TooltipIcons'
import { useProvider } from '@renderer/hooks/useProvider'
import { Provider } from '@renderer/types'
import { Flex, Switch } from 'antd'

View File

@@ -7,7 +7,7 @@ import {
VisionTag,
WebSearchTag
} from '@renderer/components/Tags/Model'
import WarnTooltip from '@renderer/components/WarnTooltip'
import { WarnTooltip } from '@renderer/components/TooltipIcons'
import { endpointTypeOptions } from '@renderer/config/endpointTypes'
import {
isEmbeddingModel,

View File

@@ -321,7 +321,7 @@ const ProviderList: FC = () => {
originalList: providers,
filteredList: filteredProviders,
onUpdate: updateProviders,
idKey: 'id'
itemKey: 'id'
})
const handleDragStart = useCallback(() => {

View File

@@ -1,6 +1,6 @@
import { loggerService } from '@logger'
import EmojiPicker from '@renderer/components/EmojiPicker'
import InfoTooltip from '@renderer/components/InfoTooltip'
import { InfoTooltip } from '@renderer/components/TooltipIcons'
import useTranslate from '@renderer/hooks/useTranslate'
import { addCustomLanguage, updateCustomLanguage } from '@renderer/services/TranslateService'
import { CustomTranslateLanguage } from '@renderer/types'

View File

@@ -71,15 +71,27 @@ const BlacklistSettings: FC = () => {
function updateManualBlacklist(blacklist: string) {
const blacklistDomains = blacklist.split('\n').filter((url) => url.trim() !== '')
const validDomains: string[] = []
const hasError = blacklistDomains.some((domain) => {
const parsed = parseMatchPattern(domain.trim())
if (parsed === null) {
return true // 有错误
const trimmedDomain = domain.trim()
// 正则表达式
if (trimmedDomain.startsWith('/') && trimmedDomain.endsWith('/')) {
try {
const regexPattern = trimmedDomain.slice(1, -1)
new RegExp(regexPattern, 'i')
validDomains.push(trimmedDomain)
return false
} catch (error) {
return true
}
} else {
const parsed = parseMatchPattern(trimmedDomain)
if (parsed === null) {
return true
}
validDomains.push(trimmedDomain)
return false
}
validDomains.push(domain.trim())
return false
})
setErrFormat(hasError)
@@ -237,7 +249,9 @@ const BlacklistSettings: FC = () => {
<Button onClick={() => updateManualBlacklist(blacklistInput)} style={{ marginTop: 10 }}>
{t('common.save')}
</Button>
{errFormat && <Alert message={t('settings.tool.websearch.blacklist_tooltip')} type="error" />}
{errFormat && (
<Alert style={{ marginTop: 10 }} message={t('settings.tool.websearch.blacklist_tooltip')} type="error" />
)}
</SettingGroup>
<SettingGroup theme={theme}>
<SettingTitle>

View File

@@ -277,7 +277,7 @@ const TranslatePage: FC = () => {
// 控制复制按钮
const onCopy = () => {
navigator.clipboard.writeText(translatedContent)
setCopied(false)
setCopied(true)
}
// 控制历史记录点击

View File

@@ -83,7 +83,8 @@ export async function fetchChatCompletion({
assistant,
options,
onChunkReceived,
topicId
topicId,
uiMessages
}: FetchChatCompletionParams) {
logger.info('fetchChatCompletion called with detailed context', {
messageCount: messages?.length || 0,
@@ -132,7 +133,8 @@ export async function fetchChatCompletion({
isImageGenerationEndpoint: isDedicatedImageGenerationModel(assistant.model || getDefaultModel()),
enableWebSearch: capabilities.enableWebSearch,
enableGenerateImage: capabilities.enableGenerateImage,
mcpTools
mcpTools,
uiMessages
}
// --- Call AI Completions ---
@@ -141,7 +143,8 @@ export async function fetchChatCompletion({
...middlewareConfig,
assistant,
topicId,
callType: 'chat'
callType: 'chat',
uiMessages
})
}

View File

@@ -134,8 +134,15 @@ export function getAssistantProvider(assistant: Assistant): Provider {
export function getProviderByModel(model?: Model): Provider {
const providers = store.getState().llm.providers
const providerId = model ? model.provider : getDefaultProvider().id
return providers.find((p) => p.id === providerId) as Provider
const provider = providers.find((p) => p.id === model?.provider)
if (!provider) {
const defaultProvider = providers.find((p) => p.id === getDefaultModel()?.provider)
const cherryinProvider = providers.find((p) => p.id === 'cherryin')
return defaultProvider || cherryinProvider || providers[0]
}
return provider
}
export function getProviderByModelId(modelId?: string) {

View File

@@ -1,7 +1,7 @@
import { convertMessagesToSdkMessages } from '@renderer/aiCore/prepareParams'
import { Assistant, Message } from '@renderer/types'
import type { StreamTextParams } from '@renderer/types/aiCoreTypes'
import { filterAdjacentUserMessaegs, filterLastAssistantMessage } from '@renderer/utils/messageUtils/filters'
import { ModelMessage } from 'ai'
import { findLast, isEmpty, takeRight } from 'lodash'
import { getAssistantSettings, getDefaultModel } from './AssistantService'
@@ -16,13 +16,16 @@ export class ConversationService {
static async prepareMessagesForModel(
messages: Message[],
assistant: Assistant
): Promise<StreamTextParams['messages']> {
): Promise<{ modelMessages: ModelMessage[]; uiMessages: Message[] }> {
const { contextCount } = getAssistantSettings(assistant)
// This logic is extracted from the original ApiService.fetchChatCompletion
// const contextMessages = filterContextMessages(messages)
const lastUserMessage = findLast(messages, (m) => m.role === 'user')
if (!lastUserMessage) {
return
return {
modelMessages: [],
uiMessages: []
}
}
const filteredMessages1 = filterAfterContextClearMessages(messages)
@@ -33,16 +36,19 @@ export class ConversationService {
const filteredMessages4 = filterAdjacentUserMessaegs(filteredMessages3)
let _messages = filterUserRoleStartMessages(
let uiMessages = filterUserRoleStartMessages(
filterEmptyMessages(filterAfterContextClearMessages(takeRight(filteredMessages4, contextCount + 2))) // 取原来几个provider的最大值
)
// Fallback: ensure at least the last user message is present to avoid empty payloads
if ((!_messages || _messages.length === 0) && lastUserMessage) {
_messages = [lastUserMessage]
if ((!uiMessages || uiMessages.length === 0) && lastUserMessage) {
uiMessages = [lastUserMessage]
}
return await convertMessagesToSdkMessages(_messages, assistant.model || getDefaultModel())
return {
modelMessages: await convertMessagesToSdkMessages(uiMessages, assistant.model || getDefaultModel()),
uiMessages
}
}
static needsWebSearch(assistant: Assistant): boolean {

View File

@@ -42,14 +42,15 @@ export class OrchestrationService {
const { messages, assistant } = request
try {
const llmMessages = await ConversationService.prepareMessagesForModel(messages, assistant)
const { modelMessages, uiMessages } = await ConversationService.prepareMessagesForModel(messages, assistant)
await fetchChatCompletion({
messages: llmMessages,
messages: modelMessages,
assistant: assistant,
options: request.options,
onChunkReceived,
topicId: request.topicId
topicId: request.topicId,
uiMessages: uiMessages
})
} catch (error: any) {
onChunkReceived({ type: ChunkType.ERROR, error })
@@ -70,17 +71,18 @@ export async function transformMessagesAndFetch(
const { messages, assistant } = request
try {
const llmMessages = await ConversationService.prepareMessagesForModel(messages, assistant)
const { modelMessages, uiMessages } = await ConversationService.prepareMessagesForModel(messages, assistant)
// replace prompt variables
assistant.prompt = await replacePromptVariables(assistant.prompt, assistant.model?.name)
await fetchChatCompletion({
messages: llmMessages,
messages: modelMessages,
assistant: assistant,
options: request.options,
onChunkReceived,
topicId: request.topicId
topicId: request.topicId,
uiMessages
})
} catch (error: any) {
onChunkReceived({ type: ChunkType.ERROR, error })

View File

@@ -34,12 +34,18 @@ class TabsService {
const remainingTabs = tabs.filter((tab) => tab.id !== tabId)
const lastTab = remainingTabs[remainingTabs.length - 1]
store.dispatch(setActiveTab(lastTab.id))
// 使用 NavigationService 导航到新的标签页
if (NavigationService.navigate) {
NavigationService.navigate(lastTab.path)
} else {
logger.error('Navigation service is not initialized')
return false
logger.warn('Navigation service not ready, will navigate on next render')
setTimeout(() => {
if (NavigationService.navigate) {
NavigationService.navigate(lastTab.path)
}
}, 100)
}
}

View File

@@ -100,7 +100,7 @@ export const createBaseCallbacks = (deps: BaseCallbacksDependencies) => {
id: uuid(),
type: 'error',
title: i18n.t('notification.assistant'),
message: serializableError.message,
message: serializableError.message ?? '',
silent: false,
timestamp: Date.now(),
source: 'assistant'

View File

@@ -97,7 +97,12 @@ export const createToolCallbacks = (deps: ToolCallbacksDependencies) => {
}
if (finalStatus === MessageBlockStatus.ERROR) {
changes.error = { message: `Tool execution failed/error`, details: toolResponse.response }
changes.error = {
message: `Tool execution failed/error`,
details: toolResponse.response,
name: null,
stack: null
}
}
blockManager.smartBlockUpdate(existingBlockId, changes, MessageBlockType.TOOL, true)

View File

@@ -67,7 +67,7 @@ const persistedReducer = persistReducer(
{
key: 'cherry-studio',
storage,
version: 145,
version: 147,
blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs'],
migrate
},

View File

@@ -2348,6 +2348,41 @@ const migrateConfig = {
logger.error('migrate 145 error', error as Error)
return state
}
},
'146': (state: RootState) => {
try {
// Migrate showWorkspace from settings to note store
if (state.settings && state.note) {
const showWorkspaceValue = (state.settings as any)?.showWorkspace
if (showWorkspaceValue !== undefined) {
state.note.settings.showWorkspace = showWorkspaceValue
// Remove from settings
delete (state.settings as any).showWorkspace
} else if (state.note.settings.showWorkspace === undefined) {
// Set default value if not exists
state.note.settings.showWorkspace = true
}
}
return state
} catch (error) {
logger.error('migrate 146 error', error as Error)
return state
}
},
'147': (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 147 error', error as Error)
return state
}
}
}

View File

@@ -9,6 +9,7 @@ export interface NotesSettings {
defaultViewMode: 'edit' | 'read'
defaultEditMode: Omit<EditorView, 'read'>
showTabStatus: boolean
showWorkspace: boolean
}
export interface NoteState {
@@ -27,7 +28,8 @@ export const initialState: NoteState = {
fontFamily: 'default',
defaultViewMode: 'edit',
defaultEditMode: 'preview',
showTabStatus: true
showTabStatus: true,
showWorkspace: true
},
notesPath: '',
sortType: 'sort_a2z'

View File

@@ -215,8 +215,6 @@ export interface SettingsState {
// API Server
apiServer: ApiServerConfig
showMessageOutline: boolean
// Notes Related
showWorkspace: boolean
}
export type MultiModelMessageStyle = 'horizontal' | 'vertical' | 'fold' | 'grid'
@@ -409,9 +407,7 @@ export const initialState: SettingsState = {
port: 23333,
apiKey: `cs-sk-${uuid()}`
},
showMessageOutline: false,
// Notes Related
showWorkspace: true
showMessageOutline: false
}
const settingsSlice = createSlice({
@@ -846,12 +842,6 @@ const settingsSlice = createSlice({
},
setShowMessageOutline: (state, action: PayloadAction<boolean>) => {
state.showMessageOutline = action.payload
},
setShowWorkspace: (state, action: PayloadAction<boolean>) => {
state.showWorkspace = action.payload
},
toggleShowWorkspace: (state) => {
state.showWorkspace = !state.showWorkspace
}
}
})
@@ -982,9 +972,7 @@ export const {
// API Server actions
setApiServerEnabled,
setApiServerPort,
setApiServerApiKey,
setShowWorkspace,
toggleShowWorkspace
setApiServerApiKey
} = settingsSlice.actions
export default settingsSlice.reducer

View File

@@ -24,6 +24,9 @@ const tabsSlice = createSlice({
name: 'tabs',
initialState,
reducers: {
setTabs: (state, action: PayloadAction<Tab[]>) => {
state.tabs = action.payload
},
addTab: (state, action: PayloadAction<Tab>) => {
const existingTab = state.tabs.find((tab) => tab.path === action.payload.path)
if (!existingTab) {
@@ -53,5 +56,5 @@ const tabsSlice = createSlice({
}
})
export const { addTab, removeTab, setActiveTab, updateTab } = tabsSlice.actions
export const { setTabs, addTab, removeTab, setActiveTab, updateTab } = tabsSlice.actions
export default tabsSlice.reducer

View File

@@ -1,4 +1,4 @@
import type { ImageModel, LanguageModel } from 'ai'
import type { AISDKError, APICallError, ImageModel, LanguageModel } from 'ai'
import { generateObject, generateText, ModelMessage, streamObject, streamText } from 'ai'
export type StreamTextParams = Omit<Parameters<typeof streamText>[0], 'model' | 'messages'> &
@@ -27,3 +27,6 @@ export type StreamObjectParams = Omit<Parameters<typeof streamObject>[0], 'model
export type GenerateObjectParams = Omit<Parameters<typeof generateObject>[0], 'model'>
export type AiSdkModel = LanguageModel | ImageModel
// 该类型用于格式化错误信息,目前只处理 APICallError待扩展
export type AiSdkErrorUnion = AISDKError | APICallError

View File

@@ -0,0 +1,32 @@
import { Serializable } from './serialize'
export interface SerializedError {
name: string | null
message: string | null
stack: string | null
[key: string]: Serializable
}
export const isSerializedError = (error: Record<string, unknown>): error is SerializedAiSdkError => {
return 'name' in error && 'message' in error && 'stack' in error
}
export interface SerializedAiSdkError extends SerializedError {
readonly cause: string | null
}
export const isSerializedAiSdkError = (error: SerializedError): error is SerializedAiSdkError => {
return 'cause' in error
}
export interface SerializedAiSdkAPICallError extends SerializedAiSdkError {
readonly url: string
readonly requestBodyValues: Serializable
readonly statusCode: number | null
readonly responseHeaders: Record<string, string> | null
readonly responseBody: string | null
readonly isRetryable: boolean
readonly data: Serializable | null
}
export const isSerializedAiSdkAPICallError = (error: SerializedError): error is SerializedAiSdkAPICallError => {
return isSerializedAiSdkError(error) && 'url' in error && 'requestBodyValues' in error && 'isRetryable' in error
}

View File

@@ -1307,6 +1307,7 @@ type BaseParams = {
options?: FetchChatCompletionOptions
onChunkReceived: (chunk: Chunk) => void
topicId?: string // 添加 topicId 参数
uiMessages?: Message[]
}
type MessagesParams = BaseParams & {
@@ -1316,7 +1317,8 @@ type MessagesParams = BaseParams & {
type PromptParams = BaseParams & {
messages?: never
// prompt: StreamTextParams['prompt']
// prompt: Just use string for convinience. Native prompt type unite more types, including messages type.
// we craete a non-intersecting prompt type to discriminate them.
// see https://github.com/vercel/ai/issues/8363
prompt: string
}

View File

@@ -16,8 +16,18 @@ export type MCPConfigSample = z.infer<typeof MCPConfigSampleSchema>
* 允许 inMemory 作为合法字段,需要额外校验 name 是否 builtin
*/
export const McpServerTypeSchema = z
.union([z.literal('stdio'), z.literal('sse'), z.literal('streamableHttp'), z.literal('inMemory')])
.default('stdio') // 大多数情况下默认使用 stdio
.string()
.transform((type) => {
if (type.includes('http')) {
return 'streamableHttp'
} else {
return type
}
})
.pipe(
z.union([z.literal('stdio'), z.literal('sse'), z.literal('streamableHttp'), z.literal('inMemory')]).default('stdio') // 大多数情况下默认使用 stdio
)
/**
* 定义单个 MCP 服务器的配置。
* FIXME: 为了兼容性,暂时允许用户编辑任意字段,这可能会导致问题。
@@ -174,6 +184,26 @@ export const McpServerConfigSchema = z
message: 'Server type is inMemory but this is not a builtin MCP server, which is not allowed'
}
)
.transform((schema) => {
// 显式传入的type会覆盖掉从url推断的逻辑
if (!schema.type) {
const url = schema.baseUrl ?? schema.url ?? null
if (url !== null) {
if (url.endsWith('/mcp')) {
return {
...schema,
type: 'streamableHttp'
} as const
} else if (url.endsWith('/sse')) {
return {
...schema,
type: 'sse'
} as const
}
}
}
return schema
})
/**
* 将服务器别名字符串ID映射到其配置的对象。
* 例如: { "my-tools": { command: "...", args: [...] }, "github": { ... } }

View File

@@ -15,6 +15,7 @@ import type {
WebSearchResponse,
WebSearchSource
} from '.'
import { SerializedError } from './error'
// MessageBlock 类型枚举 - 根据实际API返回特性优化
export enum MessageBlockType {
@@ -50,7 +51,7 @@ export interface BaseMessageBlock {
status: MessageBlockStatus // 块状态
model?: Model // 使用的模型
metadata?: Record<string, any> // 通用元数据
error?: Record<string, any> // Serializable error object instead of AISDKError
error?: SerializedError // Serializable error object instead of AISDKError
}
export interface PlaceholderMessageBlock extends BaseMessageBlock {

View File

@@ -103,7 +103,7 @@ export const isBuiltinOcrProvider = (p: OcrProvider): p is BuiltinOcrProvider =>
return isBuiltinOcrProviderId(p.id)
}
// Not sure compatiable api endpoint exists. May not support custom ocr provider
// Not sure compatible api endpoint exists. May not support custom ocr provider
export type CustomOcrProvider = OcrProvider & {
id: Exclude<string, BuiltinOcrProviderId>
}

View File

@@ -0,0 +1,68 @@
export type Serializable = null | boolean | number | string | { [key: string]: SerializableValue } | SerializableValue[]
// FIXME: any 不是可安全序列化的类型但是递归定义会报ts2589
type SerializableValue = null | boolean | number | string | { [key: string]: any } | any[]
/**
* 判断一个值是否可序列化(适合用于 Redux 状态)
* 支持嵌套对象、数组的深度检测
*/
export function isSerializable(value: unknown): boolean {
const seen = new Set() // 用于防止循环引用
function _isSerializable(val: unknown): boolean {
if (val === null || val === undefined) {
return val !== undefined // null ✅, undefined ❌
}
const type = typeof val
if (type === 'string' || type === 'number' || type === 'boolean') {
return true
}
if (type === 'object') {
// 检查循环引用
if (seen.has(val)) {
return true // 避免无限递归,假设循环引用对象本身结构合法(但实际 JSON.stringify 会报错)
}
seen.add(val)
if (Array.isArray(val)) {
return val.every((item) => _isSerializable(item))
}
// 检查是否为纯对象plain object
const proto = Object.getPrototypeOf(val)
if (proto !== null && proto !== Object.prototype && proto !== Array.prototype) {
return false // 不是 plain object比如 class 实例
}
// 检查内置对象(如 Date、RegExp、Map、Set 等)
if (
val instanceof Date ||
val instanceof RegExp ||
val instanceof Map ||
val instanceof Set ||
val instanceof Error ||
val instanceof File ||
val instanceof Blob
) {
return false
}
// 递归检查所有属性值
return Object.values(val).every((v) => _isSerializable(v))
}
// function、symbol 不可序列化
return false
}
try {
return _isSerializable(value)
} catch {
return false // 如出现循环引用错误等
}
}

View File

@@ -26,7 +26,7 @@ describe('markdownConverter', () => {
it('should convert task list HTML back to Markdown with label', () => {
const html =
'<ul data-type="taskList" class="task-list"><li data-type="taskItem" class="task-list-item" data-checked="false"><label><input type="checkbox"> abcd</label></li><li data-type="taskItem" class="task-list-item" data-checked="true"><label><input type="checkbox" checked> efgh</lable></li></ul>'
'<ul data-type="taskList" class="task-list"><li data-type="taskItem" class="task-list-item" data-checked="false"><label><input type="checkbox"> abcd</label></li><li data-type="taskItem" class="task-list-item" data-checked="true"><label><input type="checkbox" checked> efgh</label></li></ul>'
const result = htmlToMarkdown(html)
expect(result).toBe('- [ ] abcd\n\n- [x] efgh')
})
@@ -313,6 +313,26 @@ describe('markdownConverter', () => {
expect(backToMarkdown).toBe(originalMarkdown)
})
it('should maintain task list structure through html → markdown → html conversion', () => {
const originalHtml =
'<ul data-type="taskList" class="task-list"><li data-type="taskItem" class="task-list-item" data-checked="false"><label><input type="checkbox" disabled></label><div><p></p></div></li></ul>'
const markdown = htmlToMarkdown(originalHtml)
const html = markdownToHtml(markdown)
expect(html).toBe(
'<ul data-type="taskList" class="task-list">\n<li data-type="taskItem" class="task-list-item" data-checked="false"><label><input type="checkbox" disabled></label><div><p></p></div></li>\n</ul>\n'
)
})
it('should maintain task list structure through html → markdown → html conversion2', () => {
const originalHtml =
'<ul data-type="taskList" class="task-list">\n<li data-type="taskItem" class="task-list-item" data-checked="false">\n<label><input type="checkbox" disabled></label><div><p>123</p></div>\n</li>\n<li data-type="taskItem" class="task-list-item" data-checked="false">\n<label><input type="checkbox" disabled></label><div><p></p></div>\n</li>\n</ul>\n'
const markdown = htmlToMarkdown(originalHtml)
const html = markdownToHtml(markdown)
expect(html).toBe(originalHtml)
})
it('should handle complex task lists with multiple items', () => {
const originalMarkdown =
'- [ ] First unchecked task\n\n- [x] First checked task\n\n- [ ] Second unchecked task\n\n- [x] Second checked task'
@@ -361,7 +381,7 @@ describe('markdownConverter', () => {
})
describe('markdown image', () => {
it('should convert markdown iamge to HTML img tag', () => {
it('should convert markdown image to HTML img tag', () => {
const markdown = '![foo](train.jpg)'
const result = markdownToHtml(markdown)
expect(result).toBe('<p><img src="train.jpg" alt="foo" /></p>\n')

View File

@@ -1,8 +1,16 @@
import { loggerService } from '@logger'
import {
isSerializedAiSdkAPICallError,
SerializedAiSdkAPICallError,
SerializedAiSdkError,
SerializedError
} from '@renderer/types/error'
import { AISDKError, APICallError } from 'ai'
import { t } from 'i18next'
import z from 'zod'
import { safeSerialize } from './serialize'
const logger = loggerService.withContext('Utils:error')
export function getErrorDetails(err: any, seen = new WeakSet()): any {
@@ -87,12 +95,12 @@ export const formatMcpError = (error: any) => {
return error.message
}
export const serializeError = (error: AISDKError) => {
export const serializeError = (error: AISDKError): SerializedError => {
const baseError = {
name: error.name,
message: error.message,
stack: error.stack,
cause: error.cause ? String(error.cause) : undefined
stack: error.stack ?? null,
cause: error.cause ? String(error.cause) : null
}
if (APICallError.isInstance(error)) {
let content = error.message === '' ? error.responseBody || 'Unknown error' : error.message
@@ -104,11 +112,14 @@ export const serializeError = (error: AISDKError) => {
}
return {
...baseError,
status: error.statusCode,
url: error.url,
message: content,
requestBody: error.requestBodyValues
}
requestBodyValues: safeSerialize(error.requestBodyValues),
statusCode: error.statusCode ?? null,
responseBody: content,
isRetryable: error.isRetryable,
data: safeSerialize(error.data),
responseHeaders: error.responseHeaders ?? null
} satisfies SerializedAiSdkAPICallError
}
return baseError
}
@@ -123,3 +134,102 @@ export const formatZodError = (error: z.ZodError, title?: string) => {
const errorMessage = readableErrors.join('\n')
return title ? `${title}: \n${errorMessage}` : errorMessage
}
/**
* 将任意值安全地转换为字符串
* @param value - 需要转换的值unknown 类型
* @returns 转换后的字符串
*
* @description
* 该函数可以安全地处理以下情况:
* - null 和 undefined 会被转换为 'null'
* - 字符串直接返回
* - 原始类型(数字、布尔值、bigint等)使用 String() 转换
* - 对象和数组会尝试使用 JSON.stringify 序列化,并处理循环引用
* - 如果序列化失败,返回错误信息
*
* @example
* ```ts
* safeToString(null) // 'null'
* safeToString('test') // 'test'
* safeToString(123) // '123'
* safeToString({a: 1}) // '{"a":1}'
* ```
*/
export function safeToString(value: unknown): string {
// 处理 null 和 undefined
if (value == null) {
return 'null'
}
// 字符串直接返回
if (typeof value === 'string') {
return value
}
// 数字、布尔值、bigint 等原始类型,安全用 String()
if (typeof value !== 'object' && typeof value !== 'function') {
return String(value)
}
// 处理对象(包括数组)
if (typeof value === 'object') {
// 处理函数
if (typeof value === 'function') {
return value.toString()
}
// 其他对象
try {
return JSON.stringify(value, getCircularReplacer())
} catch (err) {
return '[Unserializable: ' + err + ']'
}
}
return String(value)
}
// 防止循环引用导致的 JSON.stringify 崩溃
function getCircularReplacer() {
const seen = new WeakSet()
return (_key: string, value: unknown) => {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
return '[Circular]'
}
seen.add(value)
}
return value
}
}
export function formatError(error: SerializedError): string {
return `${t('error.name')}: ${error.name}\n${t('error.message')}: ${error.message}\n${t('error.stack')}: ${error.stack}`
}
export function formatAiSdkError(error: SerializedAiSdkError): string {
let text = formatError(error) + '\n'
if (error.cause) {
text += `${t('error.cause')}: ${error.cause}\n`
}
if (isSerializedAiSdkAPICallError(error)) {
if (error.statusCode) {
text += `${t('error.statusCode')}: ${error.statusCode}\n`
}
text += `${t('error.requestUrl')}: ${error.url}\n`
const requestBodyValues = safeToString(error.requestBodyValues)
text += `${t('error.requestBodyValues')}: ${requestBodyValues}\n`
if (error.responseHeaders) {
text += `${t('error.responseHeaders')}: ${JSON.stringify(error.responseHeaders, null, 2)}\n`
}
if (error.responseBody) {
text += `${t('error.responseBody')}: ${error.responseBody}\n`
}
if (error.data) {
const data = safeToString(error.data)
text += `${t('error.data')}: ${data}\n`
}
}
return text.trim()
}

View File

@@ -182,59 +182,200 @@ export async function captureScrollableIframe(
iframeRef: React.RefObject<HTMLIFrameElement | null>
): Promise<HTMLCanvasElement | undefined> {
const iframe = iframeRef.current
if (!iframe) return Promise.resolve(undefined)
if (!iframe?.contentDocument?.defaultView) return undefined
const doc = iframe.contentDocument
const win = doc?.defaultView
if (!doc || !win) return Promise.resolve(undefined)
const win = iframe.contentWindow!
// 等待两帧渲染稳定
await new Promise<void>((r) => requestAnimationFrame(() => requestAnimationFrame(() => r())))
// 禁用动画以确保捕获静态状态
const disableAnimations = () => {
const style = doc.createElement('style')
style.textContent = `*, *::before, *::after {
animation: none !important;
transition: none !important;
// transform: none !important;
}`
doc.head.appendChild(style)
return style
}
// 触发懒加载资源尽快加载
doc.querySelectorAll('img[loading="lazy"]').forEach((img) => img.setAttribute('loading', 'eager'))
await new Promise((r) => setTimeout(r, 200))
// 内联字体以避免跨域问题
const inlineFonts = async () => {
const fontFaceRegex = /@font-face[\s\S]*?\}/g
const fontUrlRegex = /url\((['"]?)([^)"']+)\1\)/g
const fontExtRegex = /\.(woff2?|ttf|otf)(\?|#|$)/i
const de = doc.documentElement
const b = doc.body
const fetchAsDataUrl = async (url: string): Promise<string> => {
try {
const res = await fetch(url, { mode: 'cors', credentials: 'omit' })
if (!res.ok) return url
const blob = await res.blob()
return new Promise((resolve) => {
const reader = new FileReader()
reader.onloadend = () => resolve(reader.result as string)
reader.onerror = () => resolve(url)
reader.readAsDataURL(blob)
})
} catch {
return url
}
}
// 计算完整尺寸
const totalWidth = Math.max(b.scrollWidth, de.scrollWidth, b.clientWidth, de.clientWidth)
const totalHeight = Math.max(b.scrollHeight, de.scrollHeight, b.clientHeight, de.clientHeight)
const processCss = async (cssText: string, baseUrl: string): Promise<string[]> => {
const fontBlocks: string[] = []
let match: RegExpExecArray | null
logger.verbose('The iframe to be captured has size:', { totalWidth, totalHeight })
while ((match = fontFaceRegex.exec(cssText)) !== null) {
let block = match[0]
const fontUrls: Array<[string, string]> = []
// 按比例缩放以不超过上限
const MAX = 32767
const maxSide = Math.max(totalWidth, totalHeight)
const scale = maxSide > MAX ? MAX / maxSide : 1
const pixelRatio = (win.devicePixelRatio || 1) * scale
let urlMatch: RegExpExecArray | null
fontUrlRegex.lastIndex = 0
while ((urlMatch = fontUrlRegex.exec(block)) !== null) {
const url = urlMatch[2]
if (!url.startsWith('data:') && fontExtRegex.test(url)) {
try {
const absoluteUrl = new URL(url, baseUrl).href
fontUrls.push([urlMatch[0], absoluteUrl])
} catch {
// ignore
}
}
}
const bg = win.getComputedStyle(b).backgroundColor || '#ffffff'
const fg = win.getComputedStyle(b).color || '#000000'
// 并行处理所有字体URL
const dataUrls = await Promise.all(
fontUrls.map(async ([original, url]) => {
const dataUrl = await fetchAsDataUrl(url)
return [original, `url(${dataUrl})`] as const
})
)
dataUrls.forEach(([original, replacement]) => {
block = block.replace(original, replacement)
})
fontBlocks.push(block)
}
return fontBlocks
}
const allFontBlocks: string[] = []
// 处理外部样式表
const externalSheets = doc.querySelectorAll<HTMLLinkElement>('link[rel="stylesheet"]')
await Promise.all(
Array.from(externalSheets).map(async (link) => {
if (!link.href) return
try {
const res = await fetch(link.href, { mode: 'cors', credentials: 'omit' })
if (res.ok) {
const cssText = await res.text()
const blocks = await processCss(cssText, link.href)
allFontBlocks.push(...blocks)
}
} catch {
// ignore
}
})
)
// 处理内联样式
const inlineStyles = doc.querySelectorAll('style')
await Promise.all(
Array.from(inlineStyles).map(async (style) => {
const cssText = style.textContent || ''
const blocks = await processCss(cssText, doc.baseURI)
allFontBlocks.push(...blocks)
})
)
return allFontBlocks.join('\n')
}
const animationStyle = disableAnimations()
let injectedFontStyle: HTMLStyleElement | null = null
const ensureFontStyle = (css: string): HTMLStyleElement => {
const EXISTING = doc.head.querySelector('style[data-cs-inline-fonts="true"]') as HTMLStyleElement | null
if (EXISTING) {
if (css && css.trim()) {
EXISTING.textContent = `${EXISTING.textContent || ''}\n${css}`
}
return EXISTING
}
const style = doc.createElement('style')
style.setAttribute('data-cs-inline-fonts', 'true')
style.textContent = css
doc.head.appendChild(style)
return style
}
try {
const canvas = await htmlToImage.toCanvas(de, {
backgroundColor: bg,
// 等待渲染稳定
await new Promise((r) => win.requestAnimationFrame(() => win.requestAnimationFrame(() => r(null))))
// 强制加载懒加载图片
doc.querySelectorAll('img[loading="lazy"]').forEach((img) => img.setAttribute('loading', 'eager'))
// 获取字体CSS
const fontEmbedCSS = await inlineFonts()
// 将字体 CSS 注入到 iframe 文档中,确保注册到 FontFaceSet
if (fontEmbedCSS && fontEmbedCSS.trim().length > 0) {
injectedFontStyle = ensureFontStyle(fontEmbedCSS)
// 访问一次以避免被标记为未使用
if (injectedFontStyle.parentNode == null) {
doc.head.appendChild(injectedFontStyle)
}
}
// 等待字体就绪,避免序列化时回退到系统字体
await Promise.race([
(doc as any).fonts?.ready ?? Promise.resolve(),
new Promise((resolve) => setTimeout(resolve, 1000))
])
// 计算尺寸
const { documentElement: de, body: b } = doc
const totalWidth = Math.max(b.scrollWidth, de.scrollWidth, b.clientWidth, de.clientWidth)
const totalHeight = Math.max(b.scrollHeight, de.scrollHeight, b.clientHeight, de.clientHeight)
logger.verbose('Capturing iframe:', { totalWidth, totalHeight })
// 限制最大尺寸,按比例缩放
const MAX_SIZE = 32767
const scale = Math.min(1, MAX_SIZE / Math.max(totalWidth, totalHeight))
const pixelRatio = (win.devicePixelRatio || 1) * scale
const styles = win.getComputedStyle(b)
const backgroundColor = styles.backgroundColor || '#ffffff'
const color = styles.color || '#000000'
return await htmlToImage.toCanvas(de, {
fontEmbedCSS,
backgroundColor,
cacheBust: true,
pixelRatio,
skipAutoScale: true,
width: Math.floor(totalWidth),
height: Math.floor(totalHeight),
style: {
backgroundColor: bg,
color: fg,
backgroundColor,
color,
width: `${totalWidth}px`,
height: `${totalHeight}px`,
overflow: 'visible',
display: 'block'
}
})
return canvas
} catch (error) {
logger.error('Error capturing iframe full snapshot:', error as Error)
return Promise.resolve(undefined)
logger.error('Error capturing iframe:', error as Error)
return undefined
} finally {
// 恢复动画
animationStyle.remove()
}
}

Some files were not shown because too many files have changed in this diff Show More