Compare commits

...

35 Commits

Author SHA1 Message Date
GeorgeDong32 49320936f8 fix(token): include assistant reasoning token estimates and params 2025-11-07 23:28:15 +08:00
Phantom e268e69597 refactor(config): centralize home directory constant to shared config (#11158)
Replace hardcoded '.cherrystudio' directory references with HOME_CHERRY_DIR constant
2025-11-07 22:24:05 +08:00
kangfenmao 10e78ac60e refactor(MCPSettings): update styled components and enhance server synchronization
- Changed RightContainer from Scrollbar to a standard div for layout adjustments.
- Updated DetailContainer to use Scrollbar for improved scrolling behavior.
- Modified server synchronization logic across multiple providers to include allServers in the results, enhancing server management capabilities.
- Refactored provider configurations to ensure consistency and support for new server data structure.
2025-11-07 19:22:58 +08:00
kangfenmao 44b2b859da feat(MCPRouter): add MCPRouter provider support and integration
- Introduced MCPRouter provider with token management and server synchronization functionalities.
- Added MCPRouter logo to settings page for visual representation.
- Updated provider configuration to include MCPRouter details and API interactions.
- Implemented functions for saving, retrieving, and clearing MCPRouter tokens, along with server synchronization logic.
2025-11-07 18:41:15 +08:00
kangfenmao bfef0c5580 feat(MCPSettings): enhance MCP server management and localization support
- Updated auto-translation script to allow configurable max concurrent translations and delay via environment variables.
- Added new translations for "discover", "fetch", "marketplaces", "providers", and "servers" across multiple locales (en-us, zh-cn, zh-tw, de-de, el-gr, es-es, fr-fr, ja-jp, pt-pt, ru-ru).
- Improved MCPSettings UI by adjusting layout and adding a new provider settings component for better server management.
- Refactored MCP server list and market list components for improved usability and styling consistency.
2025-11-07 18:01:55 +08:00
fullex 1e8055031a Merge branch 'main' of github.com:CherryHQ/cherry-studio 2025-11-07 12:02:35 +08:00
fullex 8e33ff8d90 docs: update test plan documentation to clarify upgrade behavior for RC and Beta channels 2025-11-07 12:02:28 +08:00
kangfenmao a619000340 chore: update v1.7.0-beta.4 release notes
Update electron-builder.yml with release notes covering:
- UI framework upgrade with improved performance and UX
- New features: AWS Bedrock API key support, SophNet provider, auto session rename, TopP parameter, and reasoning effort control
- Improvements to UI components, quick panel, painting models, system shutdown handling, and package size optimization
- Bug fixes for provider support, i18n translations, and API issues
2025-11-06 20:51:03 +08:00
kangfenmao 78278ce96d refactor: remove heroui
commit 7c8bf8b591
Author: defi-failure <159208748+defi-failure@users.noreply.github.com>
Date:   Thu Nov 6 17:59:38 2025 +0800

    fix: add token usage to agent session message

commit ff8e5ddd27
Author: defi-failure <159208748+defi-failure@users.noreply.github.com>
Date:   Thu Nov 6 17:25:54 2025 +0800

    fix: close prompt stream when finish or error chunk received

commit 530e6516fd
Author: defi-failure <159208748+defi-failure@users.noreply.github.com>
Date:   Thu Nov 6 17:19:53 2025 +0800

    chore: code cleanup

commit ab21c0d56c
Author: kangfenmao <kangfenmao@qq.com>
Date:   Thu Nov 6 16:13:36 2025 +0800

    feat(SessionItem): implement auto-rename feature for sessions and improve context menu handling

    - Added a new context menu option to automatically rename sessions based on topics.
    - Introduced useDeferredValue for managing target session state.
    - Updated imports to include necessary thunk actions and components.
    - Enhanced API service to handle optional assistant model in message summary fetching.
    - Exported renameAgentSessionIfNeeded function for better accessibility in the store.

commit 21ea8ccf37
Merge: ab7b207d2 816a92c60
Author: kangfenmao <kangfenmao@qq.com>
Date:   Thu Nov 6 15:29:09 2025 +0800

    Merge branch 'main' of github.com:CherryHQ/cherry-studio into refactor/heroui-antd

    # Conflicts:
    #	src/renderer/src/pages/home/Tabs/components/AddButton.tsx
    #	src/renderer/src/pages/home/Tabs/components/SessionItem.tsx
    #	src/renderer/src/pages/home/Tabs/components/Sessions.tsx
    #	src/renderer/src/pages/home/Tabs/components/Topics.tsx
    #	src/renderer/src/pages/paintings/NewApiPage.tsx

commit ab7b207d29
Author: kangfenmao <kangfenmao@qq.com>
Date:   Thu Nov 6 14:50:05 2025 +0800

    refactor: streamline event listener management in useAppInit and update ToolPermissionRequestCard styling

commit 3834c5d402
Author: kangfenmao <kangfenmao@qq.com>
Date:   Thu Nov 6 14:21:25 2025 +0800

    refactor: enhance API server state management and remove unused initialization in useAppInit

commit a64b94a41f
Author: kangfenmao <kangfenmao@qq.com>
Date:   Thu Nov 6 13:21:58 2025 +0800

    refactor: update OpenAPI documentation paths to include subdirectories for better route coverage

commit 2e0ff28505
Author: kangfenmao <kangfenmao@qq.com>
Date:   Thu Nov 6 12:26:09 2025 +0800

    refactor: center align columns in InstalledPluginsList and set AntTable size to small

commit 84bf94e2ff
Author: defi-failure <159208748+defi-failure@users.noreply.github.com>
Date:   Thu Nov 6 12:06:09 2025 +0800

    refactor: align create agent model selection with edit agent

commit 84f2281506
Author: kangfenmao <kangfenmao@qq.com>
Date:   Thu Nov 6 11:29:32 2025 +0800

    refactor: integrate API server functionality into various components and enhance user notifications

commit 4e01210df4
Author: kangfenmao <kangfenmao@qq.com>
Date:   Thu Nov 6 10:56:38 2025 +0800

    refactor: replace ContextMenu with Dropdown in AgentItem and SessionItem components for improved context menu handling

commit 9df38c7e83
Author: kangfenmao <kangfenmao@qq.com>
Date:   Thu Nov 6 10:27:30 2025 +0800

    refactor: update AddButton styling to use CSS variable for border radius and remove unused settings hook

commit 251c269ab3
Author: kangfenmao <kangfenmao@qq.com>
Date:   Thu Nov 6 10:11:21 2025 +0800

    refactor: remove unused error handling alerts from AssistantsTab component

commit 9b9640d8d1
Author: kangfenmao <kangfenmao@qq.com>
Date:   Thu Nov 6 10:07:26 2025 +0800

    refactor: adjust margin styling for UnifiedAddButton component

commit edd6b11aa7
Author: kangfenmao <kangfenmao@qq.com>
Date:   Thu Nov 6 10:04:01 2025 +0800

    refactor: update AddButton styling based on topic position and clean up CSS for root element

commit 1c0de625d8
Author: kangfenmao <kangfenmao@qq.com>
Date:   Thu Nov 6 09:56:42 2025 +0800

    fix: update assistant addition messages for multiple languages

commit 0ea4dd4e3a
Author: dev <verc20.dev@proton.me>
Date:   Wed Nov 5 21:01:24 2025 +0800

    fix: init message api err

commit f3bbd4ed44
Author: dev <verc20.dev@proton.me>
Date:   Wed Nov 5 20:42:49 2025 +0800

    refactor: remove heroui

commit d01609fc36
Author: dev <verc20.dev@proton.me>
Date:   Wed Nov 5 19:08:41 2025 +0800

    refactor: migrate heroui/toast to antd message

commit f4b14dfc10
Author: kangfenmao <kangfenmao@qq.com>
Date:   Wed Nov 5 18:51:29 2025 +0800

    refactor: enhance Sessions component layout with styled Scrollbar and adjust UnifiedAddButton margins

commit 6ae5f69163
Author: kangfenmao <kangfenmao@qq.com>
Date:   Wed Nov 5 18:44:13 2025 +0800

    refactor: update PluginSettings and ToolingSettings for improved layout and functionality

commit fcb0020787
Author: kangfenmao <kangfenmao@qq.com>
Date:   Wed Nov 5 18:29:52 2025 +0800

    wip

commit 02265f369e
Author: dev <verc20.dev@proton.me>
Date:   Wed Nov 5 17:26:39 2025 +0800

    fix: error block related

commit 5e22d9d36f
Author: dev <verc20.dev@proton.me>
Date:   Wed Nov 5 17:14:25 2025 +0800

    fix: note head nav related

commit 3f52b7766a
Author: dev <verc20.dev@proton.me>
Date:   Wed Nov 5 16:45:49 2025 +0800

    chore: remove dead code

commit 484622f12b
Author: dev <verc20.dev@proton.me>
Date:   Wed Nov 5 16:43:12 2025 +0800

    chore: remove dead code

commit 2bceb302e0
Author: dev <verc20.dev@proton.me>
Date:   Wed Nov 5 15:33:25 2025 +0800

    fix: tool setting related

commit 5c455f25eb
Author: dev <verc20.dev@proton.me>
Date:   Wed Nov 5 13:59:33 2025 +0800

    chore: remove dead code

commit d1d1dbc046
Author: dev <verc20.dev@proton.me>
Date:   Wed Nov 5 13:51:41 2025 +0800

    fix: tool permission card related

commit bf4ec23ef7
Author: dev <verc20.dev@proton.me>
Date:   Wed Nov 5 12:22:53 2025 +0800

    fix: remove button and modal renaming

commit 47db5baeb1
Author: dev <verc20.dev@proton.me>
Date:   Wed Nov 5 12:20:36 2025 +0800

    fix: plugin setting related

commit 81fecce552
Author: kangfenmao <kangfenmao@qq.com>
Date:   Wed Nov 5 12:16:42 2025 +0800

    refactor: enhance ChatNavbarContent structure by replacing Breadcrumbs with custom layout and adding separators

commit fc64b6c611
Author: kangfenmao <kangfenmao@qq.com>
Date:   Wed Nov 5 12:10:48 2025 +0800

    refactor: simplify MessageAgentTools component structure by removing unnecessary wrapper div

commit e0f383a050
Author: kangfenmao <kangfenmao@qq.com>
Date:   Wed Nov 5 12:08:32 2025 +0800

    fix: update button classes in AddAssistantOrAgentPopup for improved cursor behavior

commit 720284262f
Author: kangfenmao <kangfenmao@qq.com>
Date:   Wed Nov 5 12:06:58 2025 +0800

    refactor: update AgentModal to use TopView for improved modal management and enhance form structure

commit b334a2c5be
Author: kangfenmao <kangfenmao@qq.com>
Date:   Wed Nov 5 11:40:47 2025 +0800

    refactor: replace UpdateDialog with UpdateDialogPopup for better modal handling

commit 468aebd632
Author: dev <verc20.dev@proton.me>
Date:   Wed Nov 5 10:56:40 2025 +0800

    fix: plugins related wip

commit bd4a979f62
Author: dev <verc20.dev@proton.me>
Date:   Tue Nov 4 17:46:14 2025 +0800

    fix: add button related

commit b3316a4dc8
Author: dev <verc20.dev@proton.me>
Date:   Tue Nov 4 17:18:31 2025 +0800

    fix: agent tool result related components

commit 6ca7597a98
Author: dev <verc20.dev@proton.me>
Date:   Tue Nov 4 11:12:01 2025 +0800

    fix: lint

commit 7d0f0b38a6
Author: kangfenmao <kangfenmao@qq.com>
Date:   Tue Nov 4 09:56:32 2025 +0800

    wip

commit 96a607a410
Author: kangfenmao <kangfenmao@qq.com>
Date:   Mon Nov 3 20:23:25 2025 +0800

    wip

commit 235ad16252
Author: kangfenmao <kangfenmao@qq.com>
Date:   Mon Nov 3 20:08:45 2025 +0800

    wip

commit f23fe1b9e9
Author: kangfenmao <kangfenmao@qq.com>
Date:   Mon Nov 3 19:15:01 2025 +0800

    wip

commit 28fac543fc
Author: kangfenmao <kangfenmao@qq.com>
Date:   Mon Nov 3 18:39:39 2025 +0800

    wip

commit 3cc7ee01e2
Author: kangfenmao <kangfenmao@qq.com>
Date:   Mon Nov 3 17:33:13 2025 +0800

    wip

commit 37bdf9e508
Author: kangfenmao <kangfenmao@qq.com>
Date:   Sat Nov 1 19:16:58 2025 +0800

    wip

commit 1bf5104f97
Author: kangfenmao <kangfenmao@qq.com>
Date:   Sat Nov 1 12:12:01 2025 +0800

    wip
2025-11-06 18:27:43 +08:00
Phantom 76483d828e ci(i18n): change auto i18n workflow to run weekly (#11152)
* ci(i18n): change auto i18n workflow to run weekly

Update the workflow to run on a weekly schedule instead of on pull request events.
Also simplify the workflow by using yarn for dependency management and create a PR
for changes instead of committing directly to the branch.

* ci(github-actions): improve workflow step names with emojis

Use emojis in step names to enhance readability and visual scanning of workflow logs

* ci(workflow): prevent committing package.json and yarn.lock changes in i18n workflow
2025-11-06 18:25:04 +08:00
Jake Jia 816a92c609 feat(app-menu): add full i18n support and sync lanuage with app language settings (#11131)
Previously, the macOS menu bar was always displayed in English regardless of
system language or in-app language settings. This change enables the menu bar
to dynamically follow the application's language preference.

Key changes:
- Add language change listener to automatically update menu when user switches language
- Refactor AppMenuService with proper subscription management and cleanup
- Add appMenu translations for en-us, zh-cn, and zh-tw locales
- Implement destroy method to prevent memory leaks from config subscriptions
- Convert all menu items (File, Edit, View, Window, Help) to use localized labels

The menu bar now respects the in-app language setting and updates in real-time
when users change their preferences, providing a consistent multilingual experience.
2025-11-06 14:46:42 +08:00
beyondkmp 83e4d4363f fix: add Perplexity provider support and update API host formatting (#11162)
* feat: add Perplexity provider support and update API host formatting

- Introduced `isPerplexityProvider` function to identify Perplexity providers.
- Updated `formatProviderApiHost` to handle Perplexity provider API host formatting.
- Added unit tests for Perplexity provider configuration to ensure correct API host formatting behavior.

* fix: add 'perplexity' to unsupported API version providers list
2025-11-06 10:43:33 +08:00
Phantom 1103449a4f fix: wrong migration in #10727 (#11151) 2025-11-05 14:33:07 +08:00
beyondkmp 56c7a7f066 🐛 fix: resolve TypeScript type conflicts and React hooks warnings (#11148)
* 🐛 fix: resolve TypeScript type conflicts and React hooks warnings

- Add @smithy/types@4.7.1 to resolutions to unify AWS SDK dependencies
- Wrap updatePaintingState in useCallback to fix exhaustive-deps warning
- Fix AWS Bedrock client type incompatibility issues

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

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

* fix(i18n): Auto update translations for PR #11148

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: GitHub Action <action@github.com>
2025-11-05 14:19:14 +08:00
Phantom caa59c4c50 refactor(Topics & Sessions): Style and code structure adjustments (#10868)
* refactor(Tabs): extract shared styled components into separate file

Move common styled components (ListItem, ListItemNameContainer, ListItemName, ListItemEditInput) from SessionItem.tsx and Topics.tsx into shared.tsx to improve code reuse and maintainability

* refactor(components): extract ListContainer component for shared tab layouts

Create reusable ListContainer component to standardize layout styling across tabs
Replace manual div containers in Sessions and Topics components with new ListContainer

* refactor(ListItem): convert styled component to Tailwind CSS function component

- Convert ListItem from styled-components to Tailwind CSS function component
- Maintain all original styling and hover/active states
- Use HTMLDivElement props interface for proper TypeScript typing
- Preserve CSS custom properties for theme variables

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

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

* refactor(ListItemNameContainer): convert styled component to Tailwind CSS function component

- Convert ListItemNameContainer from styled-components to Tailwind CSS function component
- Simplify layout styles using Tailwind's utility classes
- Use HTMLDivElement props interface for proper TypeScript typing

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

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

* refactor(ListItemName): convert styled component to Tailwind CSS function component

- Convert ListItemName from styled-components to Tailwind CSS function component
- Use inline styles for webkit-specific line clamping properties
- Remove complex animations from component definition (can be added via CSS classes)
- Use HTMLDivElement props interface for proper TypeScript typing

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

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

* refactor(ListItemEditInput): convert styled component to Tailwind CSS function component

- Convert ListItemEditInput from styled-components to Tailwind CSS function component
- Use proper InputHTMLAttributes type for input elements
- Remove styled-components import as no longer needed
- Maintain all original styling using Tailwind utility classes

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

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

* refactor(components): improve type safety and class ordering in shared components

- Replace HTMLAttributes with more specific ComponentProps types
- Reorder class names for better readability and consistency

* refactor(components): update styling and class handling in list items

- Replace deprecated classNames utility with cn from @heroui/react
- Consolidate style properties into className using cn
- Improve CSS selector syntax for better specificity
- Standardize padding and border radius values

* Revert "refactor(ListItemName): convert styled component to Tailwind CSS function component"

This reverts commit 196136068d.

* style(shared): increase font size and remove redundant padding

The font size was increased from 13px to 14px for better readability. Redundant padding in ListItemEditInput was removed to maintain consistent styling.

* refactor(AddButton): simplify component by removing FC type and inline props

Remove unnecessary FC type declaration and inline the Props interface with ButtonProps. Also clean up prop spreading by moving it to the end of the component.

* style(Topics): remove redundant className and add overflow styles

* refactor(components): extract MenuButton to shared components

Move MenuButton implementation from individual components to shared module to reduce code duplication and improve maintainability

* refactor(PendingIndicator): convert styled component to Tailwind CSS function component

- Convert PendingIndicator from styled-components to Tailwind CSS function component
- Use ComponentPropsWithoutRef<'div'> for consistent TypeScript typing
- Replace styled-components attrs with Tailwind animate-pulse class
- Use CSS custom properties for pulse-size variable
- Remove styled-components import as no longer needed

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

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

* refactor(components): replace styled indicators with shared StatusIndicator

Consolidate PendingIndicator and FulfilledIndicator into a single StatusIndicator component with variant support

* style(shared.tsx): adjust border styles for singlealone active state

* refactor: use type-only imports for react props

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-05 14:14:40 +08:00
fullex 2546dfbe5d chore: update Node.js version to 22 and Yarn version to 4.9.1 across workflows and documentation 2025-11-05 12:54:30 +08:00
beyondkmp 5fea202a7d fix: add PowerMonitorService for system shutdown handling (#11115)
* feat: add PowerMonitorService for system shutdown handling

- Add PowerMonitorService to monitor system shutdown events
- Use @paymoapp/electron-shutdown-handler for Windows platform
- Use Electron's powerMonitor for macOS and Linux platforms
- Support registering multiple shutdown handlers via dependency injection
- Register shutdown handlers in ipc.ts to disable auto-update and save data

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

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

* format code

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-04 18:56:09 +08:00
fullex 7dce1d776b feat: app's version history log (#11097)
* feat: integrate version tracking in app initialization

- Added versionService to record the current version during app startup.
- This change prepares for upcoming data refactoring in version 2.

* fix: lint from other PRs & format

* feat: enhance version tracking with meaningful change detection

- Updated VersionService to check for changes in version, OS, environment, packaged status, and install mode before recording a new entry.
- Improved logging to reflect whether version information has changed or remained the same.
2025-11-04 14:13:07 +08:00
beyondkmp 346af4d338 fix: add CherryAI provider support and update API host formatting (#11135)
* fix: add CherryAI provider support and update API host formatting

* format code

* add ut

* format code
2025-11-04 12:59:14 +08:00
Zephyr abd5d3b96f feat: amazon bedrock request use bedrock api key (#10727)
* feat: amazon bedrock request use bedrock api key

* feat: ai-core/provider support bedrock api key

* refactor: extract AWS Bedrock auth type and remove redundant state

* feat: add bedrock reasoning support

Add AWS Bedrock-specific reasoning parameter handling to support Extended Thinking feature for Claude models via Bedrock API.

Changes:
- Add `buildBedrockProviderOptions` function in options.ts to handle Bedrock-specific provider options
- Add `getBedrockReasoningParams` function in reasoning.ts to generate reasoning config with budget tokens
- Register 'bedrock' case in provider options switch to route to Bedrock-specific builder
- Reuse `getAnthropicThinkingBudget` helper for consistent token budget calculation

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

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

* feat: add migration for Bedrock auth type and API key fields

* refactor: replace any type with BedrockRuntimeClientConfig in AWS Bedrock client

* fix: bug fix

* fix: lint error

* fix: bedrock reasoning

* chore: bump persisted reducer version to 171

* Update src/renderer/src/store/migrate.ts

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: icarus <eurfelux@gmail.com>
2025-11-03 21:05:10 +08:00
Phantom 49bd298d37 feat(InputbarTools): add reasoning effort button to quick panel (#10959)
Add new menu item with lightbulb icon that opens the reasoning effort quick panel when clicked
2025-11-03 20:36:52 +08:00
Phantom 714a28ac29 fix(QuickPanel): Hide the options that should be hidden in the quick panel. (#10931)
* feat(QuickPanel): add hidden property to list items

Add support for hiding QuickPanel items by introducing a hidden property. This allows conditional visibility of items like the knowledge base button based on application state.

* docs(types): clarify settings field comment in Assistant type
2025-11-03 20:34:24 +08:00
beyondkmp 0cf81c04c8 chore: update electron-builder.yml to exclude additional configuration files from build (#11129)
* chore: update electron-builder.yml to exclude additional configuration files from build

* delete all hide files
2025-11-03 17:54:29 +08:00
kangfenmao 4186e9c990 feat: add support for TopP in model capabilities and update parameter builder to utilize it 2025-11-03 16:37:12 +08:00
kangfenmao d8f68a6056 feat: initialize painting model with first available option and update default provider to 'cherryin' 2025-11-03 15:12:58 +08:00
kangfenmao 11bf50e722 fix(i18n): improve label retrieval for paintings image size options 2025-11-03 14:45:21 +08:00
kangfenmao 32a84311aa feat: add SophNet LLM provider 2025-11-03 13:28:40 +08:00
beyondkmp 6eaa2b2461 refactor: remove main window dependency from PythonService and utilize WindowService for window management (#11116)
* refactor: remove main window dependency from PythonService and utilize WindowService for window management

* format code
2025-11-03 13:09:40 +08:00
defi-failure 9f00f00546 chore: update v1.7.0-beta.3 release notes (#11105)
* chore: update v1.7.0-beta.3 release notes

* fix(i18n): Auto update translations for PR #11105

* fix: code lint error

---------

Co-authored-by: GitHub Action <action@github.com>
2025-11-02 22:28:36 +08:00
SuYao bd94d23343 refactor:Unify the naming of configuration fields in thinking, change to using underscore style. (#11106)
* refactor:Unify the naming of configuration fields in thinking, change to using underscore style.

* fix(i18n): Auto update translations for PR #11106

* chore: lint

* fix: typecheck

---------

Co-authored-by: GitHub Action <action@github.com>
2025-11-02 19:24:23 +08:00
chenxue 5f1c14e2c0 fix(aihubmix): fix default rules missing app code (#11100)
* add imagen

* Update aihubmix.ts

* fix type

---------

Co-authored-by: zhaochenxue <zhaochenxue@bixin.cn>
2025-11-02 17:03:05 +08:00
dependabot[bot] cdc12d5092 ci(deps): bump actions/stale from 9 to 10 (#11088)
Bumps [actions/stale](https://github.com/actions/stale) from 9 to 10.
- [Release notes](https://github.com/actions/stale/releases)
- [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/stale/compare/v9...v10)

---
updated-dependencies:
- dependency-name: actions/stale
  dependency-version: '10'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-02 08:44:53 +08:00
dependabot[bot] e5967fd874 ci(deps): bump actions/upload-artifact from 4 to 5 (#11089)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-02 08:44:41 +08:00
dependabot[bot] e2f1d80697 ci(deps): bump actions/setup-node from 4 to 6 (#11090)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 6.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-02 08:44:28 +08:00
SuYao 28bc89ac7c perf: optimize QR code generation and connection info for phone LAN export (#11086)
* Increase QR code margin for better scanning reliability

- Change QRCodeSVG marginSize from 2 to 4 pixels
- Maintains same QR code size (160px) and error correction level (Q)
- Improves readability and scanning success rate on mobile devices

* Optimize QR code generation and connection info for phone LAN export

- Increase QR code size to 180px and reduce error correction to 'L' for better mobile scanning
- Replace hardcoded logo path with AppLogo config and increase logo size to 60px
- Simplify connection info by removing candidates array and using only essential IP/port data

* Optimize QR code data structure for LAN connection

- Compress IP addresses to numeric format to reduce QR code complexity
- Use compact array format instead of verbose JSON object structure
- Remove debug logging to streamline connection flow

* feat: 更新 WebSocket 状态和候选者响应类型,优化连接信息处理

* Increase QR code size and error correction for better scanning

- Increase QR code size from 180px to 300px for improved readability
- Change error correction level from L (low) to H (high) for better reliability
- Reduce logo size from 60px to 40px to accommodate larger QR data
- Increase margin size from 1 to 2 for better border clearance

* 调整二维码大小和图标尺寸以优化扫描体验

* fix(i18n): Auto update translations for PR #11086

* fix(i18n): Auto update translations for PR #11086

* fix(i18n): Auto update translations for PR #11086

---------

Co-authored-by: GitHub Action <action@github.com>
2025-11-01 12:13:11 +08:00
186 changed files with 6321 additions and 8077 deletions
+57 -32
View File
@@ -1,4 +1,4 @@
name: Auto I18N name: Auto I18N Weekly
env: env:
TRANSLATION_API_KEY: ${{ secrets.TRANSLATE_API_KEY }} TRANSLATION_API_KEY: ${{ secrets.TRANSLATE_API_KEY }}
@@ -7,14 +7,15 @@ env:
TRANSLATION_BASE_LOCALE: ${{ vars.AUTO_I18N_BASE_LOCALE || 'en-us'}} TRANSLATION_BASE_LOCALE: ${{ vars.AUTO_I18N_BASE_LOCALE || 'en-us'}}
on: on:
pull_request: schedule:
types: [opened, synchronize, reopened] # Runs at 00:00 UTC every Sunday.
# This corresponds to 08:00 AM UTC+8 (Beijing time) every Sunday.
- cron: "0 0 * * 0"
workflow_dispatch: workflow_dispatch:
jobs: jobs:
auto-i18n: auto-i18n:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.full_name == 'CherryHQ/cherry-studio'
name: Auto I18N name: Auto I18N
permissions: permissions:
contents: write contents: write
@@ -24,45 +25,69 @@ jobs:
- name: 🐈‍⬛ Checkout - name: 🐈‍⬛ Checkout
uses: actions/checkout@v5 uses: actions/checkout@v5
with: with:
ref: ${{ github.event.pull_request.head.ref }} fetch-depth: 0
- name: 📦 Setting Node.js - name: 📦 Setting Node.js
uses: actions/setup-node@v5 uses: actions/setup-node@v6
with: with:
node-version: 20 node-version: 22
package-manager-cache: false
- name: 📦 Install dependencies in isolated directory - name: 📦 Install corepack
run: corepack enable && corepack prepare yarn@4.9.1 --activate
- name: 📂 Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT
- name: 💾 Cache yarn dependencies
uses: actions/cache@v4
with:
path: |
${{ steps.yarn-cache-dir-path.outputs.dir }}
node_modules
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: 📦 Install dependencies
run: | run: |
# 在临时目录安装依赖 yarn install
mkdir -p /tmp/translation-deps
cd /tmp/translation-deps
echo '{"dependencies": {"@cherrystudio/openai": "^6.5.0", "cli-progress": "^3.12.0", "tsx": "^4.20.3", "@biomejs/biome": "2.2.4"}}' > package.json
npm install --no-package-lock
# 设置 NODE_PATH 让项目能找到这些依赖
echo "NODE_PATH=/tmp/translation-deps/node_modules" >> $GITHUB_ENV
- name: 🏃‍♀️ Translate - name: 🏃‍♀️ Translate
run: npx tsx scripts/sync-i18n.ts && npx tsx scripts/auto-translate-i18n.ts run: yarn sync:i18n && yarn auto:i18n
- name: 🔍 Format - name: 🔍 Format
run: cd /tmp/translation-deps && npx biome format --config-path /home/runner/work/cherry-studio/cherry-studio/biome.jsonc --write /home/runner/work/cherry-studio/cherry-studio/src/renderer/src/i18n/ run: yarn format
- name: 🔄 Commit changes - name: 🔍 Check for changes
id: git_status
run: | run: |
git config --local user.email "action@github.com" # Check if there are any uncommitted changes
git config --local user.name "GitHub Action"
git add .
git reset -- package.json yarn.lock # 不提交 package.json 和 yarn.lock 的更改 git reset -- package.json yarn.lock # 不提交 package.json 和 yarn.lock 的更改
if git diff --cached --quiet; then git diff --exit-code --quiet || echo "::set-output name=has_changes::true"
echo "No changes to commit" git status --porcelain
else
git commit -m "fix(i18n): Auto update translations for PR #${{ github.event.pull_request.number }}"
fi
- name: 🚀 Push changes - name: 📅 Set current date for PR title
uses: ad-m/github-push-action@master id: set_date
run: echo "CURRENT_DATE=$(date +'%b %d, %Y')" >> $GITHUB_ENV # e.g., "Jun 06, 2024"
- name: 🚀 Create Pull Request if changes exist
if: steps.git_status.outputs.has_changes == 'true'
uses: peter-evans/create-pull-request@v6
with: with:
github_token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }} # Use the built-in GITHUB_TOKEN for bot actions
branch: ${{ github.event.pull_request.head.ref }} commit-message: "feat(bot): Weekly automated script run"
title: "🤖 Weekly Automated Update: ${{ env.CURRENT_DATE }}"
body: |
This PR includes changes generated by the weekly auto i18n.
Review the changes before merging.
---
_Generated by the automated weekly workflow_
branch: "auto-i18n-weekly-${{ github.run_id }}" # Unique branch name
base: "main" # Or 'develop', set your base branch
delete-branch: true # Delete the branch after merging or closing the PR
- name: 📢 Notify if no changes
if: steps.git_status.outputs.has_changes != 'true'
run: echo "Bot script ran, but no changes were detected. No PR created."
+5 -5
View File
@@ -5,7 +5,7 @@ on:
types: [opened] types: [opened]
schedule: schedule:
# Run every day at 8:30 Beijing Time (00:30 UTC) # Run every day at 8:30 Beijing Time (00:30 UTC)
- cron: '30 0 * * *' - cron: "30 0 * * *"
workflow_dispatch: workflow_dispatch:
jobs: jobs:
@@ -54,9 +54,9 @@ jobs:
- name: Setup Node.js - name: Setup Node.js
if: steps.check_time.outputs.should_delay == 'false' if: steps.check_time.outputs.should_delay == 'false'
uses: actions/setup-node@v4 uses: actions/setup-node@v6
with: with:
node-version: '20' node-version: 22
- name: Process issue with Claude - name: Process issue with Claude
if: steps.check_time.outputs.should_delay == 'false' if: steps.check_time.outputs.should_delay == 'false'
@@ -121,9 +121,9 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v6
with: with:
node-version: '20' node-version: 22
- name: Process pending issues with Claude - name: Process pending issues with Claude
uses: anthropics/claude-code-action@main uses: anthropics/claude-code-action@main
+2 -2
View File
@@ -21,7 +21,7 @@ jobs:
contents: none contents: none
steps: steps:
- name: Close needs-more-info issues - name: Close needs-more-info issues
uses: actions/stale@v9 uses: actions/stale@v10
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
only-labels: 'needs-more-info' only-labels: 'needs-more-info'
@@ -42,7 +42,7 @@ jobs:
days-before-pr-close: -1 days-before-pr-close: -1
- name: Close inactive issues - name: Close inactive issues
uses: actions/stale@v9 uses: actions/stale@v10
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: ${{ env.daysBeforeStale }} days-before-stale: ${{ env.daysBeforeStale }}
+5 -5
View File
@@ -3,7 +3,7 @@ name: Nightly Build
on: on:
workflow_dispatch: workflow_dispatch:
schedule: schedule:
- cron: '0 17 * * *' # 1:00 BJ Time - cron: "0 17 * * *" # 1:00 BJ Time
permissions: permissions:
contents: write contents: write
@@ -56,9 +56,9 @@ jobs:
ref: main ref: main
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v5 uses: actions/setup-node@v6
with: with:
node-version: 20 node-version: 22
- name: macos-latest dependencies fix - name: macos-latest dependencies fix
if: matrix.os == 'macos-latest' if: matrix.os == 'macos-latest'
@@ -66,7 +66,7 @@ jobs:
brew install python-setuptools brew install python-setuptools
- name: Install corepack - name: Install corepack
run: corepack enable && corepack prepare yarn@4.6.0 --activate run: corepack enable && corepack prepare yarn@4.9.1 --activate
- name: Get yarn cache directory path - name: Get yarn cache directory path
id: yarn-cache-dir-path id: yarn-cache-dir-path
@@ -208,7 +208,7 @@ jobs:
echo "总计: $(find renamed-artifacts -type f | wc -l) 个文件" echo "总计: $(find renamed-artifacts -type f | wc -l) 个文件"
- name: Upload artifacts - name: Upload artifacts
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v5
with: with:
name: cherry-studio-nightly-${{ steps.date.outputs.date }}-${{ matrix.os }} name: cherry-studio-nightly-${{ steps.date.outputs.date }}-${{ matrix.os }}
path: renamed-artifacts/* path: renamed-artifacts/*
+3 -3
View File
@@ -24,12 +24,12 @@ jobs:
uses: actions/checkout@v5 uses: actions/checkout@v5
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v5 uses: actions/setup-node@v6
with: with:
node-version: 20 node-version: 22
- name: Install corepack - name: Install corepack
run: corepack enable && corepack prepare yarn@4.6.0 --activate run: corepack enable && corepack prepare yarn@4.9.1 --activate
- name: Get yarn cache directory path - name: Get yarn cache directory path
id: yarn-cache-dir-path id: yarn-cache-dir-path
+6 -6
View File
@@ -4,9 +4,9 @@ on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
tag: tag:
description: 'Release tag (e.g. v1.0.0)' description: "Release tag (e.g. v1.0.0)"
required: true required: true
default: 'v1.0.0' default: "v1.0.0"
push: push:
tags: tags:
- v*.*.* - v*.*.*
@@ -47,9 +47,9 @@ jobs:
npm version "$VERSION" --no-git-tag-version --allow-same-version npm version "$VERSION" --no-git-tag-version --allow-same-version
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v5 uses: actions/setup-node@v6
with: with:
node-version: 20 node-version: 22
- name: macos-latest dependencies fix - name: macos-latest dependencies fix
if: matrix.os == 'macos-latest' if: matrix.os == 'macos-latest'
@@ -57,7 +57,7 @@ jobs:
brew install python-setuptools brew install python-setuptools
- name: Install corepack - name: Install corepack
run: corepack enable && corepack prepare yarn@4.6.0 --activate run: corepack enable && corepack prepare yarn@4.9.1 --activate
- name: Get yarn cache directory path - name: Get yarn cache directory path
id: yarn-cache-dir-path id: yarn-cache-dir-path
@@ -127,5 +127,5 @@ jobs:
allowUpdates: true allowUpdates: true
makeLatest: false makeLatest: false
tag: ${{ steps.get-tag.outputs.tag }} tag: ${{ steps.get-tag.outputs.tag }}
artifacts: 'dist/*.exe,dist/*.zip,dist/*.dmg,dist/*.AppImage,dist/*.snap,dist/*.deb,dist/*.rpm,dist/*.tar.gz,dist/latest*.yml,dist/rc*.yml,dist/beta*.yml,dist/*.blockmap' artifacts: "dist/*.exe,dist/*.zip,dist/*.dmg,dist/*.AppImage,dist/*.snap,dist/*.deb,dist/*.rpm,dist/*.tar.gz,dist/latest*.yml,dist/rc*.yml,dist/beta*.yml,dist/*.blockmap"
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
-2
View File
@@ -7,7 +7,6 @@ This file provides guidance to AI coding assistants when working with code in th
- **Keep it clear**: Write code that is easy to read, maintain, and explain. - **Keep it clear**: Write code that is easy to read, maintain, and explain.
- **Match the house style**: Reuse existing patterns, naming, and conventions. - **Match the house style**: Reuse existing patterns, naming, and conventions.
- **Search smart**: Prefer `ast-grep` for semantic queries; fall back to `rg`/`grep` when needed. - **Search smart**: Prefer `ast-grep` for semantic queries; fall back to `rg`/`grep` when needed.
- **Build with HeroUI**: Use HeroUI for every new UI component; never add `antd` or `styled-components`.
- **Log centrally**: Route all logging through `loggerService` with the right context—no `console.log`. - **Log centrally**: Route all logging through `loggerService` with the right context—no `console.log`.
- **Research via subagent**: Lean on `subagent` for external docs, APIs, news, and references. - **Research via subagent**: Lean on `subagent` for external docs, APIs, news, and references.
- **Always propose before executing**: Before making any changes, clearly explain your planned approach and wait for explicit user approval to ensure alignment and prevent unwanted modifications. - **Always propose before executing**: Before making any changes, clearly explain your planned approach and wait for explicit user approval to ensure alignment and prevent unwanted modifications.
@@ -41,7 +40,6 @@ This file provides guidance to AI coding assistants when working with code in th
- **Services** (`src/main/services/`): MCPService, KnowledgeService, WindowService, etc. - **Services** (`src/main/services/`): MCPService, KnowledgeService, WindowService, etc.
- **Build System**: Electron-Vite with experimental rolldown-vite, yarn workspaces. - **Build System**: Electron-Vite with experimental rolldown-vite, yarn workspaces.
- **State Management**: Redux Toolkit (`src/renderer/src/store/`) for predictable state. - **State Management**: Redux Toolkit (`src/renderer/src/store/`) for predictable state.
- **UI Components**: HeroUI (`@heroui/*`) for all new UI elements.
### Logging ### Logging
```typescript ```typescript
+2 -2
View File
@@ -18,13 +18,13 @@ yarn
### Setup Node.js ### Setup Node.js
Download and install [Node.js v20.x.x](https://nodejs.org/en/download) Download and install [Node.js v22.x.x](https://nodejs.org/en/download)
### Setup Yarn ### Setup Yarn
```bash ```bash
corepack enable corepack enable
corepack prepare yarn@4.6.0 --activate corepack prepare yarn@4.9.1 --activate
``` ```
### Install Dependencies ### Install Dependencies
+2
View File
@@ -11,6 +11,8 @@ The Test Plan is divided into the RC channel and the Beta channel, with the foll
Users can enable the "Test Plan" and select the version channel in the software's `Settings` > `About`. Please note that the versions in the "Test Plan" cannot guarantee data consistency, so be sure to back up your data before using them. Users can enable the "Test Plan" and select the version channel in the software's `Settings` > `About`. Please note that the versions in the "Test Plan" cannot guarantee data consistency, so be sure to back up your data before using them.
After enabling the RC channel or Beta channel, if a stable version is released, users will still be upgraded to the stable version.
Users are welcome to submit issues or provide feedback through other channels for any bugs encountered during testing. Your feedback is very important to us. Users are welcome to submit issues or provide feedback through other channels for any bugs encountered during testing. Your feedback is very important to us.
## Developer Guide ## Developer Guide
+2
View File
@@ -11,6 +11,8 @@
用户可以在软件的`设置`-`关于`中,开启“测试计划”并选择版本通道。请注意“测试计划”的版本无法保证数据的一致性,请使用前一定要备份数据。 用户可以在软件的`设置`-`关于`中,开启“测试计划”并选择版本通道。请注意“测试计划”的版本无法保证数据的一致性,请使用前一定要备份数据。
用户选择RC版通道或Beta版通道后,若发布了正式版,仍旧会升级到正式版。
用户在测试过程中发现的BUG,欢迎提交issue或通过其他渠道反馈。用户的反馈对我们非常重要。 用户在测试过程中发现的BUG,欢迎提交issue或通过其他渠道反馈。用户的反馈对我们非常重要。
## 开发者指南 ## 开发者指南
+43 -98
View File
@@ -21,6 +21,8 @@ files:
- "**/*" - "**/*"
- "!**/{.vscode,.yarn,.yarn-lock,.github,.cursorrules,.prettierrc}" - "!**/{.vscode,.yarn,.yarn-lock,.github,.cursorrules,.prettierrc}"
- "!electron.vite.config.{js,ts,mjs,cjs}}" - "!electron.vite.config.{js,ts,mjs,cjs}}"
- "!.*"
- "!components.json"
- "!**/{.eslintignore,.eslintrc.js,.eslintrc.json,.eslintcache,root.eslint.config.js,eslint.config.js,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,eslint.config.mjs,dev-app-update.yml,CHANGELOG.md,README.md,biome.jsonc}" - "!**/{.eslintignore,.eslintrc.js,.eslintrc.json,.eslintcache,root.eslint.config.js,eslint.config.js,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,eslint.config.mjs,dev-app-update.yml,CHANGELOG.md,README.md,biome.jsonc}"
- "!**/{.env,.env.*,.npmrc,pnpm-lock.yaml}" - "!**/{.env,.env.*,.npmrc,pnpm-lock.yaml}"
- "!**/{tsconfig.json,tsconfig.tsbuildinfo,tsconfig.node.json,tsconfig.web.json}" - "!**/{tsconfig.json,tsconfig.tsbuildinfo,tsconfig.node.json,tsconfig.web.json}"
@@ -133,116 +135,59 @@ artifactBuildCompleted: scripts/artifact-build-completed.js
releaseInfo: releaseInfo:
releaseNotes: | releaseNotes: |
<!--LANG:en--> <!--LANG:en-->
What's New in v1.7.0-beta.3 What's New in v1.7.0-beta.4
Major Changes:
- UI Framework Upgrade: Improved performance and user experience with new design system
- App Menu i18n: Menu now supports multiple languages and syncs with app language settings
New Features: New Features:
- Enhanced Tool Permission System: Real-time tool approval interface with improved UX - AWS Bedrock API Key: Support Bedrock API key authentication with Extended Thinking (reasoning) capability
- Plugin Management System: Support for Claude Agent plugins (agents, commands, skills) - SophNet Provider: Added support for SophNet LLM provider
- Skill Tool: Add skill execution capabilities for agents - Auto Session Rename: Agent sessions automatically rename based on conversation topics
- Mobile App Data Restore: Support restoring data to mobile applications - TopP Parameter: Added TopP parameter support for more precise model control
- OpenMinerU Preprocessor: Knowledge base now supports open-source MinerU for document processing - Reasoning Effort Control: Quick access to reasoning effort settings in input bar
- HuggingFace Provider: Added HuggingFace as AI provider
- Claude Haiku 4.5: Support for the latest Claude Haiku 4.5 model
- Ling Series Models: Added support for Ling-1T and related models
- Intel OVMS Painting: New painting provider using Intel OpenVINO Model Server
- Automatic Update Checks: Implement periodic update checking with configurable intervals
- HuggingChat Mini App: New mini app for HuggingChat integration
Improvements: Improvements:
- Agent Creation: New agents are now automatically activated upon creation - Topics & Sessions: Enhanced UI with better styling and smoother interactions
- Lazy Loading: Optimize page load performance with route lazy loading - Quick Panel: Improved option visibility and control
- UI Enhancements: Improved agent item styling and layout consistency - Painting Models: Smarter model initialization with better defaults
- Navigation: Better navbar layout for fullscreen mode on macOS - System Shutdown: Better handling of shutdown events to prevent data loss
- Settings Tab: Enhanced context slider consistency - Smaller Package Size: Optimized build configuration for faster downloads
- Backup Manager: Unified footer layout for local and S3 backup managers
- Menu System: Enhanced application menu with improved help section
- Proxy Rules: Comprehensive proxy bypass rule matching
- German Language: Added German language support
- MCP Confirmation: Added confirmation modal when activating protocol-installed MCP servers
- Translation: Enhanced translation script with concurrency and validation
- Electron & Vite: Updated to Electron 38 and Vite 4.0.1
Claude Code Tool Improvements:
- GlobTool: Now counts lines instead of files in output for better clarity
- ReadTool: Automatically removes system reminder tags from output
- TodoWriteTool: Improved rendering behavior
- Environment Variables: Updated model-related environment variable names
Bug Fixes: Bug Fixes:
- Fixed session model not being used when sending messages - Fixed Perplexity provider support and API host formatting
- Fixed tool approval UI and shared workspace plugin inconsistencies - Fixed CherryAI provider support and API host formatting
- Fixed API server readiness notification to renderer - Fixed i18n translations for painting image size options
- Fixed grouped items not respecting saved tag order - Fixed agent session message token usage tracking
- Fixed assistant/agent activation when creating new ones - Fixed prompt stream handling on completion or error
- Fixed Dashscope Anthropic API host and migrated old configs - Fixed message API initialization issues
- Fixed Qwen3 thinking mode control for Ollama
- Fixed disappeared MCP button
- Fixed create assistant causing blank screen
- Fixed up-down button visibility in some cases
- Fixed hooks preventing save on composing enter key
- Fixed Azure GPT-image-1 and OpenRouter Gemini-image
- Fixed Silicon reasoning issues
- Fixed topic branch incomplete copy with two-pass ID mapping
- Fixed deep research model search context restrictions
- Fixed model capability checking logic
- Fixed reranker API error response capture
- Fixed right-click paste file content into inputbar
- Fixed minimax-m2 support in aiCore
<!--LANG:zh-CN--> <!--LANG:zh-CN-->
v1.7.0-beta.3 新特性 v1.7.0-beta.4 新特性
重大变更:
- UI 框架升级:采用新设计系统,提升性能和用户体验
- 应用菜单国际化:菜单支持多语言,并自动同步应用语言设置
新功能: 新功能:
- 增强工具权限系统:实时工具审批界面,改进用户体验 - AWS Bedrock API 密钥:支持 Bedrock API 密钥身份验证,并支持扩展思考(推理)能力
- 插件管理系统:支持 Claude Agent 插件(agents、commands、skills - SophNet 提供商:添加 SophNet LLM 提供商支持
- 技能工具:为 Agent 添加技能执行能力 - 自动会话重命名:Agent 会话根据对话主题自动重命名
- 移动应用数据恢复:支持将数据恢复到移动应用程序 - TopP 参数:添加 TopP 参数支持,更精确控制模型输出
- OpenMinerU 预处理器:知识库现支持使用开源 MinerU 处理文档
- HuggingFace 提供商:添加 HuggingFace 作为 AI 提供商
- Claude Haiku 4.5:支持最新的 Claude Haiku 4.5 模型
- Ling 系列模型:添加 Ling-1T 及相关模型支持
- Intel OVMS 绘图:使用 Intel OpenVINO 模型服务器的新绘图提供商
- 自动更新检查:实现可配置间隔的定期更新检查
- HuggingChat 小程序:新增 HuggingChat 集成小程序
改进: 改进:
- Agent 创建:新创建的 Agent 现在会自动激活 - 主题和会话:增强 UI,改进样式和交互体验
- 懒加载:通过路由懒加载优化页面加载性能 - 快速面板:改进选项可见性和控制
- UI 增强:改进 Agent 项目样式和布局一致性 - 绘图模型:更智能的模型初始化和更好的默认值
- 导航:改进 macOS 全屏模式下的导航栏布局 - 系统关机:更好地处理关机事件,防止数据丢失
- 设置选项卡:增强上下文滑块一致性 - 更小的安装包:优化构建配置,下载更快
- 备份管理器:统一本地和 S3 备份管理器的页脚布局
- 菜单系统:增强应用菜单,改进帮助部分
- 代理规则:全面的代理绕过规则匹配
- 德语支持:添加德语语言支持
- MCP 确认:添加激活协议安装的 MCP 服务器时的确认模态框
- 翻译:增强翻译脚本的并发和验证功能
- Electron & Vite:更新至 Electron 38 和 Vite 4.0.1
Claude Code 工具改进:
- GlobTool:现在计算行数而不是文件数,提供更清晰的输出
- ReadTool:自动从输出中移除系统提醒标签
- TodoWriteTool:改进渲染行为
- 环境变量:更新模型相关的环境变量名称
问题修复: 问题修复:
- 修复发送消息时未使用会话模型 - 修复 Perplexity 提供商支持和 API 主机格式化
- 修复工具审批 UI 和共享工作区插件不一致 - 修复 CherryAI 提供商支持和 API 主机格式化
- 修复 API 服务器就绪通知到渲染器 - 修复绘图图像大小选项的 i18n 翻译
- 修复分组项目不遵守已保存标签顺序 - 修复 Agent 会话消息的 token 使用量跟踪
- 修复创建新的助手/Agent 时的激活问题 - 修复完成或错误时的提示流处理
- 修复 Dashscope Anthropic API 主机并迁移旧配置 - 修复消息 API 初始化问题
- 修复 Ollama 的 Qwen3 思考模式控制
- 修复 MCP 按钮消失
- 修复创建助手导致空白屏幕
- 修复某些情况下上下按钮可见性
- 修复钩子在输入法输入时阻止保存
- 修复 Azure GPT-image-1 和 OpenRouter Gemini-image
- 修复 Silicon 推理问题
- 修复主题分支不完整复制,采用两阶段 ID 映射
- 修复深度研究模型搜索上下文限制
- 修复模型能力检查逻辑
- 修复 reranker API 错误响应捕获
- 修复右键粘贴文件内容到输入栏
- 修复 aiCore 中的 minimax-m2 支持
<!--LANG:END--> <!--LANG:END-->
+6 -4
View File
@@ -82,6 +82,7 @@
"@libsql/client": "0.14.0", "@libsql/client": "0.14.0",
"@libsql/win32-x64-msvc": "^0.4.7", "@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", "@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",
"@paymoapp/electron-shutdown-handler": "^1.1.2",
"@strongtz/win32-arm64-msvc": "^0.4.7", "@strongtz/win32-arm64-msvc": "^0.4.7",
"express": "^5.1.0", "express": "^5.1.0",
"font-list": "^2.0.0", "font-list": "^2.0.0",
@@ -113,9 +114,9 @@
"@ant-design/v5-patch-for-react-19": "^1.0.3", "@ant-design/v5-patch-for-react-19": "^1.0.3",
"@anthropic-ai/sdk": "^0.41.0", "@anthropic-ai/sdk": "^0.41.0",
"@anthropic-ai/vertex-sdk": "patch:@anthropic-ai/vertex-sdk@npm%3A0.11.4#~/.yarn/patches/@anthropic-ai-vertex-sdk-npm-0.11.4-c19cb41edb.patch", "@anthropic-ai/vertex-sdk": "patch:@anthropic-ai/vertex-sdk@npm%3A0.11.4#~/.yarn/patches/@anthropic-ai-vertex-sdk-npm-0.11.4-c19cb41edb.patch",
"@aws-sdk/client-bedrock": "^3.840.0", "@aws-sdk/client-bedrock": "^3.910.0",
"@aws-sdk/client-bedrock-runtime": "^3.840.0", "@aws-sdk/client-bedrock-runtime": "^3.910.0",
"@aws-sdk/client-s3": "^3.840.0", "@aws-sdk/client-s3": "^3.910.0",
"@biomejs/biome": "2.2.4", "@biomejs/biome": "2.2.4",
"@cherrystudio/ai-core": "workspace:^1.0.0-alpha.18", "@cherrystudio/ai-core": "workspace:^1.0.0-alpha.18",
"@cherrystudio/embedjs": "^0.1.31", "@cherrystudio/embedjs": "^0.1.31",
@@ -146,7 +147,6 @@
"@eslint/js": "^9.22.0", "@eslint/js": "^9.22.0",
"@google/genai": "patch:@google/genai@npm%3A1.0.1#~/.yarn/patches/@google-genai-npm-1.0.1-e26f0f9af7.patch", "@google/genai": "patch:@google/genai@npm%3A1.0.1#~/.yarn/patches/@google-genai-npm-1.0.1-e26f0f9af7.patch",
"@hello-pangea/dnd": "^18.0.1", "@hello-pangea/dnd": "^18.0.1",
"@heroui/react": "^2.8.3",
"@kangfenmao/keyv-storage": "^0.1.0", "@kangfenmao/keyv-storage": "^0.1.0",
"@langchain/community": "^1.0.0", "@langchain/community": "^1.0.0",
"@langchain/core": "patch:@langchain/core@npm%3A1.0.2#~/.yarn/patches/@langchain-core-npm-1.0.2-183ef83fe4.patch", "@langchain/core": "patch:@langchain/core@npm%3A1.0.2#~/.yarn/patches/@langchain-core-npm-1.0.2-183ef83fe4.patch",
@@ -348,6 +348,7 @@
"striptags": "^3.2.0", "striptags": "^3.2.0",
"styled-components": "^6.1.11", "styled-components": "^6.1.11",
"swr": "^2.3.6", "swr": "^2.3.6",
"tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.13", "tailwindcss": "^4.1.13",
"tar": "^7.4.3", "tar": "^7.4.3",
"tiny-pinyin": "^1.3.2", "tiny-pinyin": "^1.3.2",
@@ -373,6 +374,7 @@
"zod": "^4.1.5" "zod": "^4.1.5"
}, },
"resolutions": { "resolutions": {
"@smithy/types": "4.7.1",
"@codemirror/language": "6.11.3", "@codemirror/language": "6.11.3",
"@codemirror/lint": "6.8.5", "@codemirror/lint": "6.8.5",
"@codemirror/view": "6.38.1", "@codemirror/view": "6.38.1",
+3
View File
@@ -470,3 +470,6 @@ export const MACOS_TERMINALS_WITH_COMMANDS: TerminalConfigWithCommand[] = [
}) })
} }
] ]
// resources/scripts should be maintained manually
export const HOME_CHERRY_DIR = '.cherrystudio'
+13
View File
@@ -31,3 +31,16 @@ export type WebviewKeyEvent = {
shift: boolean shift: boolean
alt: boolean alt: boolean
} }
export interface WebSocketStatusResponse {
isRunning: boolean
port?: number
ip?: string
clientConnected: boolean
}
export interface WebSocketCandidatesResponse {
host: string
interface: string
priority: number
}
+4 -2
View File
@@ -18,8 +18,10 @@ import { sortedObjectByKeys } from './sort'
// ========== SCRIPT CONFIGURATION AREA - MODIFY SETTINGS HERE ========== // ========== SCRIPT CONFIGURATION AREA - MODIFY SETTINGS HERE ==========
const SCRIPT_CONFIG = { const SCRIPT_CONFIG = {
// 🔧 Concurrency Control Configuration // 🔧 Concurrency Control Configuration
MAX_CONCURRENT_TRANSLATIONS: 5, // Max concurrent requests (Make sure the concurrency level does not exceed your provider's limits.) MAX_CONCURRENT_TRANSLATIONS: process.env.TRANSLATION_MAX_CONCURRENT_REQUESTS
TRANSLATION_DELAY_MS: 100, // Delay between requests to avoid rate limiting (Recommended: 100-500ms, Range: 0-5000ms) ? parseInt(process.env.TRANSLATION_MAX_CONCURRENT_REQUESTS)
: 5, // Max concurrent requests (Make sure the concurrency level does not exceed your provider's limits.)
TRANSLATION_DELAY_MS: process.env.TRANSLATION_DELAY_MS ? parseInt(process.env.TRANSLATION_DELAY_MS) : 500, // Delay between requests to avoid rate limiting (Recommended: 100-500ms, Range: 0-5000ms)
// 🔑 API Configuration // 🔑 API Configuration
API_KEY: process.env.TRANSLATION_API_KEY || '', // API key from environment variable API_KEY: process.env.TRANSLATION_API_KEY || '', // API key from environment variable
+1 -1
View File
@@ -171,7 +171,7 @@ const swaggerOptions: swaggerJSDoc.Options = {
} }
] ]
}, },
apis: ['./src/main/apiServer/routes/*.ts', './src/main/apiServer/app.ts'] apis: ['./src/main/apiServer/routes/**/*.ts', './src/main/apiServer/app.ts']
} }
export function setupOpenAPIDocumentation(app: Express) { export function setupOpenAPIDocumentation(app: Express) {
+7
View File
@@ -21,6 +21,7 @@ import { appMenuService } from './services/AppMenuService'
import { configManager } from './services/ConfigManager' import { configManager } from './services/ConfigManager'
import mcpService from './services/MCPService' import mcpService from './services/MCPService'
import { nodeTraceService } from './services/NodeTraceService' import { nodeTraceService } from './services/NodeTraceService'
import powerMonitorService from './services/PowerMonitorService'
import { import {
CHERRY_STUDIO_PROTOCOL, CHERRY_STUDIO_PROTOCOL,
handleProtocolUrl, handleProtocolUrl,
@@ -30,6 +31,7 @@ import {
import selectionService, { initSelectionService } from './services/SelectionService' import selectionService, { initSelectionService } from './services/SelectionService'
import { registerShortcuts } from './services/ShortcutService' import { registerShortcuts } from './services/ShortcutService'
import { TrayService } from './services/TrayService' import { TrayService } from './services/TrayService'
import { versionService } from './services/VersionService'
import { windowService } from './services/WindowService' import { windowService } from './services/WindowService'
import { initWebviewHotkeys } from './services/WebviewService' import { initWebviewHotkeys } from './services/WebviewService'
@@ -110,6 +112,10 @@ if (!app.requestSingleInstanceLock()) {
// Some APIs can only be used after this event occurs. // Some APIs can only be used after this event occurs.
app.whenReady().then(async () => { app.whenReady().then(async () => {
// Record current version for tracking
// A preparation for v2 data refactoring
versionService.recordCurrentVersion()
initWebviewHotkeys() initWebviewHotkeys()
// Set app user model id for windows // Set app user model id for windows
electronApp.setAppUserModelId(import.meta.env.VITE_MAIN_BUNDLE_ID || 'com.kangfenmao.CherryStudio') electronApp.setAppUserModelId(import.meta.env.VITE_MAIN_BUNDLE_ID || 'com.kangfenmao.CherryStudio')
@@ -127,6 +133,7 @@ if (!app.requestSingleInstanceLock()) {
appMenuService?.setupApplicationMenu() appMenuService?.setupApplicationMenu()
nodeTraceService.init() nodeTraceService.init()
powerMonitorService.init()
app.on('activate', function () { app.on('activate', function () {
const mainWindow = windowService.getMainWindow() const mainWindow = windowService.getMainWindow()
+12 -2
View File
@@ -50,6 +50,7 @@ import * as NutstoreService from './services/NutstoreService'
import ObsidianVaultService from './services/ObsidianVaultService' import ObsidianVaultService from './services/ObsidianVaultService'
import { ocrService } from './services/ocr/OcrService' import { ocrService } from './services/ocr/OcrService'
import OvmsManager from './services/OvmsManager' import OvmsManager from './services/OvmsManager'
import powerMonitorService from './services/PowerMonitorService'
import { proxyManager } from './services/ProxyManager' import { proxyManager } from './services/ProxyManager'
import { pythonService } from './services/PythonService' import { pythonService } from './services/PythonService'
import { FileServiceManager } from './services/remotefile/FileServiceManager' import { FileServiceManager } from './services/remotefile/FileServiceManager'
@@ -115,8 +116,17 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
const appUpdater = new AppUpdater() const appUpdater = new AppUpdater()
const notificationService = new NotificationService() const notificationService = new NotificationService()
// Initialize Python service with main window // Register shutdown handlers
pythonService.setMainWindow(mainWindow) powerMonitorService.registerShutdownHandler(() => {
appUpdater.setAutoUpdate(false)
})
powerMonitorService.registerShutdownHandler(() => {
const mw = windowService.getMainWindow()
if (mw && !mw.isDestroyed()) {
mw.webContents.send(IpcChannel.App_SaveData)
}
})
const checkMainWindow = () => { const checkMainWindow = () => {
if (!mainWindow || mainWindow.isDestroyed()) { if (!mainWindow || mainWindow.isDestroyed()) {
+61 -16
View File
@@ -7,16 +7,33 @@ import { app, Menu, shell } from 'electron'
import { configManager } from './ConfigManager' import { configManager } from './ConfigManager'
export class AppMenuService { export class AppMenuService {
private languageChangeCallback?: (newLanguage: string) => void
constructor() {
// Subscribe to language change events
this.languageChangeCallback = () => {
this.setupApplicationMenu()
}
configManager.subscribe('language', this.languageChangeCallback)
}
public destroy(): void {
// Clean up subscription to prevent memory leaks
if (this.languageChangeCallback) {
configManager.unsubscribe('language', this.languageChangeCallback)
}
}
public setupApplicationMenu(): void { public setupApplicationMenu(): void {
const locale = locales[configManager.getLanguage()] const locale = locales[configManager.getLanguage()]
const { common } = locale.translation const { appMenu } = locale.translation
const template: MenuItemConstructorOptions[] = [ const template: MenuItemConstructorOptions[] = [
{ {
label: app.name, label: app.name,
submenu: [ submenu: [
{ {
label: common.about + ' ' + app.name, label: appMenu.about + ' ' + app.name,
click: () => { click: () => {
// Emit event to navigate to About page // Emit event to navigate to About page
const mainWindow = windowService.getMainWindow() const mainWindow = windowService.getMainWindow()
@@ -27,50 +44,78 @@ export class AppMenuService {
} }
}, },
{ type: 'separator' }, { type: 'separator' },
{ role: 'services' }, { role: 'services', label: appMenu.services },
{ type: 'separator' }, { type: 'separator' },
{ role: 'hide' }, { role: 'hide', label: `${appMenu.hide} ${app.name}` },
{ role: 'hideOthers' }, { role: 'hideOthers', label: appMenu.hideOthers },
{ role: 'unhide' }, { role: 'unhide', label: appMenu.unhide },
{ type: 'separator' }, { type: 'separator' },
{ role: 'quit' } { role: 'quit', label: `${appMenu.quit} ${app.name}` }
] ]
}, },
{ {
role: 'fileMenu' label: appMenu.file,
submenu: [{ role: 'close', label: appMenu.close }]
}, },
{ {
role: 'editMenu' label: appMenu.edit,
submenu: [
{ role: 'undo', label: appMenu.undo },
{ role: 'redo', label: appMenu.redo },
{ type: 'separator' },
{ role: 'cut', label: appMenu.cut },
{ role: 'copy', label: appMenu.copy },
{ role: 'paste', label: appMenu.paste },
{ role: 'delete', label: appMenu.delete },
{ role: 'selectAll', label: appMenu.selectAll }
]
}, },
{ {
role: 'viewMenu' label: appMenu.view,
submenu: [
{ role: 'reload', label: appMenu.reload },
{ role: 'forceReload', label: appMenu.forceReload },
{ role: 'toggleDevTools', label: appMenu.toggleDevTools },
{ type: 'separator' },
{ role: 'resetZoom', label: appMenu.resetZoom },
{ role: 'zoomIn', label: appMenu.zoomIn },
{ role: 'zoomOut', label: appMenu.zoomOut },
{ type: 'separator' },
{ role: 'togglefullscreen', label: appMenu.toggleFullscreen }
]
}, },
{ {
role: 'windowMenu' label: appMenu.window,
submenu: [
{ role: 'minimize', label: appMenu.minimize },
{ role: 'zoom', label: appMenu.zoom },
{ type: 'separator' },
{ role: 'front', label: appMenu.front }
]
}, },
{ {
role: 'help', label: appMenu.help,
submenu: [ submenu: [
{ {
label: 'Website', label: appMenu.website,
click: () => { click: () => {
shell.openExternal('https://cherry-ai.com') shell.openExternal('https://cherry-ai.com')
} }
}, },
{ {
label: 'Documentation', label: appMenu.documentation,
click: () => { click: () => {
shell.openExternal('https://cherry-ai.com/docs') shell.openExternal('https://cherry-ai.com/docs')
} }
}, },
{ {
label: 'Feedback', label: appMenu.feedback,
click: () => { click: () => {
shell.openExternal('https://github.com/CherryHQ/cherry-studio/issues/new/choose') shell.openExternal('https://github.com/CherryHQ/cherry-studio/issues/new/choose')
} }
}, },
{ {
label: 'Releases', label: appMenu.releases,
click: () => { click: () => {
shell.openExternal('https://github.com/CherryHQ/cherry-studio/releases') shell.openExternal('https://github.com/CherryHQ/cherry-studio/releases')
} }
+7 -6
View File
@@ -10,6 +10,7 @@ import { getBinaryName } from '@main/utils/process'
import type { TerminalConfig, TerminalConfigWithCommand } from '@shared/config/constant' import type { TerminalConfig, TerminalConfigWithCommand } from '@shared/config/constant'
import { import {
codeTools, codeTools,
HOME_CHERRY_DIR,
MACOS_TERMINALS, MACOS_TERMINALS,
MACOS_TERMINALS_WITH_COMMANDS, MACOS_TERMINALS_WITH_COMMANDS,
terminalApps, terminalApps,
@@ -66,7 +67,7 @@ class CodeToolsService {
} }
public async getBunPath() { public async getBunPath() {
const dir = path.join(os.homedir(), '.cherrystudio', 'bin') const dir = path.join(os.homedir(), HOME_CHERRY_DIR, 'bin')
const bunName = await getBinaryName('bun') const bunName = await getBinaryName('bun')
const bunPath = path.join(dir, bunName) const bunPath = path.join(dir, bunName)
return bunPath return bunPath
@@ -362,7 +363,7 @@ class CodeToolsService {
private async isPackageInstalled(cliTool: string): Promise<boolean> { private async isPackageInstalled(cliTool: string): Promise<boolean> {
const executableName = await this.getCliExecutableName(cliTool) const executableName = await this.getCliExecutableName(cliTool)
const binDir = path.join(os.homedir(), '.cherrystudio', 'bin') const binDir = path.join(os.homedir(), HOME_CHERRY_DIR, 'bin')
const executablePath = path.join(binDir, executableName + (isWin ? '.exe' : '')) const executablePath = path.join(binDir, executableName + (isWin ? '.exe' : ''))
// Ensure bin directory exists // Ensure bin directory exists
@@ -389,7 +390,7 @@ class CodeToolsService {
logger.info(`${cliTool} is installed, getting current version`) logger.info(`${cliTool} is installed, getting current version`)
try { try {
const executableName = await this.getCliExecutableName(cliTool) const executableName = await this.getCliExecutableName(cliTool)
const binDir = path.join(os.homedir(), '.cherrystudio', 'bin') const binDir = path.join(os.homedir(), HOME_CHERRY_DIR, 'bin')
const executablePath = path.join(binDir, executableName + (isWin ? '.exe' : '')) const executablePath = path.join(binDir, executableName + (isWin ? '.exe' : ''))
const { stdout } = await execAsync(`"${executablePath}" --version`, { const { stdout } = await execAsync(`"${executablePath}" --version`, {
@@ -500,7 +501,7 @@ class CodeToolsService {
try { try {
const packageName = await this.getPackageName(cliTool) const packageName = await this.getPackageName(cliTool)
const bunPath = await this.getBunPath() const bunPath = await this.getBunPath()
const bunInstallPath = path.join(os.homedir(), '.cherrystudio') const bunInstallPath = path.join(os.homedir(), HOME_CHERRY_DIR)
const registryUrl = await this.getNpmRegistryUrl() const registryUrl = await this.getNpmRegistryUrl()
const installEnvPrefix = isWin const installEnvPrefix = isWin
@@ -550,7 +551,7 @@ class CodeToolsService {
const packageName = await this.getPackageName(cliTool) const packageName = await this.getPackageName(cliTool)
const bunPath = await this.getBunPath() const bunPath = await this.getBunPath()
const executableName = await this.getCliExecutableName(cliTool) const executableName = await this.getCliExecutableName(cliTool)
const binDir = path.join(os.homedir(), '.cherrystudio', 'bin') const binDir = path.join(os.homedir(), HOME_CHERRY_DIR, 'bin')
const executablePath = path.join(binDir, executableName + (isWin ? '.exe' : '')) const executablePath = path.join(binDir, executableName + (isWin ? '.exe' : ''))
logger.debug(`Package name: ${packageName}`) logger.debug(`Package name: ${packageName}`)
@@ -652,7 +653,7 @@ class CodeToolsService {
baseCommand = `${baseCommand} ${configParams}` baseCommand = `${baseCommand} ${configParams}`
} }
const bunInstallPath = path.join(os.homedir(), '.cherrystudio') const bunInstallPath = path.join(os.homedir(), HOME_CHERRY_DIR)
if (isInstalled) { if (isInstalled) {
// If already installed, run executable directly (with optional update message) // If already installed, run executable directly (with optional update message)
+2 -1
View File
@@ -30,6 +30,7 @@ import {
ToolListChangedNotificationSchema ToolListChangedNotificationSchema
} from '@modelcontextprotocol/sdk/types.js' } from '@modelcontextprotocol/sdk/types.js'
import { nanoid } from '@reduxjs/toolkit' import { nanoid } from '@reduxjs/toolkit'
import { HOME_CHERRY_DIR } from '@shared/config/constant'
import type { MCPProgressEvent } from '@shared/config/types' import type { MCPProgressEvent } from '@shared/config/types'
import { IpcChannel } from '@shared/IpcChannel' import { IpcChannel } from '@shared/IpcChannel'
import { defaultAppHeaders } from '@shared/utils' import { defaultAppHeaders } from '@shared/utils'
@@ -715,7 +716,7 @@ class McpService {
} }
public async getInstallInfo() { public async getInstallInfo() {
const dir = path.join(os.homedir(), '.cherrystudio', 'bin') const dir = path.join(os.homedir(), HOME_CHERRY_DIR, 'bin')
const uvName = await getBinaryName('uv') const uvName = await getBinaryName('uv')
const bunName = await getBinaryName('bun') const bunName = await getBinaryName('bun')
const uvPath = path.join(dir, uvName) const uvPath = path.join(dir, uvName)
+9 -8
View File
@@ -3,6 +3,7 @@ import { homedir } from 'node:os'
import { promisify } from 'node:util' import { promisify } from 'node:util'
import { loggerService } from '@logger' import { loggerService } from '@logger'
import { HOME_CHERRY_DIR } from '@shared/config/constant'
import * as fs from 'fs-extra' import * as fs from 'fs-extra'
import * as path from 'path' import * as path from 'path'
@@ -145,7 +146,7 @@ class OvmsManager {
*/ */
public async runOvms(): Promise<{ success: boolean; message?: string }> { public async runOvms(): Promise<{ success: boolean; message?: string }> {
const homeDir = homedir() const homeDir = homedir()
const ovmsDir = path.join(homeDir, '.cherrystudio', 'ovms', 'ovms') const ovmsDir = path.join(homeDir, HOME_CHERRY_DIR, 'ovms', 'ovms')
const configPath = path.join(ovmsDir, 'models', 'config.json') const configPath = path.join(ovmsDir, 'models', 'config.json')
const runBatPath = path.join(ovmsDir, 'run.bat') const runBatPath = path.join(ovmsDir, 'run.bat')
@@ -195,7 +196,7 @@ class OvmsManager {
*/ */
public async getOvmsStatus(): Promise<'not-installed' | 'not-running' | 'running'> { public async getOvmsStatus(): Promise<'not-installed' | 'not-running' | 'running'> {
const homeDir = homedir() const homeDir = homedir()
const ovmsPath = path.join(homeDir, '.cherrystudio', 'ovms', 'ovms', 'ovms.exe') const ovmsPath = path.join(homeDir, HOME_CHERRY_DIR, 'ovms', 'ovms', 'ovms.exe')
try { try {
// Check if OVMS executable exists // Check if OVMS executable exists
@@ -273,7 +274,7 @@ class OvmsManager {
} }
const homeDir = homedir() const homeDir = homedir()
const configPath = path.join(homeDir, '.cherrystudio', 'ovms', 'ovms', 'models', 'config.json') const configPath = path.join(homeDir, HOME_CHERRY_DIR, 'ovms', 'ovms', 'models', 'config.json')
try { try {
if (!(await fs.pathExists(configPath))) { if (!(await fs.pathExists(configPath))) {
logger.warn(`Config file does not exist: ${configPath}`) logger.warn(`Config file does not exist: ${configPath}`)
@@ -304,7 +305,7 @@ class OvmsManager {
private async applyModelPath(modelDirPath: string): Promise<boolean> { private async applyModelPath(modelDirPath: string): Promise<boolean> {
const homeDir = homedir() const homeDir = homedir()
const patchDir = path.join(homeDir, '.cherrystudio', 'ovms', 'patch') const patchDir = path.join(homeDir, HOME_CHERRY_DIR, 'ovms', 'patch')
if (!(await fs.pathExists(patchDir))) { if (!(await fs.pathExists(patchDir))) {
return true return true
} }
@@ -355,7 +356,7 @@ class OvmsManager {
logger.info(`Adding model: ${modelName} with ID: ${modelId}, Source: ${modelSource}, Task: ${task}`) logger.info(`Adding model: ${modelName} with ID: ${modelId}, Source: ${modelSource}, Task: ${task}`)
const homeDir = homedir() const homeDir = homedir()
const ovdndDir = path.join(homeDir, '.cherrystudio', 'ovms', 'ovms') const ovdndDir = path.join(homeDir, HOME_CHERRY_DIR, 'ovms', 'ovms')
const pathModel = path.join(ovdndDir, 'models', modelId) const pathModel = path.join(ovdndDir, 'models', modelId)
try { try {
@@ -468,7 +469,7 @@ class OvmsManager {
*/ */
public async checkModelExists(modelId: string): Promise<boolean> { public async checkModelExists(modelId: string): Promise<boolean> {
const homeDir = homedir() const homeDir = homedir()
const ovmsDir = path.join(homeDir, '.cherrystudio', 'ovms', 'ovms') const ovmsDir = path.join(homeDir, HOME_CHERRY_DIR, 'ovms', 'ovms')
const configPath = path.join(ovmsDir, 'models', 'config.json') const configPath = path.join(ovmsDir, 'models', 'config.json')
try { try {
@@ -495,7 +496,7 @@ class OvmsManager {
*/ */
public async updateModelConfig(modelName: string, modelId: string): Promise<boolean> { public async updateModelConfig(modelName: string, modelId: string): Promise<boolean> {
const homeDir = homedir() const homeDir = homedir()
const ovmsDir = path.join(homeDir, '.cherrystudio', 'ovms', 'ovms') const ovmsDir = path.join(homeDir, HOME_CHERRY_DIR, 'ovms', 'ovms')
const configPath = path.join(ovmsDir, 'models', 'config.json') const configPath = path.join(ovmsDir, 'models', 'config.json')
try { try {
@@ -548,7 +549,7 @@ class OvmsManager {
*/ */
public async getModels(): Promise<ModelConfig[]> { public async getModels(): Promise<ModelConfig[]> {
const homeDir = homedir() const homeDir = homedir()
const ovmsDir = path.join(homeDir, '.cherrystudio', 'ovms', 'ovms') const ovmsDir = path.join(homeDir, HOME_CHERRY_DIR, 'ovms', 'ovms')
const configPath = path.join(ovmsDir, 'models', 'config.json') const configPath = path.join(ovmsDir, 'models', 'config.json')
try { try {
+112
View File
@@ -0,0 +1,112 @@
import { loggerService } from '@logger'
import { isLinux, isMac, isWin } from '@main/constant'
import ElectronShutdownHandler from '@paymoapp/electron-shutdown-handler'
import { BrowserWindow } from 'electron'
import { powerMonitor } from 'electron'
const logger = loggerService.withContext('PowerMonitorService')
type ShutdownHandler = () => void | Promise<void>
export class PowerMonitorService {
private static instance: PowerMonitorService
private initialized = false
private shutdownHandlers: ShutdownHandler[] = []
private constructor() {
// Private constructor to prevent direct instantiation
}
public static getInstance(): PowerMonitorService {
if (!PowerMonitorService.instance) {
PowerMonitorService.instance = new PowerMonitorService()
}
return PowerMonitorService.instance
}
/**
* Register a shutdown handler to be called when system shutdown is detected
* @param handler - The handler function to be called on shutdown
*/
public registerShutdownHandler(handler: ShutdownHandler): void {
this.shutdownHandlers.push(handler)
logger.info('Shutdown handler registered', { totalHandlers: this.shutdownHandlers.length })
}
/**
* Initialize power monitor to listen for shutdown events
*/
public init(): void {
if (this.initialized) {
logger.warn('PowerMonitorService already initialized')
return
}
if (isWin) {
this.initWindowsShutdownHandler()
} else if (isMac || isLinux) {
this.initElectronPowerMonitor()
}
this.initialized = true
logger.info('PowerMonitorService initialized', { platform: process.platform })
}
/**
* Execute all registered shutdown handlers
*/
private async executeShutdownHandlers(): Promise<void> {
logger.info('Executing shutdown handlers', { count: this.shutdownHandlers.length })
for (const handler of this.shutdownHandlers) {
try {
await handler()
} catch (error) {
logger.error('Error executing shutdown handler', error as Error)
}
}
}
/**
* Initialize shutdown handler for Windows using @paymoapp/electron-shutdown-handler
*/
private initWindowsShutdownHandler(): void {
try {
const zeroMemoryWindow = new BrowserWindow({ show: false })
// Set the window handle for the shutdown handler
ElectronShutdownHandler.setWindowHandle(zeroMemoryWindow.getNativeWindowHandle())
// Listen for shutdown event
ElectronShutdownHandler.on('shutdown', async () => {
logger.info('System shutdown event detected (Windows)')
// Execute all registered shutdown handlers
await this.executeShutdownHandlers()
// Release the shutdown block to allow the system to shut down
ElectronShutdownHandler.releaseShutdown()
})
logger.info('Windows shutdown handler registered')
} catch (error) {
logger.error('Failed to initialize Windows shutdown handler', error as Error)
}
}
/**
* Initialize power monitor for macOS and Linux using Electron's powerMonitor
*/
private initElectronPowerMonitor(): void {
try {
powerMonitor.on('shutdown', async () => {
logger.info('System shutdown event detected', { platform: process.platform })
// Execute all registered shutdown handlers
await this.executeShutdownHandlers()
})
logger.info('Electron powerMonitor shutdown listener registered')
} catch (error) {
logger.error('Failed to initialize Electron powerMonitor', error as Error)
}
}
}
// Default export as singleton instance
export default PowerMonitorService.getInstance()
+5 -9
View File
@@ -1,8 +1,9 @@
import { randomUUID } from 'node:crypto' import { randomUUID } from 'node:crypto'
import type { BrowserWindow } from 'electron'
import { ipcMain } from 'electron' import { ipcMain } from 'electron'
import { windowService } from './WindowService'
interface PythonExecutionRequest { interface PythonExecutionRequest {
id: string id: string
script: string script: string
@@ -21,7 +22,6 @@ interface PythonExecutionResponse {
*/ */
export class PythonService { export class PythonService {
private static instance: PythonService | null = null private static instance: PythonService | null = null
private mainWindow: BrowserWindow | null = null
private pendingRequests = new Map<string, { resolve: (value: string) => void; reject: (error: Error) => void }>() private pendingRequests = new Map<string, { resolve: (value: string) => void; reject: (error: Error) => void }>()
private constructor() { private constructor() {
@@ -51,10 +51,6 @@ export class PythonService {
}) })
} }
public setMainWindow(mainWindow: BrowserWindow) {
this.mainWindow = mainWindow
}
/** /**
* Execute Python code by sending request to renderer PyodideService * Execute Python code by sending request to renderer PyodideService
*/ */
@@ -63,8 +59,8 @@ export class PythonService {
context: Record<string, any> = {}, context: Record<string, any> = {},
timeout: number = 60000 timeout: number = 60000
): Promise<string> { ): Promise<string> {
if (!this.mainWindow) { if (!windowService.getMainWindow()) {
throw new Error('Main window not set in PythonService') throw new Error('Main window not found')
} }
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@@ -95,7 +91,7 @@ export class PythonService {
// Send request to renderer // Send request to renderer
const request: PythonExecutionRequest = { id: requestId, script, context, timeout } const request: PythonExecutionRequest = { id: requestId, script, context, timeout }
this.mainWindow?.webContents.send('python-execution-request', request) windowService.getMainWindow()?.webContents.send('python-execution-request', request)
}) })
} }
} }
+2 -1
View File
@@ -3,6 +3,7 @@ import type { Attributes, SpanEntity, TokenUsage, TraceCache } from '@mcp-trace/
import { convertSpanToSpanEntity } from '@mcp-trace/trace-core' import { convertSpanToSpanEntity } from '@mcp-trace/trace-core'
import { SpanStatusCode } from '@opentelemetry/api' import { SpanStatusCode } from '@opentelemetry/api'
import type { ReadableSpan } from '@opentelemetry/sdk-trace-base' import type { ReadableSpan } from '@opentelemetry/sdk-trace-base'
import { HOME_CHERRY_DIR } from '@shared/config/constant'
import fs from 'fs/promises' import fs from 'fs/promises'
import * as os from 'os' import * as os from 'os'
import * as path from 'path' import * as path from 'path'
@@ -18,7 +19,7 @@ class SpanCacheService implements TraceCache {
pri pri
constructor() { constructor() {
this.fileDir = path.join(os.homedir(), '.cherrystudio', 'trace') this.fileDir = path.join(os.homedir(), HOME_CHERRY_DIR, 'trace')
} }
createSpan: (span: ReadableSpan) => void = (span: ReadableSpan) => { createSpan: (span: ReadableSpan) => void = (span: ReadableSpan) => {
+285
View File
@@ -0,0 +1,285 @@
import { loggerService } from '@logger'
import { app } from 'electron'
import fs from 'fs'
import path from 'path'
const logger = loggerService.withContext('VersionService')
type OS = 'win' | 'mac' | 'linux' | 'unknown'
type Environment = 'prod' | 'dev'
type Packaged = 'packaged' | 'unpackaged'
type Mode = 'install' | 'portable'
/**
* Version record stored in version.log
*/
interface VersionRecord {
version: string
os: OS
environment: Environment
packaged: Packaged
mode: Mode
timestamp: string
}
/**
* Service for tracking application version history
* Stores version information in userData/version.log for data migration and diagnostics
*/
class VersionService {
private readonly VERSION_LOG_FILE = 'version.log'
private versionLogPath: string | null = null
constructor() {
// Lazy initialization of path since app.getPath may not be available during construction
}
/**
* Gets the full path to version.log file
* @returns {string} Full path to version log file
*/
private getVersionLogPath(): string {
if (!this.versionLogPath) {
this.versionLogPath = path.join(app.getPath('userData'), this.VERSION_LOG_FILE)
}
return this.versionLogPath
}
/**
* Gets current operating system identifier
* @returns {OS} OS identifier
*/
private getCurrentOS(): OS {
switch (process.platform) {
case 'win32':
return 'win'
case 'darwin':
return 'mac'
case 'linux':
return 'linux'
default:
return 'unknown'
}
}
/**
* Gets current environment (production or development)
* @returns {Environment} Environment identifier
*/
private getCurrentEnvironment(): Environment {
return import.meta.env.MODE === 'production' ? 'prod' : 'dev'
}
/**
* Gets packaging status
* @returns {Packaged} Packaging status
*/
private getPackagedStatus(): Packaged {
return app.isPackaged ? 'packaged' : 'unpackaged'
}
/**
* Gets installation mode (install or portable)
* @returns {Mode} Installation mode
*/
private getInstallMode(): Mode {
return process.env.PORTABLE_EXECUTABLE_DIR !== undefined ? 'portable' : 'install'
}
/**
* Generates version log line for current application state
* @returns {string} Pipe-separated version record line
*/
private generateCurrentVersionLine(): string {
const version = app.getVersion()
const os = this.getCurrentOS()
const environment = this.getCurrentEnvironment()
const packaged = this.getPackagedStatus()
const mode = this.getInstallMode()
const timestamp = new Date().toISOString()
return `${version}|${os}|${environment}|${packaged}|${mode}|${timestamp}`
}
/**
* Parses a version log line into a VersionRecord object
* @param {string} line - Pipe-separated version record line
* @returns {VersionRecord | null} Parsed version record or null if invalid
*/
private parseVersionLine(line: string): VersionRecord | null {
try {
const parts = line.trim().split('|')
if (parts.length !== 6) {
return null
}
const [version, os, environment, packaged, mode, timestamp] = parts
// Validate data
if (
!version ||
!['win', 'mac', 'linux', 'unknown'].includes(os) ||
!['prod', 'dev'].includes(environment) ||
!['packaged', 'unpackaged'].includes(packaged) ||
!['install', 'portable'].includes(mode) ||
!timestamp
) {
return null
}
return {
version,
os: os as OS,
environment: environment as Environment,
packaged: packaged as Packaged,
mode: mode as Mode,
timestamp
}
} catch (error) {
logger.warn(`Failed to parse version line: ${line}`, error as Error)
return null
}
}
/**
* Reads the last 1KB from version.log and returns all lines
* Uses reverse reading from file end to avoid reading the entire file
* @returns {string[]} Array of version lines from the last 1KB
*/
private readLastVersionLines(): string[] {
const logPath = this.getVersionLogPath()
try {
if (!fs.existsSync(logPath)) {
return []
}
const stats = fs.statSync(logPath)
const fileSize = stats.size
if (fileSize === 0) {
return []
}
// Read from the end of the file, 1KB is enough to find previous version
// Typical line: "1.7.0-beta.3|win|prod|packaged|install|2025-01-15T08:30:00.000Z\n" (~70 bytes)
// 1KB can store ~14 lines, which is more than enough
const bufferSize = Math.min(1024, fileSize)
const buffer = Buffer.alloc(bufferSize)
const fd = fs.openSync(logPath, 'r')
try {
const startPosition = Math.max(0, fileSize - bufferSize)
fs.readSync(fd, buffer, 0, bufferSize, startPosition)
const content = buffer.toString('utf-8')
const lines = content
.trim()
.split('\n')
.filter((line) => line.trim())
return lines
} finally {
fs.closeSync(fd)
}
} catch (error) {
logger.error('Failed to read version log:', error as Error)
return []
}
}
/**
* Appends a version record line to version.log
* @param {string} line - Version record line to append
*/
private appendVersionLine(line: string): void {
const logPath = this.getVersionLogPath()
try {
fs.appendFileSync(logPath, line + '\n', 'utf-8')
logger.debug(`Version recorded: ${line}`)
} catch (error) {
logger.error('Failed to append version log:', error as Error)
}
}
/**
* Records the current version on application startup
* Only adds a new record if the version has changed since the last run
*/
recordCurrentVersion(): void {
try {
const currentLine = this.generateCurrentVersionLine()
const lines = this.readLastVersionLines()
// Add new record if this is the first run or version has changed
if (lines.length === 0) {
logger.info('First run detected, creating version log')
this.appendVersionLine(currentLine)
return
}
const lastLine = lines[lines.length - 1]
const lastRecord = this.parseVersionLine(lastLine)
const currentVersion = app.getVersion()
// Check if any meaningful field has changed (version, os, environment, packaged, mode)
const currentOS = this.getCurrentOS()
const currentEnvironment = this.getCurrentEnvironment()
const currentPackaged = this.getPackagedStatus()
const currentMode = this.getInstallMode()
const hasMeaningfulChange =
!lastRecord ||
lastRecord.version !== currentVersion ||
lastRecord.os !== currentOS ||
lastRecord.environment !== currentEnvironment ||
lastRecord.packaged !== currentPackaged ||
lastRecord.mode !== currentMode
if (hasMeaningfulChange) {
logger.info(`Version information changed, recording new entry`)
this.appendVersionLine(currentLine)
} else {
logger.debug(`Version information not changed, skip recording`)
}
} catch (error) {
logger.error('Failed to record current version:', error as Error)
}
}
/**
* Gets the previous version record (last record with different version than current)
* Reads from the last 1KB of version.log to find the most recent different version
* Useful for detecting version upgrades and running migrations
* @returns {VersionRecord | null} Previous version record or null if not available
*/
getPreviousVersion(): VersionRecord | null {
try {
const lines = this.readLastVersionLines()
if (lines.length === 0) {
return null
}
const currentVersion = app.getVersion()
// Read from the end backwards to find the first different version
for (let i = lines.length - 1; i >= 0; i--) {
const record = this.parseVersionLine(lines[i])
if (record && record.version !== currentVersion) {
return record
}
}
return null
} catch (error) {
logger.error('Failed to get previous version:', error as Error)
return null
}
}
}
/**
* Singleton instance of VersionService
*/
export const versionService = new VersionService()
+3 -13
View File
@@ -1,4 +1,5 @@
import { loggerService } from '@logger' import { loggerService } from '@logger'
import type { WebSocketCandidatesResponse, WebSocketStatusResponse } from '@shared/config/types'
import * as fs from 'fs' import * as fs from 'fs'
import { networkInterfaces } from 'os' import { networkInterfaces } from 'os'
import * as path from 'path' import * as path from 'path'
@@ -202,12 +203,7 @@ class WebSocketService {
} }
} }
public getStatus = async (): Promise<{ public getStatus = async (): Promise<WebSocketStatusResponse> => {
isRunning: boolean
port?: number
ip?: string
clientConnected: boolean
}> => {
return { return {
isRunning: this.isStarted, isRunning: this.isStarted,
port: this.isStarted ? this.port : undefined, port: this.isStarted ? this.port : undefined,
@@ -216,13 +212,7 @@ class WebSocketService {
} }
} }
public getAllCandidates = async (): Promise< public getAllCandidates = async (): Promise<WebSocketCandidatesResponse[]> => {
Array<{
host: string
interface: string
priority: number
}>
> => {
const interfaces = networkInterfaces() const interfaces = networkInterfaces()
// 按优先级排序的网络接口名称模式 // 按优先级排序的网络接口名称模式
@@ -365,6 +365,16 @@ class ClaudeCodeService implements AgentServiceInterface {
type: 'chunk', type: 'chunk',
chunk chunk
}) })
// Close prompt stream when SDK signals completion or error
if (chunk.type === 'finish' || chunk.type === 'error') {
logger.info('Closing prompt stream as SDK signaled completion', {
chunkType: chunk.type,
reason: chunk.type === 'finish' ? 'finished' : 'error_occurred'
})
closePromptStream()
logger.info('Prompt stream closed successfully')
}
} }
} }
@@ -1,5 +1,6 @@
import { loggerService } from '@logger' import { loggerService } from '@logger'
import { isWin } from '@main/constant' import { isWin } from '@main/constant'
import { HOME_CHERRY_DIR } from '@shared/config/constant'
import type { OcrOvConfig, OcrResult, SupportedOcrFile } from '@types' import type { OcrOvConfig, OcrResult, SupportedOcrFile } from '@types'
import { isImageFileMetadata } from '@types' import { isImageFileMetadata } from '@types'
import { exec } from 'child_process' import { exec } from 'child_process'
@@ -13,7 +14,7 @@ import { OcrBaseService } from './OcrBaseService'
const logger = loggerService.withContext('OvOcrService') const logger = loggerService.withContext('OvOcrService')
const execAsync = promisify(exec) const execAsync = promisify(exec)
const PATH_BAT_FILE = path.join(os.homedir(), '.cherrystudio', 'ovms', 'ovocr', 'run.npu.bat') const PATH_BAT_FILE = path.join(os.homedir(), HOME_CHERRY_DIR, 'ovms', 'ovocr', 'run.npu.bat')
export class OvOcrService extends OcrBaseService { export class OvOcrService extends OcrBaseService {
constructor() { constructor() {
@@ -30,7 +31,7 @@ export class OvOcrService extends OcrBaseService {
} }
private getOvOcrPath(): string { private getOvOcrPath(): string {
return path.join(os.homedir(), '.cherrystudio', 'ovms', 'ovocr') return path.join(os.homedir(), HOME_CHERRY_DIR, 'ovms', 'ovocr')
} }
private getImgDir(): string { private getImgDir(): string {
+3 -3
View File
@@ -5,7 +5,7 @@ import os from 'node:os'
import path from 'node:path' import path from 'node:path'
import { loggerService } from '@logger' import { loggerService } from '@logger'
import { audioExts, documentExts, imageExts, MB, textExts, videoExts } from '@shared/config/constant' import { audioExts, documentExts, HOME_CHERRY_DIR, imageExts, MB, textExts, videoExts } from '@shared/config/constant'
import type { FileMetadata, NotesTreeNode } from '@types' import type { FileMetadata, NotesTreeNode } from '@types'
import { FileTypes } from '@types' import { FileTypes } from '@types'
import chardet from 'chardet' import chardet from 'chardet'
@@ -160,7 +160,7 @@ export function getNotesDir() {
} }
export function getConfigDir() { export function getConfigDir() {
return path.join(os.homedir(), '.cherrystudio', 'config') return path.join(os.homedir(), HOME_CHERRY_DIR, 'config')
} }
export function getCacheDir() { export function getCacheDir() {
@@ -172,7 +172,7 @@ export function getAppConfigDir(name: string) {
} }
export function getMcpDir() { export function getMcpDir() {
return path.join(os.homedir(), '.cherrystudio', 'mcp') return path.join(os.homedir(), HOME_CHERRY_DIR, 'mcp')
} }
/** /**
+2 -1
View File
@@ -3,6 +3,7 @@ import os from 'node:os'
import path from 'node:path' import path from 'node:path'
import { isLinux, isPortable, isWin } from '@main/constant' import { isLinux, isPortable, isWin } from '@main/constant'
import { HOME_CHERRY_DIR } from '@shared/config/constant'
import { app } from 'electron' import { app } from 'electron'
// Please don't import any other modules which is not node/electron built-in modules // Please don't import any other modules which is not node/electron built-in modules
@@ -17,7 +18,7 @@ function hasWritePermission(path: string) {
} }
function getConfigDir() { function getConfigDir() {
return path.join(os.homedir(), '.cherrystudio', 'config') return path.join(os.homedir(), HOME_CHERRY_DIR, 'config')
} }
export function initAppDataDir() { export function initAppDataDir() {
+3 -2
View File
@@ -1,4 +1,5 @@
import { loggerService } from '@logger' import { loggerService } from '@logger'
import { HOME_CHERRY_DIR } from '@shared/config/constant'
import { spawn } from 'child_process' import { spawn } from 'child_process'
import fs from 'fs' import fs from 'fs'
import os from 'os' import os from 'os'
@@ -46,11 +47,11 @@ export async function getBinaryName(name: string): Promise<string> {
export async function getBinaryPath(name?: string): Promise<string> { export async function getBinaryPath(name?: string): Promise<string> {
if (!name) { if (!name) {
return path.join(os.homedir(), '.cherrystudio', 'bin') return path.join(os.homedir(), HOME_CHERRY_DIR, 'bin')
} }
const binaryName = await getBinaryName(name) const binaryName = await getBinaryName(name)
const binariesDir = path.join(os.homedir(), '.cherrystudio', 'bin') const binariesDir = path.join(os.homedir(), HOME_CHERRY_DIR, 'bin')
const binariesDirExists = fs.existsSync(binariesDir) const binariesDirExists = fs.existsSync(binariesDir)
return binariesDirExists ? path.join(binariesDir, binaryName) : binaryName return binariesDirExists ? path.join(binariesDir, binaryName) : binaryName
} }
+15 -20
View File
@@ -6,11 +6,9 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { Provider } from 'react-redux' import { Provider } from 'react-redux'
import { PersistGate } from 'redux-persist/integration/react' import { PersistGate } from 'redux-persist/integration/react'
import { ToastPortal } from './components/ToastPortal'
import TopViewContainer from './components/TopView' import TopViewContainer from './components/TopView'
import AntdProvider from './context/AntdProvider' import AntdProvider from './context/AntdProvider'
import { CodeStyleProvider } from './context/CodeStyleProvider' import { CodeStyleProvider } from './context/CodeStyleProvider'
import { HeroUIProvider } from './context/HeroUIProvider'
import { NotificationProvider } from './context/NotificationProvider' import { NotificationProvider } from './context/NotificationProvider'
import StyleSheetManager from './context/StyleSheetManager' import StyleSheetManager from './context/StyleSheetManager'
import { ThemeProvider } from './context/ThemeProvider' import { ThemeProvider } from './context/ThemeProvider'
@@ -34,24 +32,21 @@ function App(): React.ReactElement {
return ( return (
<Provider store={store}> <Provider store={store}>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<HeroUIProvider> <StyleSheetManager>
<StyleSheetManager> <ThemeProvider>
<ThemeProvider> <AntdProvider>
<AntdProvider> <NotificationProvider>
<NotificationProvider> <CodeStyleProvider>
<CodeStyleProvider> <PersistGate loading={null} persistor={persistor}>
<PersistGate loading={null} persistor={persistor}> <TopViewContainer>
<TopViewContainer> <Router />
<Router /> </TopViewContainer>
</TopViewContainer> </PersistGate>
</PersistGate> </CodeStyleProvider>
</CodeStyleProvider> </NotificationProvider>
</NotificationProvider> </AntdProvider>
</AntdProvider> </ThemeProvider>
</ThemeProvider> </StyleSheetManager>
</StyleSheetManager>
<ToastPortal />
</HeroUIProvider>
</QueryClientProvider> </QueryClientProvider>
</Provider> </Provider>
) )
@@ -1,6 +1,7 @@
import { BedrockClient, ListFoundationModelsCommand, ListInferenceProfilesCommand } from '@aws-sdk/client-bedrock' import { BedrockClient, ListFoundationModelsCommand, ListInferenceProfilesCommand } from '@aws-sdk/client-bedrock'
import { import {
BedrockRuntimeClient, BedrockRuntimeClient,
type BedrockRuntimeClientConfig,
ConverseCommand, ConverseCommand,
InvokeModelCommand, InvokeModelCommand,
InvokeModelWithResponseStreamCommand InvokeModelWithResponseStreamCommand
@@ -11,6 +12,8 @@ import { DEFAULT_MAX_TOKENS } from '@renderer/config/constant'
import { findTokenLimit, isReasoningModel } from '@renderer/config/models' import { findTokenLimit, isReasoningModel } from '@renderer/config/models'
import { import {
getAwsBedrockAccessKeyId, getAwsBedrockAccessKeyId,
getAwsBedrockApiKey,
getAwsBedrockAuthType,
getAwsBedrockRegion, getAwsBedrockRegion,
getAwsBedrockSecretAccessKey getAwsBedrockSecretAccessKey
} from '@renderer/hooks/useAwsBedrock' } from '@renderer/hooks/useAwsBedrock'
@@ -75,32 +78,48 @@ export class AwsBedrockAPIClient extends BaseApiClient<
} }
const region = getAwsBedrockRegion() const region = getAwsBedrockRegion()
const accessKeyId = getAwsBedrockAccessKeyId() const authType = getAwsBedrockAuthType()
const secretAccessKey = getAwsBedrockSecretAccessKey()
if (!region) { if (!region) {
throw new Error('AWS region is required. Please configure AWS-Region in extra headers.') throw new Error('AWS region is required. Please configure AWS region in settings.')
} }
if (!accessKeyId || !secretAccessKey) { // Build client configuration based on auth type
throw new Error('AWS credentials are required. Please configure AWS-Access-Key-ID and AWS-Secret-Access-Key.') let clientConfig: BedrockRuntimeClientConfig
if (authType === 'iam') {
// IAM credentials authentication
const accessKeyId = getAwsBedrockAccessKeyId()
const secretAccessKey = getAwsBedrockSecretAccessKey()
if (!accessKeyId || !secretAccessKey) {
throw new Error('AWS credentials are required. Please configure Access Key ID and Secret Access Key.')
}
clientConfig = {
region,
credentials: {
accessKeyId,
secretAccessKey
}
}
} else {
// API Key authentication
const awsBedrockApiKey = getAwsBedrockApiKey()
if (!awsBedrockApiKey) {
throw new Error('AWS Bedrock API Key is required. Please configure API Key in settings.')
}
clientConfig = {
region,
token: { token: awsBedrockApiKey },
authSchemePreference: ['httpBearerAuth']
}
} }
const client = new BedrockRuntimeClient({ const client = new BedrockRuntimeClient(clientConfig)
region, const bedrockClient = new BedrockClient(clientConfig)
credentials: {
accessKeyId,
secretAccessKey
}
})
const bedrockClient = new BedrockClient({
region,
credentials: {
accessKeyId,
secretAccessKey
}
})
this.sdkInstance = { client, bedrockClient, region } this.sdkInstance = { client, bedrockClient, region }
return this.sdkInstance return this.sdkInstance
@@ -192,7 +192,7 @@ export class OpenAIAPIClient extends OpenAIBaseClient<
extra_body: { extra_body: {
google: { google: {
thinking_config: { thinking_config: {
thinkingBudget: 0 thinking_budget: 0
} }
} }
} }
@@ -327,8 +327,8 @@ export class OpenAIAPIClient extends OpenAIBaseClient<
extra_body: { extra_body: {
google: { google: {
thinking_config: { thinking_config: {
thinkingBudget: -1, thinking_budget: -1,
includeThoughts: true include_thoughts: true
} }
} }
} }
@@ -338,8 +338,8 @@ export class OpenAIAPIClient extends OpenAIBaseClient<
extra_body: { extra_body: {
google: { google: {
thinking_config: { thinking_config: {
thinkingBudget: budgetTokens, thinking_budget: budgetTokens,
includeThoughts: true include_thoughts: true
} }
} }
} }
@@ -670,7 +670,7 @@ export class OpenAIAPIClient extends OpenAIBaseClient<
} else if (isClaudeReasoningModel(model) && reasoningEffort.thinking?.budget_tokens) { } else if (isClaudeReasoningModel(model) && reasoningEffort.thinking?.budget_tokens) {
suffix = ` --thinking_budget ${reasoningEffort.thinking.budget_tokens}` suffix = ` --thinking_budget ${reasoningEffort.thinking.budget_tokens}`
} else if (isGeminiReasoningModel(model) && reasoningEffort.extra_body?.google?.thinking_config) { } else if (isGeminiReasoningModel(model) && reasoningEffort.extra_body?.google?.thinking_config) {
suffix = ` --thinking_budget ${reasoningEffort.extra_body.google.thinking_config.thinkingBudget}` suffix = ` --thinking_budget ${reasoningEffort.extra_body.google.thinking_config.thinking_budget}`
} }
// FIXME: poe 不支持多个text part,上传文本文件的时候用的不是file part而是text part,因此会出问题 // FIXME: poe 不支持多个text part,上传文本文件的时候用的不是file part而是text part,因此会出问题
// 临时解决方案是强制poe用string content,但是其实poe部分支持array // 临时解决方案是强制poe用string content,但是其实poe部分支持array
@@ -85,6 +85,19 @@ export function supportsLargeFileUpload(model: Model): boolean {
}) })
} }
/**
* 检查模型是否支持TopP
*/
export function supportsTopP(model: Model): boolean {
const provider = getProviderByModel(model)
if (provider?.type === 'anthropic' || model?.endpoint_type === 'anthropic') {
return false
}
return true
}
/** /**
* 获取提供商特定的文件大小限制 * 获取提供商特定的文件大小限制
*/ */
@@ -34,6 +34,7 @@ import { setupToolsConfig } from '../utils/mcp'
import { buildProviderOptions } from '../utils/options' import { buildProviderOptions } from '../utils/options'
import { getAnthropicThinkingBudget } from '../utils/reasoning' import { getAnthropicThinkingBudget } from '../utils/reasoning'
import { buildProviderBuiltinWebSearchConfig } from '../utils/websearch' import { buildProviderBuiltinWebSearchConfig } from '../utils/websearch'
import { supportsTopP } from './modelCapabilities'
import { getTemperature, getTopP } from './modelParameters' import { getTemperature, getTopP } from './modelParameters'
const logger = loggerService.withContext('parameterBuilder') const logger = loggerService.withContext('parameterBuilder')
@@ -176,20 +177,27 @@ export async function buildStreamTextParams(
messages: sdkMessages, messages: sdkMessages,
maxOutputTokens: maxTokens, maxOutputTokens: maxTokens,
temperature: getTemperature(assistant, model), temperature: getTemperature(assistant, model),
topP: getTopP(assistant, model),
abortSignal: options.requestOptions?.signal, abortSignal: options.requestOptions?.signal,
headers: options.requestOptions?.headers, headers: options.requestOptions?.headers,
providerOptions, providerOptions,
stopWhen: stepCountIs(20), stopWhen: stepCountIs(20),
maxRetries: 0 maxRetries: 0
} }
if (supportsTopP(model)) {
params.topP = getTopP(assistant, model)
}
if (tools) { if (tools) {
params.tools = tools params.tools = tools
} }
if (assistant.prompt) { if (assistant.prompt) {
params.system = await replacePromptVariables(assistant.prompt, model.name) params.system = await replacePromptVariables(assistant.prompt, model.name)
} }
logger.debug('params', params) logger.debug('params', params)
return { return {
params, params,
modelId: model.id, modelId: model.id,
@@ -21,10 +21,45 @@ vi.mock('@renderer/store', () => ({
} }
})) }))
vi.mock('@renderer/utils/api', () => ({
formatApiHost: vi.fn((host, isSupportedAPIVersion = true) => {
if (isSupportedAPIVersion === false) {
return host // Return host as-is when isSupportedAPIVersion is false
}
return `${host}/v1` // Default behavior when isSupportedAPIVersion is true
}),
routeToEndpoint: vi.fn((host) => ({
baseURL: host,
endpoint: '/chat/completions'
}))
}))
vi.mock('@renderer/config/providers', async (importOriginal) => {
const actual = (await importOriginal()) as any
return {
...actual,
isCherryAIProvider: vi.fn(),
isPerplexityProvider: vi.fn(),
isAnthropicProvider: vi.fn(() => false),
isAzureOpenAIProvider: vi.fn(() => false),
isGeminiProvider: vi.fn(() => false),
isNewApiProvider: vi.fn(() => false)
}
})
vi.mock('@renderer/hooks/useVertexAI', () => ({
isVertexProvider: vi.fn(() => false),
isVertexAIConfigured: vi.fn(() => false),
createVertexProvider: vi.fn()
}))
import { isCherryAIProvider, isPerplexityProvider } from '@renderer/config/providers'
import { getProviderByModel } from '@renderer/services/AssistantService'
import type { Model, Provider } from '@renderer/types' import type { Model, Provider } from '@renderer/types'
import { formatApiHost } from '@renderer/utils/api'
import { COPILOT_DEFAULT_HEADERS, COPILOT_EDITOR_VERSION, isCopilotResponsesModel } from '../constants' import { COPILOT_DEFAULT_HEADERS, COPILOT_EDITOR_VERSION, isCopilotResponsesModel } from '../constants'
import { providerToAiSdkConfig } from '../providerConfig' import { getActualProvider, providerToAiSdkConfig } from '../providerConfig'
const createWindowKeyv = () => { const createWindowKeyv = () => {
const store = new Map<string, string>() const store = new Map<string, string>()
@@ -46,11 +81,31 @@ const createCopilotProvider = (): Provider => ({
isSystem: true isSystem: true
}) })
const createModel = (id: string, name = id): Model => ({ const createModel = (id: string, name = id, provider = 'copilot'): Model => ({
id, id,
name, name,
provider: 'copilot', provider,
group: 'copilot' group: provider
})
const createCherryAIProvider = (): Provider => ({
id: 'cherryai',
type: 'openai',
name: 'CherryAI',
apiKey: 'test-key',
apiHost: 'https://api.cherryai.com',
models: [],
isSystem: false
})
const createPerplexityProvider = (): Provider => ({
id: 'perplexity',
type: 'openai',
name: 'Perplexity',
apiKey: 'test-key',
apiHost: 'https://api.perplexity.ai',
models: [],
isSystem: false
}) })
describe('Copilot responses routing', () => { describe('Copilot responses routing', () => {
@@ -87,3 +142,134 @@ describe('Copilot responses routing', () => {
expect(config.options.headers?.['Copilot-Integration-Id']).toBe(COPILOT_DEFAULT_HEADERS['Copilot-Integration-Id']) expect(config.options.headers?.['Copilot-Integration-Id']).toBe(COPILOT_DEFAULT_HEADERS['Copilot-Integration-Id'])
}) })
}) })
describe('CherryAI provider configuration', () => {
beforeEach(() => {
;(globalThis as any).window = {
...(globalThis as any).window,
keyv: createWindowKeyv()
}
vi.clearAllMocks()
})
it('formats CherryAI provider apiHost with false parameter', () => {
const provider = createCherryAIProvider()
const model = createModel('gpt-4', 'GPT-4', 'cherryai')
// Mock the functions to simulate CherryAI provider detection
vi.mocked(isCherryAIProvider).mockReturnValue(true)
vi.mocked(getProviderByModel).mockReturnValue(provider)
// Call getActualProvider which should trigger formatProviderApiHost
const actualProvider = getActualProvider(model)
// Verify that formatApiHost was called with false as the second parameter
expect(formatApiHost).toHaveBeenCalledWith('https://api.cherryai.com', false)
expect(actualProvider.apiHost).toBe('https://api.cherryai.com')
})
it('does not format non-CherryAI provider with false parameter', () => {
const provider = {
id: 'openai',
type: 'openai',
name: 'OpenAI',
apiKey: 'test-key',
apiHost: 'https://api.openai.com',
models: [],
isSystem: false
} as Provider
const model = createModel('gpt-4', 'GPT-4', 'openai')
// Mock the functions to simulate non-CherryAI provider
vi.mocked(isCherryAIProvider).mockReturnValue(false)
vi.mocked(getProviderByModel).mockReturnValue(provider)
// Call getActualProvider
const actualProvider = getActualProvider(model)
// Verify that formatApiHost was called with default parameters (true)
expect(formatApiHost).toHaveBeenCalledWith('https://api.openai.com')
expect(actualProvider.apiHost).toBe('https://api.openai.com/v1')
})
it('handles CherryAI provider with empty apiHost', () => {
const provider = createCherryAIProvider()
provider.apiHost = ''
const model = createModel('gpt-4', 'GPT-4', 'cherryai')
vi.mocked(isCherryAIProvider).mockReturnValue(true)
vi.mocked(getProviderByModel).mockReturnValue(provider)
const actualProvider = getActualProvider(model)
expect(formatApiHost).toHaveBeenCalledWith('', false)
expect(actualProvider.apiHost).toBe('')
})
})
describe('Perplexity provider configuration', () => {
beforeEach(() => {
;(globalThis as any).window = {
...(globalThis as any).window,
keyv: createWindowKeyv()
}
vi.clearAllMocks()
})
it('formats Perplexity provider apiHost with false parameter', () => {
const provider = createPerplexityProvider()
const model = createModel('sonar', 'Sonar', 'perplexity')
// Mock the functions to simulate Perplexity provider detection
vi.mocked(isCherryAIProvider).mockReturnValue(false)
vi.mocked(isPerplexityProvider).mockReturnValue(true)
vi.mocked(getProviderByModel).mockReturnValue(provider)
// Call getActualProvider which should trigger formatProviderApiHost
const actualProvider = getActualProvider(model)
// Verify that formatApiHost was called with false as the second parameter
expect(formatApiHost).toHaveBeenCalledWith('https://api.perplexity.ai', false)
expect(actualProvider.apiHost).toBe('https://api.perplexity.ai')
})
it('does not format non-Perplexity provider with false parameter', () => {
const provider = {
id: 'openai',
type: 'openai',
name: 'OpenAI',
apiKey: 'test-key',
apiHost: 'https://api.openai.com',
models: [],
isSystem: false
} as Provider
const model = createModel('gpt-4', 'GPT-4', 'openai')
// Mock the functions to simulate non-Perplexity provider
vi.mocked(isCherryAIProvider).mockReturnValue(false)
vi.mocked(isPerplexityProvider).mockReturnValue(false)
vi.mocked(getProviderByModel).mockReturnValue(provider)
// Call getActualProvider
const actualProvider = getActualProvider(model)
// Verify that formatApiHost was called with default parameters (true)
expect(formatApiHost).toHaveBeenCalledWith('https://api.openai.com')
expect(actualProvider.apiHost).toBe('https://api.openai.com/v1')
})
it('handles Perplexity provider with empty apiHost', () => {
const provider = createPerplexityProvider()
provider.apiHost = ''
const model = createModel('sonar', 'Sonar', 'perplexity')
vi.mocked(isCherryAIProvider).mockReturnValue(false)
vi.mocked(isPerplexityProvider).mockReturnValue(true)
vi.mocked(getProviderByModel).mockReturnValue(provider)
const actualProvider = getActualProvider(model)
expect(formatApiHost).toHaveBeenCalledWith('', false)
expect(actualProvider.apiHost).toBe('')
})
})
@@ -52,7 +52,7 @@ const AIHUBMIX_RULES: RuleSet = {
} }
} }
], ],
fallbackRule: (provider: Provider) => provider fallbackRule: (provider: Provider) => extraProviderConfig(provider)
} }
export const aihubmixProviderCreator = provider2Provider.bind(null, AIHUBMIX_RULES) export const aihubmixProviderCreator = provider2Provider.bind(null, AIHUBMIX_RULES)
@@ -9,11 +9,15 @@ import { isOpenAIChatCompletionOnlyModel } from '@renderer/config/models'
import { import {
isAnthropicProvider, isAnthropicProvider,
isAzureOpenAIProvider, isAzureOpenAIProvider,
isCherryAIProvider,
isGeminiProvider, isGeminiProvider,
isNewApiProvider isNewApiProvider,
isPerplexityProvider
} from '@renderer/config/providers' } from '@renderer/config/providers'
import { import {
getAwsBedrockAccessKeyId, getAwsBedrockAccessKeyId,
getAwsBedrockApiKey,
getAwsBedrockAuthType,
getAwsBedrockRegion, getAwsBedrockRegion,
getAwsBedrockSecretAccessKey getAwsBedrockSecretAccessKey
} from '@renderer/hooks/useAwsBedrock' } from '@renderer/hooks/useAwsBedrock'
@@ -98,6 +102,10 @@ function formatProviderApiHost(provider: Provider): Provider {
formatted.apiHost = formatAzureOpenAIApiHost(formatted.apiHost) formatted.apiHost = formatAzureOpenAIApiHost(formatted.apiHost)
} else if (isVertexProvider(formatted)) { } else if (isVertexProvider(formatted)) {
formatted.apiHost = formatVertexApiHost(formatted) formatted.apiHost = formatVertexApiHost(formatted)
} else if (isCherryAIProvider(formatted)) {
formatted.apiHost = formatApiHost(formatted.apiHost, false)
} else if (isPerplexityProvider(formatted)) {
formatted.apiHost = formatApiHost(formatted.apiHost, false)
} else { } else {
formatted.apiHost = formatApiHost(formatted.apiHost) formatted.apiHost = formatApiHost(formatted.apiHost)
} }
@@ -192,9 +200,15 @@ export function providerToAiSdkConfig(
// bedrock // bedrock
if (aiSdkProviderId === 'bedrock') { if (aiSdkProviderId === 'bedrock') {
const authType = getAwsBedrockAuthType()
extraOptions.region = getAwsBedrockRegion() extraOptions.region = getAwsBedrockRegion()
extraOptions.accessKeyId = getAwsBedrockAccessKeyId()
extraOptions.secretAccessKey = getAwsBedrockSecretAccessKey() if (authType === 'apiKey') {
extraOptions.apiKey = getAwsBedrockApiKey()
} else {
extraOptions.accessKeyId = getAwsBedrockAccessKeyId()
extraOptions.secretAccessKey = getAwsBedrockSecretAccessKey()
}
} }
// google-vertex // google-vertex
if (aiSdkProviderId === 'google-vertex' || aiSdkProviderId === 'google-vertex-anthropic') { if (aiSdkProviderId === 'google-vertex' || aiSdkProviderId === 'google-vertex-anthropic') {
+30
View File
@@ -17,6 +17,7 @@ import { getAiSdkProviderId } from '../provider/factory'
import { buildGeminiGenerateImageParams } from './image' import { buildGeminiGenerateImageParams } from './image'
import { import {
getAnthropicReasoningParams, getAnthropicReasoningParams,
getBedrockReasoningParams,
getCustomParameters, getCustomParameters,
getGeminiReasoningParams, getGeminiReasoningParams,
getOpenAIReasoningParams, getOpenAIReasoningParams,
@@ -127,6 +128,9 @@ export function buildProviderOptions(
case 'google-vertex-anthropic': case 'google-vertex-anthropic':
providerSpecificOptions = buildAnthropicProviderOptions(assistant, model, capabilities) providerSpecificOptions = buildAnthropicProviderOptions(assistant, model, capabilities)
break break
case 'bedrock':
providerSpecificOptions = buildBedrockProviderOptions(assistant, model, capabilities)
break
default: default:
// 对于其他 provider,使用通用的构建逻辑 // 对于其他 provider,使用通用的构建逻辑
providerSpecificOptions = { providerSpecificOptions = {
@@ -266,6 +270,32 @@ function buildXAIProviderOptions(
return providerOptions return providerOptions
} }
/**
* Build Bedrock providerOptions
*/
function buildBedrockProviderOptions(
assistant: Assistant,
model: Model,
capabilities: {
enableReasoning: boolean
enableWebSearch: boolean
enableGenerateImage: boolean
}
): Record<string, any> {
const { enableReasoning } = capabilities
let providerOptions: Record<string, any> = {}
if (enableReasoning) {
const reasoningParams = getBedrockReasoningParams(assistant, model)
providerOptions = {
...providerOptions,
...reasoningParams
}
}
return providerOptions
}
/** /**
* 构建通用的 providerOptions(用于其他 provider * 构建通用的 providerOptions(用于其他 provider
*/ */
+38 -10
View File
@@ -98,7 +98,7 @@ export function getReasoningEffort(assistant: Assistant, model: Model): Reasonin
extra_body: { extra_body: {
google: { google: {
thinking_config: { thinking_config: {
thinkingBudget: 0 thinking_budget: 0
} }
} }
} }
@@ -259,8 +259,8 @@ export function getReasoningEffort(assistant: Assistant, model: Model): Reasonin
extra_body: { extra_body: {
google: { google: {
thinking_config: { thinking_config: {
thinkingBudget: -1, thinking_budget: -1,
includeThoughts: true include_thoughts: true
} }
} }
} }
@@ -270,8 +270,8 @@ export function getReasoningEffort(assistant: Assistant, model: Model): Reasonin
extra_body: { extra_body: {
google: { google: {
thinking_config: { thinking_config: {
thinkingBudget: budgetTokens, thinking_budget: budgetTokens ?? -1,
includeThoughts: true include_thoughts: true
} }
} }
} }
@@ -431,8 +431,8 @@ export function getGeminiReasoningParams(assistant: Assistant, model: Model): Re
if (reasoningEffort === undefined) { if (reasoningEffort === undefined) {
return { return {
thinkingConfig: { thinkingConfig: {
includeThoughts: false, include_thoughts: false,
...(GEMINI_FLASH_MODEL_REGEX.test(model.id) ? { thinkingBudget: 0 } : {}) ...(GEMINI_FLASH_MODEL_REGEX.test(model.id) ? { thinking_budget: 0 } : {})
} }
} }
} }
@@ -442,7 +442,7 @@ export function getGeminiReasoningParams(assistant: Assistant, model: Model): Re
if (effortRatio > 1) { if (effortRatio > 1) {
return { return {
thinkingConfig: { thinkingConfig: {
includeThoughts: true include_thoughts: true
} }
} }
} }
@@ -452,8 +452,8 @@ export function getGeminiReasoningParams(assistant: Assistant, model: Model): Re
return { return {
thinkingConfig: { thinkingConfig: {
...(budget > 0 ? { thinkingBudget: budget } : {}), ...(budget > 0 ? { thinking_budget: budget } : {}),
includeThoughts: true include_thoughts: true
} }
} }
} }
@@ -485,6 +485,34 @@ export function getXAIReasoningParams(assistant: Assistant, model: Model): Recor
} }
} }
/**
* Get Bedrock reasoning parameters
*/
export function getBedrockReasoningParams(assistant: Assistant, model: Model): Record<string, any> {
if (!isReasoningModel(model)) {
return {}
}
const reasoningEffort = assistant?.settings?.reasoning_effort
if (reasoningEffort === undefined) {
return {}
}
// Only apply thinking budget for Claude reasoning models
if (!isSupportedThinkingTokenClaudeModel(model)) {
return {}
}
const budgetTokens = getAnthropicThinkingBudget(assistant, model)
return {
reasoningConfig: {
type: 'enabled',
budgetTokens: budgetTokens
}
}
}
/** /**
* 获取自定义参数 * 获取自定义参数
* 从 assistant 设置中提取自定义参数 * 从 assistant 设置中提取自定义参数
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" width="33" height="38" viewBox="0 0 33 38" fill="none">
<g clip-path="url(#clip0_4321_9943)">
<path d="M1.51221 6.59813C1.51221 4.09263 3.54331 2.06152 6.04881 2.06152H27.9757C30.4812 2.06152 32.5123 4.09263 32.5123 6.59813C32.5123 9.10362 30.4812 11.1347 27.9757 11.1347H6.04881C3.54331 11.1347 1.51221 9.10362 1.51221 6.59813Z" fill="#6200EE"/>
<path d="M3.38905 3.56467C5.26076 1.89906 8.12831 2.06615 9.79391 3.93785L22.1493 17.8221C23.8149 19.6938 23.6478 22.5614 21.7761 24.227C19.9044 25.8926 17.0369 25.7255 15.3713 23.8538L3.01586 9.96953C1.35026 8.09782 1.51734 5.23027 3.38905 3.56467Z" fill="#6200EE"/>
<path d="M1.51221 20.9643C1.51221 18.4588 3.54331 16.4277 6.04881 16.4277H18.9025C21.408 16.4277 23.4391 18.4588 23.4391 20.9643C23.4391 23.4698 21.408 25.5009 18.9025 25.5009H6.04881C3.54331 25.5009 1.51221 23.4698 1.51221 20.9643Z" fill="#6200EE"/>
<path d="M10.5854 32.3052C10.5854 34.8107 8.55431 36.8418 6.04881 36.8418C3.54331 36.8418 1.51221 34.8107 1.51221 32.3052C1.51221 29.7997 3.54331 27.7686 6.04881 27.7686C8.55431 27.7686 10.5854 29.7997 10.5854 32.3052Z" fill="#BF7AFF"/>
</g>
<defs>
<clipPath id="clip0_4321_9943">
<rect width="32.5124" height="36.9029" fill="white" transform="translate(0 0.548828)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+2 -2
View File
@@ -41,11 +41,11 @@ body,
margin: 0; margin: 0;
} }
/* #root { #root {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
flex: 1; flex: 1;
} */ }
body { body {
display: flex; display: flex;
@@ -1,10 +1,6 @@
@import 'tailwindcss' source('../../../../renderer'); @import 'tailwindcss' source('../../../../renderer');
@import 'tw-animate-css'; @import 'tw-animate-css';
/* heroui */
@plugin '../../hero.ts';
@source '../../../../../node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}';
@custom-variant dark (&:is(.dark *)); @custom-variant dark (&:is(.dark *));
/* 如需自定义: /* 如需自定义:
@@ -156,11 +152,6 @@
body { body {
@apply bg-background text-foreground; @apply bg-background text-foreground;
} }
/* To disable drag title bar on toast. tailwind css doesn't provide such class name. */
.hero-toast {
-webkit-app-region: no-drag;
}
} }
:root { :root {
@@ -1,31 +0,0 @@
import { Avatar, cn } from '@heroui/react'
import { getModelLogoById } from '@renderer/config/models'
import type { ApiModel } from '@renderer/types'
import React from 'react'
import Ellipsis from './Ellipsis'
export interface ModelLabelProps extends Omit<React.ComponentPropsWithRef<'div'>, 'children'> {
model?: ApiModel
classNames?: {
container?: string
avatar?: string
modelName?: string
divider?: string
providerName?: string
}
}
export const ApiModelLabel: React.FC<ModelLabelProps> = ({ model, className, classNames, ...props }) => {
return (
<div className={cn('flex items-center gap-1', className, classNames?.container)} {...props}>
<Avatar
src={model ? (getModelLogoById(model.id) ?? getModelLogoById(model.name)) : undefined}
className={cn('h-4 w-4', classNames?.avatar)}
/>
<Ellipsis className={classNames?.modelName}>{model?.name}</Ellipsis>
<span className={classNames?.divider}> | </span>
<Ellipsis className={classNames?.providerName}>{model?.provider_name}</Ellipsis>
</div>
)
}
@@ -1,4 +1,4 @@
import { Button, Popover, PopoverContent, PopoverTrigger } from '@heroui/react' import { Button, Popover } from 'antd'
import React from 'react' import React from 'react'
import EmojiPicker from '../EmojiPicker' import EmojiPicker from '../EmojiPicker'
@@ -10,13 +10,10 @@ type Props = {
export const EmojiAvatarWithPicker: React.FC<Props> = ({ emoji, onPick }) => { export const EmojiAvatarWithPicker: React.FC<Props> = ({ emoji, onPick }) => {
return ( return (
<Popover> <Popover content={<EmojiPicker onEmojiClick={onPick} />} trigger="click">
<PopoverTrigger> <Button type="text" style={{ width: 32, height: 32, fontSize: 18 }}>
<Button size="sm" startContent={<span className="text-lg">{emoji}</span>} isIconOnly /> {emoji}
</PopoverTrigger> </Button>
<PopoverContent>
<EmojiPicker onEmojiClick={onPick}></EmojiPicker>
</PopoverContent>
</Popover> </Popover>
) )
} }
@@ -1,4 +1,4 @@
import { cn } from '@heroui/react' import { cn } from '@renderer/utils'
import type { ButtonProps } from 'antd' import type { ButtonProps } from 'antd'
import { Button } from 'antd' import { Button } from 'antd'
import React, { memo } from 'react' import React, { memo } from 'react'
+18 -8
View File
@@ -1,5 +1,5 @@
import { Button } from '@heroui/react' import { CheckOutlined, CloseOutlined } from '@ant-design/icons'
import { CheckIcon, XIcon } from 'lucide-react' import { Button } from 'antd'
import type { FC } from 'react' import type { FC } from 'react'
import { createPortal } from 'react-dom' import { createPortal } from 'react-dom'
@@ -28,12 +28,22 @@ const ConfirmDialog: FC<Props> = ({ x, y, message, onConfirm, onCancel }) => {
<div className="flex min-w-[160px] items-center rounded-lg border border-[var(--color-border)] bg-[var(--color-background)] p-3 shadow-[0_4px_12px_rgba(0,0,0,0.15)]"> <div className="flex min-w-[160px] items-center rounded-lg border border-[var(--color-border)] bg-[var(--color-background)] p-3 shadow-[0_4px_12px_rgba(0,0,0,0.15)]">
<div className="mr-2 text-sm leading-[1.4]">{message}</div> <div className="mr-2 text-sm leading-[1.4]">{message}</div>
<div className="flex justify-center gap-2"> <div className="flex justify-center gap-2">
<Button onPress={onCancel} radius="full" className="h-6 w-6 min-w-0 p-1" color="danger"> <Button
<XIcon className="text-danger-foreground" size={16} /> onClick={onCancel}
</Button> shape="circle"
<Button onPress={onConfirm} radius="full" className="h-6 w-6 min-w-0 p-1" color="success"> size="small"
<CheckIcon className="text-success-foreground" size={16} /> danger
</Button> icon={<CloseOutlined />}
style={{ width: 24, height: 24, minWidth: 24 }}
/>
<Button
onClick={onConfirm}
shape="circle"
size="small"
type="primary"
icon={<CheckOutlined />}
style={{ width: 24, height: 24, minWidth: 24, backgroundColor: '#52c41a' }}
/>
</div> </div>
</div> </div>
</div> </div>
@@ -1,5 +1,5 @@
import { Button } from '@heroui/button'
import { formatErrorMessage } from '@renderer/utils/error' import { formatErrorMessage } from '@renderer/utils/error'
import { Button } from 'antd'
import { Alert, Space } from 'antd' import { Alert, Space } from 'antd'
import type { ComponentType, ReactNode } from 'react' import type { ComponentType, ReactNode } from 'react'
import type { FallbackProps } from 'react-error-boundary' import type { FallbackProps } from 'react-error-boundary'
@@ -24,10 +24,10 @@ const DefaultFallback: ComponentType<FallbackProps> = (props: FallbackProps): Re
type="error" type="error"
action={ action={
<Space> <Space>
<Button size="sm" onPress={debug}> <Button size="small" onClick={debug}>
{t('error.boundary.default.devtools')} {t('error.boundary.default.devtools')}
</Button> </Button>
<Button size="sm" onPress={reload}> <Button size="small" onClick={reload}>
{t('error.boundary.default.reload')} {t('error.boundary.default.reload')}
</Button> </Button>
</Space> </Space>
@@ -1,5 +1,5 @@
import { cn } from '@heroui/react'
import Scrollbar from '@renderer/components/Scrollbar' import Scrollbar from '@renderer/components/Scrollbar'
import { cn } from '@renderer/utils'
import { ChevronRight } from 'lucide-react' import { ChevronRight } from 'lucide-react'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
@@ -1,5 +1,5 @@
import { cn } from '@heroui/react'
import { TopView } from '@renderer/components/TopView' import { TopView } from '@renderer/components/TopView'
import { cn } from '@renderer/utils'
import { Modal } from 'antd' import { Modal } from 'antd'
import { Bot, MessageSquare } from 'lucide-react' import { Bot, MessageSquare } from 'lucide-react'
import { useState } from 'react' import { useState } from 'react'
@@ -51,7 +51,7 @@ const PopupContainer: React.FC<Props> = ({ onSelect, resolve }) => {
<button <button
type="button" type="button"
onClick={() => handleSelect('assistant')} onClick={() => handleSelect('assistant')}
className="group flex flex-col items-center gap-3 rounded-lg bg-[var(--color-background-soft)] p-6 transition-all hover:bg-[var(--color-hover)]" className="group flex cursor-pointer flex-col items-center gap-3 rounded-lg bg-[var(--color-background-soft)] p-6 transition-all hover:bg-[var(--color-hover)]"
onMouseEnter={() => setHoveredOption('assistant')} onMouseEnter={() => setHoveredOption('assistant')}
onMouseLeave={() => setHoveredOption(null)}> onMouseLeave={() => setHoveredOption(null)}>
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-[var(--color-list-item)] transition-colors"> <div className="flex h-12 w-12 items-center justify-center rounded-full bg-[var(--color-list-item)] transition-colors">
@@ -73,7 +73,7 @@ const PopupContainer: React.FC<Props> = ({ onSelect, resolve }) => {
<button <button
onClick={() => handleSelect('agent')} onClick={() => handleSelect('agent')}
type="button" type="button"
className="group flex flex-col items-center gap-3 rounded-lg bg-[var(--color-background-soft)] p-6 transition-all hover:bg-[var(--color-hover)]" className="group flex cursor-pointer flex-col items-center gap-3 rounded-lg bg-[var(--color-background-soft)] p-6 transition-all hover:bg-[var(--color-hover)]"
onMouseEnter={() => setHoveredOption('agent')} onMouseEnter={() => setHoveredOption('agent')}
onMouseLeave={() => setHoveredOption(null)}> onMouseLeave={() => setHoveredOption(null)}>
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-[var(--color-list-item)] transition-colors"> <div className="flex h-12 w-12 items-center justify-center rounded-full bg-[var(--color-list-item)] transition-colors">
@@ -1,9 +1,8 @@
import { Button } from '@heroui/button'
import { Modal, ModalBody, ModalContent, ModalFooter, ModalHeader } from '@heroui/modal'
import { Progress } from '@heroui/progress'
import { Spinner } from '@heroui/spinner'
import { loggerService } from '@logger' import { loggerService } from '@logger'
import { AppLogo } from '@renderer/config/env'
import { SettingHelpText, SettingRow } from '@renderer/pages/settings' import { SettingHelpText, SettingRow } from '@renderer/pages/settings'
import type { WebSocketCandidatesResponse } from '@shared/config/types'
import { Alert, Button, Modal, Progress, Spin } from 'antd'
import { QRCodeSVG } from 'qrcode.react' import { QRCodeSVG } from 'qrcode.react'
import { useCallback, useEffect, useMemo, useState } from 'react' import { useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@@ -23,7 +22,7 @@ const LoadingQRCode: React.FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '12px' }}> <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '12px' }}>
<Spinner /> <Spin />
<span style={{ fontSize: '14px', color: 'var(--color-text-2)' }}> <span style={{ fontSize: '14px', color: 'var(--color-text-2)' }}>
{t('settings.data.export_to_phone.lan.generating_qr')} {t('settings.data.export_to_phone.lan.generating_qr')}
</span> </span>
@@ -38,10 +37,10 @@ const ScanQRCode: React.FC<{ qrCodeValue: string }> = ({ qrCodeValue }) => {
<QRCodeSVG <QRCodeSVG
marginSize={2} marginSize={2}
value={qrCodeValue} value={qrCodeValue}
level="Q" level="H"
size={160} size={200}
imageSettings={{ imageSettings={{
src: '/src/assets/images/logo.png', src: AppLogo,
width: 40, width: 40,
height: 40, height: 40,
excavate: true excavate: true
@@ -70,7 +69,7 @@ const ConnectingAnimation: React.FC = () => {
borderRadius: '12px', borderRadius: '12px',
backgroundColor: 'var(--color-status-warning)' backgroundColor: 'var(--color-status-warning)'
}}> }}>
<Spinner size="lg" color="warning" /> <Spin size="large" />
<span style={{ fontSize: '14px', color: 'var(--color-text)', marginTop: '12px' }}> <span style={{ fontSize: '14px', color: 'var(--color-text)', marginTop: '12px' }}>
{t('settings.data.export_to_phone.lan.status.connecting')} {t('settings.data.export_to_phone.lan.status.connecting')}
</span> </span>
@@ -135,7 +134,6 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
const [selectedFolderPath, setSelectedFolderPath] = useState<string | null>(null) const [selectedFolderPath, setSelectedFolderPath] = useState<string | null>(null)
const [sendProgress, setSendProgress] = useState(0) const [sendProgress, setSendProgress] = useState(0)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const [showCloseConfirm, setShowCloseConfirm] = useState(false)
const [autoCloseCountdown, setAutoCloseCountdown] = useState<number | null>(null) const [autoCloseCountdown, setAutoCloseCountdown] = useState<number | null>(null)
const { t } = useTranslation() const { t } = useTranslation()
@@ -198,17 +196,28 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
const { port, ip } = await window.api.webSocket.status() const { port, ip } = await window.api.webSocket.status()
if (ip && port) { if (ip && port) {
const candidates = await window.api.webSocket.getAllCandidates() const candidatesData = await window.api.webSocket.getAllCandidates()
const connectionInfo = {
type: 'cherry-studio-app', const optimizeConnectionInfo = () => {
candidates, const ipToNumber = (ip: string) => {
selectedHost: ip, return ip.split('.').reduce((acc, octet) => (acc << 8) + parseInt(octet), 0)
port, }
timestamp: Date.now()
const compressedData = [
'CSA',
ipToNumber(ip),
candidatesData.map((candidate: WebSocketCandidatesResponse) => ipToNumber(candidate.host)),
port, // 端口号
Date.now() % 86400000
]
return compressedData
} }
setQrCodeValue(JSON.stringify(connectionInfo))
const compressedData = optimizeConnectionInfo()
const qrCodeValue = JSON.stringify(compressedData)
setQrCodeValue(qrCodeValue)
setConnectionPhase('waiting_qr_scan') setConnectionPhase('waiting_qr_scan')
logger.info(`QR code generated: ${ip}:${port} with ${candidates.length} IP candidates`)
} else { } else {
setError(t('settings.data.export_to_phone.lan.error.no_ip')) setError(t('settings.data.export_to_phone.lan.error.no_ip'))
setConnectionPhase('error') setConnectionPhase('error')
@@ -286,22 +295,20 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
// 尝试关闭弹窗 - 如果正在传输则显示确认 // 尝试关闭弹窗 - 如果正在传输则显示确认
const handleCancel = useCallback(() => { const handleCancel = useCallback(() => {
if (isSending) { if (isSending) {
setShowCloseConfirm(true) window.modal.confirm({
title: t('settings.data.export_to_phone.lan.confirm_close_title'),
content: t('settings.data.export_to_phone.lan.confirm_close_message'),
centered: true,
okButtonProps: {
danger: true
},
okText: t('settings.data.export_to_phone.lan.force_close'),
onOk: () => setIsOpen(false)
})
} else { } else {
setIsOpen(false) setIsOpen(false)
} }
}, [isSending]) }, [isSending, t])
// 确认强制关闭
const handleForceClose = useCallback(() => {
logger.info('Force closing popup during transfer')
setIsOpen(false)
}, [])
// 取消关闭确认
const handleCancelClose = useCallback(() => {
setShowCloseConfirm(false)
}, [])
// 清理并关闭 // 清理并关闭
const handleClose = useCallback(async () => { const handleClose = useCallback(async () => {
@@ -363,11 +370,13 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
style={{ style={{
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center',
gap: '8px', gap: '8px',
padding: '8px 12px', padding: '5px 12px',
borderRadius: '8px', width: '100%',
backgroundColor: connectionStatusStyles.bg, backgroundColor: connectionStatusStyles.bg,
border: `1px solid ${connectionStatusStyles.border}` border: `1px solid ${connectionStatusStyles.border}`,
marginBottom: 10
}}> }}>
<span style={{ fontSize: '14px', fontWeight: '500', color: 'var(--color-text)' }}>{connectionStatusText}</span> <span style={{ fontSize: '14px', fontWeight: '500', color: 'var(--color-text)' }}>{connectionStatusText}</span>
</div> </div>
@@ -399,7 +408,7 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
if (!isSending && transferPhase !== 'completed') return null if (!isSending && transferPhase !== 'completed') return null
return ( return (
<div style={{ paddingTop: '8px' }}> <div style={{ paddingTop: '20px' }}>
<div <div
style={{ style={{
display: 'flex', display: 'flex',
@@ -428,11 +437,9 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
</div> </div>
<Progress <Progress
value={Math.round(sendProgress)} percent={Math.round(sendProgress)}
size="md" status={transferPhase === 'completed' ? 'success' : 'active'}
color={transferPhase === 'completed' ? 'success' : 'primary'} showInfo={false}
showValueLabel={false}
aria-label="Send progress"
/> />
</div> </div>
</div> </div>
@@ -475,95 +482,50 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
return ( return (
<Modal <Modal
isOpen={isOpen} open={isOpen}
onOpenChange={(open) => { onCancel={handleCancel}
if (!open) { afterClose={handleClose}
handleCancel() title={t('settings.data.export_to_phone.lan.title')}
} centered
}} closable={!isSending}
isDismissable={false} maskClosable={false}
isKeyboardDismissDisabled={false} keyboard={true}
placement="center" footer={null}
onClose={handleClose}> styles={{ body: { paddingBottom: 10 } }}>
<ModalContent> <SettingRow>
{() => ( <StatusIndicator />
<> </SettingRow>
<ModalHeader>{t('settings.data.export_to_phone.lan.title')}</ModalHeader>
<ModalBody>
<SettingRow>
<StatusIndicator />
</SettingRow>
<SettingRow> <Alert message={t('settings.data.export_to_phone.lan.content')} type="info" style={{ borderRadius: 0 }} />
<div>{t('settings.data.export_to_phone.lan.content')}</div>
</SettingRow>
<SettingRow style={{ display: 'flex', justifyContent: 'center', minHeight: '180px' }}> <SettingRow style={{ display: 'flex', justifyContent: 'center', minHeight: '180px', marginBlock: 25 }}>
<QRCodeDisplay /> <QRCodeDisplay />
</SettingRow> </SettingRow>
<SettingRow style={{ display: 'flex', alignItems: 'center' }}> <SettingRow style={{ display: 'flex', alignItems: 'center', marginBlock: 10 }}>
<div style={{ display: 'flex', gap: 10, justifyContent: 'center', width: '100%' }}> <div style={{ display: 'flex', gap: 10, justifyContent: 'center', width: '100%' }}>
<Button color="default" variant="flat" onPress={handleSelectZip} isDisabled={isSending}> <Button onClick={handleSelectZip} disabled={isSending}>
{t('settings.data.export_to_phone.lan.selectZip')} {t('settings.data.export_to_phone.lan.selectZip')}
</Button> </Button>
<Button color="primary" onPress={handleSendZip} isDisabled={!canSend} isLoading={isSending}> <Button type="primary" onClick={handleSendZip} disabled={!canSend} loading={isSending}>
{transferStatusText || t('settings.data.export_to_phone.lan.sendZip')} {transferStatusText || t('settings.data.export_to_phone.lan.sendZip')}
</Button> </Button>
</div> </div>
</SettingRow> </SettingRow>
<SettingHelpText <SettingHelpText
style={{ style={{
overflow: 'hidden', overflow: 'hidden',
textOverflow: 'ellipsis', textOverflow: 'ellipsis',
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
textAlign: 'center' textAlign: 'center'
}}> }}>
{selectedFolderPath || t('settings.data.export_to_phone.lan.noZipSelected')} {selectedFolderPath || t('settings.data.export_to_phone.lan.noZipSelected')}
</SettingHelpText> </SettingHelpText>
<TransferProgress /> <TransferProgress />
<AutoCloseCountdown /> <AutoCloseCountdown />
<ErrorDisplay /> <ErrorDisplay />
</ModalBody>
{showCloseConfirm && (
<ModalFooter>
<div
style={{
display: 'flex',
flexDirection: 'column',
width: '100%',
gap: '12px',
padding: '8px',
borderRadius: '8px',
backgroundColor: 'var(--color-status-warning)',
border: '1px solid var(--color-status-warning)'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span style={{ fontSize: '20px' }}></span>
<span style={{ fontSize: '14px', color: 'var(--color-text)', fontWeight: '500' }}>
{t('settings.data.export_to_phone.lan.confirm_close_title')}
</span>
</div>
<span style={{ fontSize: '13px', color: 'var(--color-text-2)', marginLeft: '28px' }}>
{t('settings.data.export_to_phone.lan.confirm_close_message')}
</span>
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '8px', marginTop: '4px' }}>
<Button size="sm" color="default" variant="flat" onPress={handleCancelClose}>
{t('common.cancel')}
</Button>
<Button size="sm" color="danger" onPress={handleForceClose}>
{t('settings.data.export_to_phone.lan.force_close')}
</Button>
</div>
</div>
</ModalFooter>
)}
</>
)}
</ModalContent>
</Modal> </Modal>
) )
} }
@@ -0,0 +1,205 @@
import { loggerService } from '@logger'
import { TopView } from '@renderer/components/TopView'
import { handleSaveData } from '@renderer/store'
import { Button, Modal } from 'antd'
import type { ReleaseNoteInfo, UpdateInfo } from 'builder-util-runtime'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Markdown from 'react-markdown'
import styled from 'styled-components'
const logger = loggerService.withContext('UpdateDialog')
interface ShowParams {
releaseInfo: UpdateInfo | null
}
interface Props extends ShowParams {
resolve: (data: any) => void
}
const PopupContainer: React.FC<Props> = ({ releaseInfo, resolve }) => {
const { t } = useTranslation()
const [open, setOpen] = useState(true)
const [isInstalling, setIsInstalling] = useState(false)
useEffect(() => {
if (releaseInfo) {
logger.info('Update dialog opened', { version: releaseInfo.version })
}
}, [releaseInfo])
const handleInstall = async () => {
setIsInstalling(true)
try {
await handleSaveData()
await window.api.quitAndInstall()
setOpen(false)
} catch (error) {
logger.error('Failed to save data before update', error as Error)
setIsInstalling(false)
window.toast.error(t('update.saveDataError'))
}
}
const onCancel = () => {
setOpen(false)
}
const onClose = () => {
resolve({})
}
UpdateDialogPopup.hide = onCancel
const releaseNotes = releaseInfo?.releaseNotes
return (
<Modal
title={
<ModalHeaderWrapper>
<h3>{t('update.title')}</h3>
<p>{t('update.message').replace('{{version}}', releaseInfo?.version || '')}</p>
</ModalHeaderWrapper>
}
open={open}
onCancel={onCancel}
afterClose={onClose}
transitionName="animation-move-down"
centered
width={720}
footer={[
<Button key="later" onClick={onCancel} disabled={isInstalling}>
{t('update.later')}
</Button>,
<Button key="install" type="primary" onClick={handleInstall} loading={isInstalling}>
{t('update.install')}
</Button>
]}>
<ModalBodyWrapper>
<ReleaseNotesWrapper className="markdown">
<Markdown>
{typeof releaseNotes === 'string'
? releaseNotes
: Array.isArray(releaseNotes)
? releaseNotes
.map((note: ReleaseNoteInfo) => note.note)
.filter(Boolean)
.join('\n\n')
: t('update.noReleaseNotes')}
</Markdown>
</ReleaseNotesWrapper>
</ModalBodyWrapper>
</Modal>
)
}
const TopViewKey = 'UpdateDialogPopup'
export default class UpdateDialogPopup {
static topviewId = 0
static hide() {
TopView.hide(TopViewKey)
}
static show(props: ShowParams) {
return new Promise<any>((resolve) => {
TopView.show(
<PopupContainer
{...props}
resolve={(v) => {
resolve(v)
TopView.hide(TopViewKey)
}}
/>,
TopViewKey
)
})
}
}
const ModalHeaderWrapper = styled.div`
display: flex;
flex-direction: column;
gap: 4px;
h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: var(--color-text-1);
}
p {
margin: 0;
font-size: 14px;
color: var(--color-text-2);
}
`
const ModalBodyWrapper = styled.div`
max-height: 450px;
overflow-y: auto;
padding: 12px 0;
`
const ReleaseNotesWrapper = styled.div`
background-color: var(--color-bg-2);
border-radius: 8px;
p {
margin: 0 0 12px 0;
color: var(--color-text-2);
font-size: 14px;
line-height: 1.6;
&:last-child {
margin-bottom: 0;
}
}
h1,
h2,
h3,
h4,
h5,
h6 {
margin: 16px 0 8px 0;
color: var(--color-text-1);
font-weight: 600;
&:first-child {
margin-top: 0;
}
}
ul,
ol {
margin: 8px 0;
padding-left: 24px;
color: var(--color-text-2);
}
li {
margin: 4px 0;
}
code {
padding: 2px 6px;
background-color: var(--color-bg-3);
border-radius: 4px;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 13px;
}
pre {
padding: 12px;
background-color: var(--color-bg-3);
border-radius: 6px;
overflow-x: auto;
code {
padding: 0;
background-color: transparent;
}
}
`
@@ -1,44 +1,32 @@
import type { SelectedItemProps } from '@heroui/react'
import {
Button,
Form,
Input,
Modal,
ModalBody,
ModalContent,
ModalFooter,
ModalHeader,
Select,
SelectItem,
Textarea,
useDisclosure
} from '@heroui/react'
import { loggerService } from '@logger' import { loggerService } from '@logger'
import type { Selection } from '@react-types/shared'
import ClaudeIcon from '@renderer/assets/images/models/claude.png' import ClaudeIcon from '@renderer/assets/images/models/claude.png'
import { ErrorBoundary } from '@renderer/components/ErrorBoundary'
import { TopView } from '@renderer/components/TopView'
import { permissionModeCards } from '@renderer/config/agent' import { permissionModeCards } from '@renderer/config/agent'
import { agentModelFilter, getModelLogoById } from '@renderer/config/models'
import { useAgents } from '@renderer/hooks/agents/useAgents' import { useAgents } from '@renderer/hooks/agents/useAgents'
import { useApiModels } from '@renderer/hooks/agents/useModels'
import { useUpdateAgent } from '@renderer/hooks/agents/useUpdateAgent' import { useUpdateAgent } from '@renderer/hooks/agents/useUpdateAgent'
import SelectAgentBaseModelButton from '@renderer/pages/home/components/SelectAgentBaseModelButton'
import type { import type {
AddAgentForm, AddAgentForm,
AgentEntity, AgentEntity,
AgentType, AgentType,
ApiModel,
BaseAgentForm, BaseAgentForm,
PermissionMode, PermissionMode,
Tool, Tool,
UpdateAgentForm UpdateAgentForm
} from '@renderer/types' } from '@renderer/types'
import { AgentConfigurationSchema, isAgentType } from '@renderer/types' import { AgentConfigurationSchema, isAgentType } from '@renderer/types'
import { Avatar, Button, Input, Modal, Select } from 'antd'
import { AlertTriangleIcon } from 'lucide-react' import { AlertTriangleIcon } from 'lucide-react'
import type { ChangeEvent, FormEvent } from 'react' import type { ChangeEvent, FormEvent } from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { ErrorBoundary } from '../../ErrorBoundary' import type { BaseOption } from './shared'
import type { BaseOption, ModelOption } from './shared'
import { Option, renderOption } from './shared' const { TextArea } = Input
const logger = loggerService.withContext('AddAgentPopup') const logger = loggerService.withContext('AddAgentPopup')
@@ -48,8 +36,6 @@ interface AgentTypeOption extends BaseOption {
name: AgentEntity['name'] name: AgentEntity['name']
} }
type Option = AgentTypeOption | ModelOption
type AgentWithTools = AgentEntity & { tools?: Tool[] } type AgentWithTools = AgentEntity & { tools?: Tool[] }
const buildAgentForm = (existing?: AgentWithTools): BaseAgentForm => ({ const buildAgentForm = (existing?: AgentWithTools): BaseAgentForm => ({
@@ -64,58 +50,37 @@ const buildAgentForm = (existing?: AgentWithTools): BaseAgentForm => ({
configuration: AgentConfigurationSchema.parse(existing?.configuration ?? {}) configuration: AgentConfigurationSchema.parse(existing?.configuration ?? {})
}) })
type Props = { interface ShowParams {
agent?: AgentWithTools agent?: AgentWithTools
isOpen: boolean
onClose: () => void
afterSubmit?: (a: AgentEntity) => void afterSubmit?: (a: AgentEntity) => void
} }
/** interface Props extends ShowParams {
* Modal component for creating or editing an agent. resolve: (data: any) => void
* }
* Either trigger or isOpen and onClose is given.
* @param agent - Optional agent entity for editing mode. const PopupContainer: React.FC<Props> = ({ agent, afterSubmit, resolve }) => {
* @param isOpen - Optional controlled modal open state. From useDisclosure.
* @param onClose - Optional callback when modal closes. From useDisclosure.
* @returns Modal component for agent creation/editing
*/
export const AgentModal: React.FC<Props> = ({ agent, isOpen: _isOpen, onClose: _onClose, afterSubmit }) => {
const { isOpen, onClose } = useDisclosure({ isOpen: _isOpen, onClose: _onClose })
const { t } = useTranslation() const { t } = useTranslation()
const [open, setOpen] = useState(true)
const loadingRef = useRef(false) const loadingRef = useRef(false)
// const { setTimeoutTimer } = useTimer()
const { addAgent } = useAgents() const { addAgent } = useAgents()
const { updateAgent } = useUpdateAgent() const { updateAgent } = useUpdateAgent()
// hard-coded. We only support anthropic for now.
const { models } = useApiModels({ providerType: 'anthropic' })
const isEditing = (agent?: AgentWithTools) => agent !== undefined const isEditing = (agent?: AgentWithTools) => agent !== undefined
const [form, setForm] = useState<BaseAgentForm>(() => buildAgentForm(agent)) const [form, setForm] = useState<BaseAgentForm>(() => buildAgentForm(agent))
useEffect(() => { useEffect(() => {
if (isOpen) { if (open) {
setForm(buildAgentForm(agent)) setForm(buildAgentForm(agent))
} }
}, [agent, isOpen]) }, [agent, open])
const selectedPermissionMode = form.configuration?.permission_mode ?? 'default' const selectedPermissionMode = form.configuration?.permission_mode ?? 'default'
const onPermissionModeChange = useCallback((keys: Selection) => { const onPermissionModeChange = useCallback((value: PermissionMode) => {
if (keys === 'all') {
return
}
const [first] = Array.from(keys)
if (!first) {
return
}
setForm((prev) => { setForm((prev) => {
const parsedConfiguration = AgentConfigurationSchema.parse(prev.configuration ?? {}) const parsedConfiguration = AgentConfigurationSchema.parse(prev.configuration ?? {})
const nextMode = first as PermissionMode if (parsedConfiguration.permission_mode === value) {
if (parsedConfiguration.permission_mode === nextMode) {
if (!prev.configuration) { if (!prev.configuration) {
return { return {
...prev, ...prev,
@@ -129,7 +94,7 @@ export const AgentModal: React.FC<Props> = ({ agent, isOpen: _isOpen, onClose: _
...prev, ...prev,
configuration: { configuration: {
...parsedConfiguration, ...parsedConfiguration,
permission_mode: nextMode permission_mode: value
} }
} }
}) })
@@ -150,55 +115,57 @@ export const AgentModal: React.FC<Props> = ({ agent, isOpen: _isOpen, onClose: _
[] []
) )
const agentOptions: AgentTypeOption[] = useMemo( const agentOptions = useMemo(
() => () =>
agentConfig.map( agentConfig.map((option) => ({
(option) => value: option.key,
({ label: (
...option, <OptionWrapper>
rendered: <Option option={option} /> <Avatar src={option.avatar} size={24} />
}) as const satisfies SelectedItemProps <span>{option.label}</span>
), </OptionWrapper>
)
})),
[agentConfig] [agentConfig]
) )
const onAgentTypeChange = useCallback( const onAgentTypeChange = useCallback(
(e: ChangeEvent<HTMLSelectElement>) => { (value: AgentType) => {
const prevConfig = agentConfig.find((config) => config.key === form.type) const prevConfig = agentConfig.find((config) => config.key === form.type)
let newName: string | undefined = form.name let newName: string | undefined = form.name
if (prevConfig && prevConfig.name === form.name) { if (prevConfig && prevConfig.name === form.name) {
const newConfig = agentConfig.find((config) => config.key === e.target.value) const newConfig = agentConfig.find((config) => config.key === value)
if (newConfig) { if (newConfig) {
newName = newConfig.name newName = newConfig.name
} }
} }
setForm((prev) => ({ setForm((prev) => ({
...prev, ...prev,
type: e.target.value as AgentType, type: value,
name: newName name: newName
})) }))
}, },
[agentConfig, form.name, form.type] [agentConfig, form.name, form.type]
) )
const onNameChange = useCallback((name: string) => { const onNameChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setForm((prev) => ({ setForm((prev) => ({
...prev, ...prev,
name name: e.target.value
})) }))
}, []) }, [])
const onDescChange = useCallback((description: string) => { const onDescChange = useCallback((e: ChangeEvent<HTMLTextAreaElement>) => {
setForm((prev) => ({ setForm((prev) => ({
...prev, ...prev,
description description: e.target.value
})) }))
}, []) }, [])
const onInstChange = useCallback((instructions: string) => { const onInstChange = useCallback((e: ChangeEvent<HTMLTextAreaElement>) => {
setForm((prev) => ({ setForm((prev) => ({
...prev, ...prev,
instructions instructions: e.target.value
})) }))
}, []) }, [])
@@ -231,34 +198,36 @@ export const AgentModal: React.FC<Props> = ({ agent, isOpen: _isOpen, onClose: _
})) }))
}, []) }, [])
const modelOptions = useMemo(() => { // Create a temporary agentBase object for SelectAgentBaseModelButton
// mocked data. not final version const tempAgentBase: AgentEntity = useMemo(
return (models ?? []) () => ({
.filter((m) => id: agent?.id ?? 'temp-creating',
agentModelFilter({ type: form.type,
id: m.id, name: form.name,
provider: m.provider || '', model: form.model,
name: m.name, accessible_paths: form.accessible_paths.length > 0 ? form.accessible_paths : ['/'],
group: '' allowed_tools: form.allowed_tools ?? [],
}) description: form.description,
) instructions: form.instructions,
.map((model) => ({ configuration: form.configuration,
type: 'model', created_at: agent?.created_at ?? new Date().toISOString(),
key: model.id, updated_at: agent?.updated_at ?? new Date().toISOString()
label: model.name, }),
avatar: getModelLogoById(model.id), [form, agent?.id, agent?.created_at, agent?.updated_at]
providerId: model.provider, )
providerName: model.provider_name
})) satisfies ModelOption[]
}, [models])
const onModelChange = useCallback((e: ChangeEvent<HTMLSelectElement>) => { const handleModelSelect = useCallback(async (model: ApiModel) => {
setForm((prev) => ({ setForm((prev) => ({ ...prev, model: model.id }))
...prev,
model: e.target.value
}))
}, []) }, [])
const onCancel = () => {
setOpen(false)
}
const onClose = () => {
resolve({})
}
const onSubmit = useCallback( const onSubmit = useCallback(
async (e: FormEvent<HTMLFormElement>) => { async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault() e.preventDefault()
@@ -330,9 +299,7 @@ export const AgentModal: React.FC<Props> = ({ agent, isOpen: _isOpen, onClose: _
afterSubmit?.(result.data) afterSubmit?.(result.data)
} }
loadingRef.current = false loadingRef.current = false
setOpen(false)
// setTimeoutTimer('onCreateAgent', () => EventEmitter.emit(EVENT_NAMES.SHOW_ASSISTANTS), 0)
onClose()
}, },
[ [
form.type, form.type,
@@ -344,7 +311,6 @@ export const AgentModal: React.FC<Props> = ({ agent, isOpen: _isOpen, onClose: _
form.allowed_tools, form.allowed_tools,
form.configuration, form.configuration,
agent, agent,
onClose,
t, t,
updateAgent, updateAgent,
afterSubmit, afterSubmit,
@@ -352,138 +318,312 @@ export const AgentModal: React.FC<Props> = ({ agent, isOpen: _isOpen, onClose: _
] ]
) )
AgentModalPopup.hide = onCancel
return ( return (
<ErrorBoundary> <ErrorBoundary>
<Modal <Modal
isOpen={isOpen} title={isEditing(agent) ? t('agent.edit.title') : t('agent.add.title')}
onClose={onClose} open={open}
classNames={{ onCancel={onCancel}
base: 'max-h-[90vh]', afterClose={onClose}
wrapper: 'overflow-hidden' transitionName="animation-move-down"
}}> centered
<ModalContent> width={500}
{(onClose) => ( footer={null}>
<> <StyledForm onSubmit={onSubmit}>
<ModalHeader>{isEditing(agent) ? t('agent.edit.title') : t('agent.add.title')}</ModalHeader> <FormContent>
<Form onSubmit={onSubmit} className="min-h-0 w-full shrink overflow-auto"> <FormRow>
<ModalBody className="min-h-0 w-full flex-1 shrink overflow-auto"> <FormItem style={{ flex: 1 }}>
<div className="flex gap-2"> <Label>{t('agent.type.label')}</Label>
<Select <Select
isRequired value={form.type}
isDisabled={isEditing(agent)} onChange={onAgentTypeChange}
selectionMode="single" options={agentOptions}
selectedKeys={[form.type]} disabled={isEditing(agent)}
disallowEmptySelection style={{ width: '100%' }}
onChange={onAgentTypeChange} />
items={agentOptions} </FormItem>
label={t('agent.type.label')} <FormItem style={{ flex: 1 }}>
placeholder={t('agent.add.type.placeholder')} <Label>
renderValue={renderOption}> {t('common.name')} <RequiredMark>*</RequiredMark>
{(option) => ( </Label>
<SelectItem key={option.key} textValue={option.label}> <Input value={form.name} onChange={onNameChange} required />
<Option option={option} /> </FormItem>
</SelectItem> </FormRow>
)}
</Select> <FormItem>
<Input isRequired value={form.name} onValueChange={onNameChange} label={t('common.name')} /> <Label>
</div> {t('common.model')} <RequiredMark>*</RequiredMark>
<Select </Label>
isRequired <SelectAgentBaseModelButton
selectionMode="single" agentBase={tempAgentBase}
selectedKeys={form.model ? [form.model] : []} onSelect={handleModelSelect}
disallowEmptySelection fontSize={14}
onChange={onModelChange} avatarSize={24}
items={modelOptions} iconSize={16}
label={t('common.model')} buttonStyle={{
placeholder={t('common.placeholders.select.model')} padding: '8px 12px',
renderValue={renderOption}> width: '100%',
{(option) => ( border: '1px solid var(--color-border)',
<SelectItem key={option.key} textValue={option.label}> borderRadius: 6,
<Option option={option} /> height: 'auto'
</SelectItem> }}
)} containerClassName="flex items-center justify-between w-full"
</Select> />
<Select </FormItem>
isRequired
selectionMode="single" <FormItem>
selectedKeys={[selectedPermissionMode]} <Label>
onSelectionChange={onPermissionModeChange} {t('agent.settings.tooling.permissionMode.title', 'Permission mode')} <RequiredMark>*</RequiredMark>
label={t('agent.settings.tooling.permissionMode.title', 'Permission mode')} </Label>
placeholder={t('agent.settings.tooling.permissionMode.placeholder', 'Select permission mode')} <Select
description={t( value={selectedPermissionMode}
'agent.settings.tooling.permissionMode.helper', onChange={onPermissionModeChange}
'Choose how the agent handles tool approvals.' style={{ width: '100%' }}
)} placeholder={t('agent.settings.tooling.permissionMode.placeholder', 'Select permission mode')}
items={permissionModeCards}> dropdownStyle={{ minWidth: '500px' }}
{(item) => ( optionLabelProp="label">
<SelectItem key={item.mode} textValue={t(item.titleKey, item.titleFallback)}> {permissionModeCards.map((item) => (
<div className="flex flex-col gap-1"> <Select.Option key={item.mode} value={item.mode} label={t(item.titleKey, item.titleFallback)}>
<span className="font-medium text-sm">{t(item.titleKey, item.titleFallback)}</span> <PermissionOptionWrapper>
<span className="text-foreground-500 text-xs"> <div className="title">{t(item.titleKey, item.titleFallback)}</div>
{t(item.descriptionKey, item.descriptionFallback)} <div className="description">{t(item.descriptionKey, item.descriptionFallback)}</div>
</span> <div className="behavior">{t(item.behaviorKey, item.behaviorFallback)}</div>
<span className="text-foreground-400 text-xs"> {item.caution && (
{t(item.behaviorKey, item.behaviorFallback)} <div className="caution">
</span> <AlertTriangleIcon size={12} />
{item.caution ? ( {t(
<span className="flex items-center gap-1 text-danger-500 text-xs"> 'agent.settings.tooling.permissionMode.bypassPermissions.warning',
<AlertTriangleIcon size={12} className="text-danger" /> 'Use with caution — all tools will run without asking for approval.'
{t( )}
'agent.settings.tooling.permissionMode.bypassPermissions.warning',
'Use with caution — all tools will run without asking for approval.'
)}
</span>
) : null}
</div> </div>
</SelectItem> )}
)} </PermissionOptionWrapper>
</Select> </Select.Option>
<div className="space-y-2"> ))}
<div className="flex items-center justify-between"> </Select>
<span className="font-medium text-foreground text-sm"> <HelpText>
{t('agent.session.accessible_paths.label')} {t('agent.settings.tooling.permissionMode.helper', 'Choose how the agent handles tool approvals.')}
</span> </HelpText>
<Button size="sm" variant="flat" onPress={addAccessiblePath}> </FormItem>
{t('agent.session.accessible_paths.add')}
<FormItem>
<LabelWithButton>
<Label>
{t('agent.session.accessible_paths.label')} <RequiredMark>*</RequiredMark>
</Label>
<Button size="small" onClick={addAccessiblePath}>
{t('agent.session.accessible_paths.add')}
</Button>
</LabelWithButton>
{form.accessible_paths.length > 0 ? (
<PathList>
{form.accessible_paths.map((path) => (
<PathItem key={path}>
<PathText title={path}>{path}</PathText>
<Button size="small" danger onClick={() => removeAccessiblePath(path)}>
{t('common.delete')}
</Button> </Button>
</div> </PathItem>
{form.accessible_paths.length > 0 ? ( ))}
<div className="space-y-2"> </PathList>
{form.accessible_paths.map((path) => ( ) : (
<div <EmptyText>{t('agent.session.accessible_paths.empty')}</EmptyText>
key={path} )}
className="flex items-center justify-between gap-2 rounded-medium border border-default-200 px-3 py-2"> </FormItem>
<span className="truncate text-sm" title={path}>
{path} <FormItem>
</span> <Label>{t('common.prompt')}</Label>
<Button size="sm" variant="light" color="danger" onPress={() => removeAccessiblePath(path)}> <TextArea rows={3} value={form.instructions ?? ''} onChange={onInstChange} />
{t('common.delete')} </FormItem>
</Button>
</div> <FormItem>
))} <Label>{t('common.description')}</Label>
</div> <TextArea rows={2} value={form.description ?? ''} onChange={onDescChange} />
) : ( </FormItem>
<p className="text-foreground-400 text-sm">{t('agent.session.accessible_paths.empty')}</p> </FormContent>
)}
</div> <FormFooter>
<Textarea label={t('common.prompt')} value={form.instructions ?? ''} onValueChange={onInstChange} /> <Button onClick={onCancel}>{t('common.close')}</Button>
<Textarea <Button type="primary" htmlType="submit" loading={loadingRef.current}>
label={t('common.description')} {isEditing(agent) ? t('common.confirm') : t('common.add')}
value={form.description ?? ''} </Button>
onValueChange={onDescChange} </FormFooter>
/> </StyledForm>
</ModalBody>
<ModalFooter className="w-full">
<Button onPress={onClose}>{t('common.close')}</Button>
<Button color="primary" type="submit" isLoading={loadingRef.current}>
{isEditing(agent) ? t('common.confirm') : t('common.add')}
</Button>
</ModalFooter>
</Form>
</>
)}
</ModalContent>
</Modal> </Modal>
</ErrorBoundary> </ErrorBoundary>
) )
} }
const TopViewKey = 'AgentModalPopup'
export default class AgentModalPopup {
static topviewId = 0
static hide() {
TopView.hide(TopViewKey)
}
static show(props: ShowParams) {
return new Promise<any>((resolve) => {
TopView.show(
<PopupContainer
{...props}
resolve={(v) => {
resolve(v)
TopView.hide(TopViewKey)
}}
/>,
TopViewKey
)
})
}
}
// Keep the old export for backward compatibility during migration
export const AgentModal = AgentModalPopup
const StyledForm = styled.form`
display: flex;
flex-direction: column;
gap: 16px;
`
const FormContent = styled.div`
display: flex;
flex-direction: column;
gap: 16px;
max-height: 60vh;
overflow-y: auto;
padding-right: 8px;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-thumb {
background-color: var(--color-border);
border-radius: 3px;
}
`
const FormRow = styled.div`
display: flex;
gap: 12px;
`
const FormItem = styled.div`
display: flex;
flex-direction: column;
gap: 8px;
`
const Label = styled.label`
font-size: 14px;
color: var(--color-text-1);
font-weight: 500;
`
const RequiredMark = styled.span`
color: #ff4d4f;
margin-left: 4px;
`
const HelpText = styled.div`
font-size: 12px;
color: var(--color-text-3);
`
const LabelWithButton = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
`
const PathList = styled.div`
display: flex;
flex-direction: column;
gap: 8px;
`
const PathItem = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 8px 12px;
border: 1px solid var(--color-border);
border-radius: 6px;
background-color: var(--color-bg-1);
`
const PathText = styled.span`
flex: 1;
font-size: 13px;
color: var(--color-text-2);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`
const EmptyText = styled.p`
font-size: 13px;
color: var(--color-text-3);
margin: 0;
`
const FormFooter = styled.div`
display: flex;
justify-content: flex-end;
gap: 8px;
padding-top: 16px;
border-top: 1px solid var(--color-border);
`
const OptionWrapper = styled.div`
display: flex;
align-items: center;
gap: 8px;
`
const PermissionOptionWrapper = styled.div`
display: flex;
flex-direction: column;
gap: 6px;
padding: 8px 0;
.title {
font-size: 14px;
font-weight: 600;
color: var(--color-text-1);
margin-bottom: 2px;
}
.description {
font-size: 12px;
color: var(--color-text-2);
line-height: 1.4;
}
.behavior {
font-size: 12px;
color: var(--color-text-3);
line-height: 1.4;
}
.caution {
display: flex;
align-items: flex-start;
gap: 6px;
font-size: 12px;
color: #ff4d4f;
margin-top: 4px;
padding: 6px 8px;
background-color: rgba(255, 77, 79, 0.1);
border-radius: 4px;
svg {
flex-shrink: 0;
margin-top: 2px;
}
}
`
@@ -1,320 +0,0 @@
import {
Button,
cn,
Form,
Input,
Modal,
ModalBody,
ModalContent,
ModalFooter,
ModalHeader,
Textarea,
useDisclosure
} from '@heroui/react'
import { loggerService } from '@logger'
import type { Selection } from '@react-types/shared'
import { AllowedToolsSelect } from '@renderer/components/agent'
import { useAgent } from '@renderer/hooks/agents/useAgent'
import { useSessions } from '@renderer/hooks/agents/useSessions'
import { useUpdateSession } from '@renderer/hooks/agents/useUpdateSession'
import type {
AgentEntity,
AgentSessionEntity,
BaseSessionForm,
CreateSessionForm,
Tool,
UpdateSessionForm
} from '@renderer/types'
import type { FormEvent, ReactNode } from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { ErrorBoundary } from '../../ErrorBoundary'
const logger = loggerService.withContext('SessionAgentPopup')
type AgentWithTools = AgentEntity & { tools?: Tool[] }
type SessionWithTools = AgentSessionEntity & { tools?: Tool[] }
const buildSessionForm = (existing?: SessionWithTools, agent?: AgentWithTools): BaseSessionForm => ({
name: existing?.name ?? agent?.name ?? 'Claude Code',
description: existing?.description ?? agent?.description,
instructions: existing?.instructions ?? agent?.instructions,
model: existing?.model ?? agent?.model ?? '',
accessible_paths: existing?.accessible_paths
? [...existing.accessible_paths]
: agent?.accessible_paths
? [...agent.accessible_paths]
: [],
allowed_tools: existing?.allowed_tools
? [...existing.allowed_tools]
: agent?.allowed_tools
? [...agent.allowed_tools]
: [],
mcps: existing?.mcps ? [...existing.mcps] : agent?.mcps ? [...agent.mcps] : []
})
interface BaseProps {
agentId: string
session?: SessionWithTools
onSessionCreated?: (session: AgentSessionEntity) => void
}
interface TriggerProps extends BaseProps {
trigger: { content: ReactNode; className?: string }
isOpen?: never
onClose?: never
}
interface StateProps extends BaseProps {
trigger?: never
isOpen: boolean
onClose: () => void
}
type Props = TriggerProps | StateProps
/**
* Modal component for creating or editing a Session.
* @deprecated may as a reference when migrating to v2
*
* Either trigger or isOpen and onClose is given.
* @param agentId - The ID of agent which the session is related.
* @param session - Optional session entity for editing mode.
* @param trigger - Optional trigger element that opens the modal. It MUST propagate the click event to trigger the modal.
* @param isOpen - Optional controlled modal open state. From useDisclosure.
* @param onClose - Optional callback when modal closes. From useDisclosure.
* @returns Modal component for agent creation/editing
*/
export const SessionModal: React.FC<Props> = ({
agentId,
session,
trigger,
isOpen: _isOpen,
onClose: _onClose,
onSessionCreated
}) => {
const { isOpen, onClose, onOpen } = useDisclosure({ isOpen: _isOpen, onClose: _onClose })
const { t } = useTranslation()
const loadingRef = useRef(false)
// const { setTimeoutTimer } = useTimer()
const { createSession } = useSessions(agentId)
const { updateSession } = useUpdateSession(agentId)
const { agent } = useAgent(agentId)
const isEditing = (session?: AgentSessionEntity) => session !== undefined
const [form, setForm] = useState<BaseSessionForm>(() => buildSessionForm(session, agent ?? undefined))
useEffect(() => {
if (isOpen) {
setForm(buildSessionForm(session, agent ?? undefined))
}
}, [session, agent, isOpen])
const availableTools = useMemo(() => session?.tools ?? agent?.tools ?? [], [agent?.tools, session?.tools])
const selectedToolKeys = useMemo(() => new Set(form.allowed_tools ?? []), [form.allowed_tools])
useEffect(() => {
if (!availableTools.length) {
return
}
setForm((prev) => {
const allowed = prev.allowed_tools ?? []
const validTools = allowed.filter((id) => availableTools.some((tool) => tool.id === id))
if (validTools.length === allowed.length) {
return prev
}
return {
...prev,
allowed_tools: validTools
}
})
}, [availableTools])
const onNameChange = useCallback((name: string) => {
setForm((prev) => ({
...prev,
name
}))
}, [])
const onDescChange = useCallback((description: string) => {
setForm((prev) => ({
...prev,
description
}))
}, [])
const onInstChange = useCallback((instructions: string) => {
setForm((prev) => ({
...prev,
instructions
}))
}, [])
const onAllowedToolsChange = useCallback(
(keys: Selection) => {
setForm((prev) => {
const existing = prev.allowed_tools ?? []
if (keys === 'all') {
return {
...prev,
allowed_tools: availableTools.map((tool) => tool.id)
}
}
const next = Array.from(keys).map(String)
const filtered = availableTools.length
? next.filter((id) => availableTools.some((tool) => tool.id === id))
: next
if (existing.length === filtered.length && existing.every((id) => filtered.includes(id))) {
return prev
}
return {
...prev,
allowed_tools: filtered
}
})
},
[availableTools]
)
const onSubmit = useCallback(
async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
if (loadingRef.current) {
return
}
loadingRef.current = true
// Additional validation check besides native HTML validation to ensure security
if (!form.model) {
window.toast.error(t('error.model.not_exists'))
loadingRef.current = false
return
}
if (form.accessible_paths.length === 0) {
window.toast.error(t('agent.session.accessible_paths.error.at_least_one'))
loadingRef.current = false
return
}
try {
if (isEditing(session)) {
if (!session) {
throw new Error('Agent is required for editing mode')
}
const updatePayload = {
id: session.id,
name: form.name,
description: form.description,
instructions: form.instructions,
model: form.model,
accessible_paths: [...form.accessible_paths],
allowed_tools: [...(form.allowed_tools ?? [])],
mcps: [...(form.mcps ?? [])]
} satisfies UpdateSessionForm
updateSession(updatePayload)
logger.debug('Updated agent', updatePayload)
} else {
const newSession = {
name: form.name,
description: form.description,
instructions: form.instructions,
model: form.model,
accessible_paths: [...form.accessible_paths],
allowed_tools: [...(form.allowed_tools ?? [])],
mcps: [...(form.mcps ?? [])]
} satisfies CreateSessionForm
const createdSession = await createSession(newSession)
if (createdSession) {
onSessionCreated?.(createdSession)
}
logger.debug('Added agent', newSession)
}
// setTimeoutTimer('onCreateAgent', () => EventEmitter.emit(EVENT_NAMES.SHOW_ASSISTANTS), 0)
onClose()
} finally {
loadingRef.current = false
}
},
[
form.model,
form.name,
form.description,
form.instructions,
form.accessible_paths,
form.allowed_tools,
form.mcps,
session,
onClose,
onSessionCreated,
t,
updateSession,
createSession
]
)
return (
<ErrorBoundary>
{/* NOTE: Hero UI Modal Pattern: Combine the Button and Modal components into a single
encapsulated component. This is because the Modal component needs to bind the onOpen
event handler to the Button for proper focus management.
Or just use external isOpen/onOpen/onClose to control modal state.
*/}
{trigger && (
<div
onClick={(e) => {
e.stopPropagation()
onOpen()
}}
className={cn('w-full', trigger.className)}>
{trigger.content}
</div>
)}
<Modal isOpen={isOpen} onClose={onClose}>
<ModalContent>
{(onClose) => (
<>
<ModalHeader>
{isEditing(session) ? t('agent.session.edit.title') : t('agent.session.add.title')}
</ModalHeader>
<Form onSubmit={onSubmit} className="w-full">
<ModalBody className="w-full">
<Input isRequired value={form.name} onValueChange={onNameChange} label={t('common.name')} />
<Textarea
label={t('common.description')}
value={form.description ?? ''}
onValueChange={onDescChange}
/>
<AllowedToolsSelect
items={availableTools}
selectedKeys={selectedToolKeys}
onSelectionChange={onAllowedToolsChange}
/>
<Textarea label={t('common.prompt')} value={form.instructions ?? ''} onValueChange={onInstChange} />
</ModalBody>
<ModalFooter className="w-full">
<Button onPress={onClose}>{t('common.close')}</Button>
<Button color="primary" type="submit" isLoading={loadingRef.current}>
{isEditing(session) ? t('common.confirm') : t('common.add')}
</Button>
</ModalFooter>
</Form>
</>
)}
</ModalContent>
</Modal>
</ErrorBoundary>
)
}
@@ -1,8 +1,3 @@
import type { SelectedItemProps, SelectedItems } from '@heroui/react'
import { Avatar } from '@heroui/react'
import { getProviderLabel } from '@renderer/i18n/label'
import { useTranslation } from 'react-i18next'
export interface BaseOption { export interface BaseOption {
type: 'type' | 'model' type: 'type' | 'model'
key: string key: string
@@ -10,43 +5,3 @@ export interface BaseOption {
// img src // img src
avatar?: string avatar?: string
} }
export interface ModelOption extends BaseOption {
providerId?: string
providerName?: string
}
export function isModelOption(option: BaseOption): option is ModelOption {
return option.type === 'model'
}
export const Item = ({ item }: { item: SelectedItemProps<BaseOption> }) => <Option option={item.data} />
export const renderOption = (items: SelectedItems<BaseOption>) =>
items.map((item) => <Item key={item.key} item={item} />)
export const Option = ({ option }: { option?: BaseOption | null }) => {
const { t } = useTranslation()
if (!option) {
return (
<div className="flex gap-2">
<Avatar name="?" className="h-5 w-5" />
{t('common.invalid_value')}
</div>
)
}
const providerLabel = (() => {
if (!isModelOption(option)) return null
if (option.providerName) return option.providerName
if (option.providerId) return getProviderLabel(option.providerId)
return null
})()
return (
<div className="flex gap-2">
<Avatar src={option.avatar} className="h-5 w-5" />
<span className="truncate">{option.label}</span>
{providerLabel ? <span className="truncate text-foreground-500">| {providerLabel}</span> : null}
</div>
)
}
@@ -64,6 +64,7 @@ export type QuickPanelListItem = {
isSelected?: boolean isSelected?: boolean
isMenu?: boolean isMenu?: boolean
disabled?: boolean disabled?: boolean
hidden?: boolean
/** /**
* *
* alwaysVisible * alwaysVisible
@@ -143,7 +143,8 @@ export const QuickPanelView: React.FC<Props> = ({ setInputText }) => {
prevSymbolRef.current = ctx.symbol prevSymbolRef.current = ctx.symbol
// 固定项置顶 + 过滤后的普通项 // 固定项置顶 + 过滤后的普通项
return [...pinnedItems, ...filteredNormalItems] const pinnedFiltered = [...pinnedItems, ...filteredNormalItems]
return pinnedFiltered.filter((item) => !item.hidden)
}, [ctx.isVisible, ctx.symbol, ctx.list, searchText]) }, [ctx.isVisible, ctx.symbol, ctx.list, searchText])
const canForwardAndBackward = useMemo(() => { const canForwardAndBackward = useMemo(() => {
@@ -1,32 +0,0 @@
import { ToastProvider } from '@heroui/toast'
import { useEffect, useState } from 'react'
import { createPortal } from 'react-dom'
export const ToastPortal = () => {
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
return () => setMounted(false)
}, [])
if (!mounted) return null
return createPortal(
<ToastProvider
placement="top-center"
regionProps={{
className: 'z-[1001]'
}}
toastOffset={20}
toastProps={{
timeout: 3000,
classNames: {
// This setting causes the 'hero-toast' class to be applied twice to the toast element. This is weird and I don't know why, but it works.
base: 'hero-toast'
}
}}
/>,
document.body
)
}
@@ -2,12 +2,12 @@
import TopViewMinappContainer from '@renderer/components/MinApp/TopViewMinappContainer' import TopViewMinappContainer from '@renderer/components/MinApp/TopViewMinappContainer'
import { useAppInit } from '@renderer/hooks/useAppInit' import { useAppInit } from '@renderer/hooks/useAppInit'
import { useShortcuts } from '@renderer/hooks/useShortcuts' import { useShortcuts } from '@renderer/hooks/useShortcuts'
import { Modal } from 'antd' import { message, Modal } from 'antd'
import type { PropsWithChildren } from 'react' import type { PropsWithChildren } from 'react'
import React, { useCallback, useEffect, useRef, useState } from 'react' import React, { useCallback, useEffect, useRef, useState } from 'react'
import { Box } from '../Layout' import { Box } from '../Layout'
import { getToastUtilities } from './toast' import { getToastUtilities, initMessageApi } from './toast'
let onPop = () => {} let onPop = () => {}
let onShow = ({ element, id }: { element: React.FC | React.ReactNode; id: string }) => { let onShow = ({ element, id }: { element: React.FC | React.ReactNode; id: string }) => {
@@ -36,6 +36,7 @@ const TopViewContainer: React.FC<Props> = ({ children }) => {
elementsRef.current = elements elementsRef.current = elements
const [modal, modalContextHolder] = Modal.useModal() const [modal, modalContextHolder] = Modal.useModal()
const [messageApi, messageContextHolder] = message.useMessage()
const { shortcuts } = useShortcuts() const { shortcuts } = useShortcuts()
const enableQuitFullScreen = shortcuts.find((item) => item.key === 'exit_fullscreen')?.enabled const enableQuitFullScreen = shortcuts.find((item) => item.key === 'exit_fullscreen')?.enabled
@@ -43,8 +44,9 @@ const TopViewContainer: React.FC<Props> = ({ children }) => {
useEffect(() => { useEffect(() => {
window.modal = modal window.modal = modal
initMessageApi(messageApi)
window.toast = getToastUtilities() window.toast = getToastUtilities()
}, [modal]) }, [messageApi, modal])
onPop = () => { onPop = () => {
const views = [...elementsRef.current] const views = [...elementsRef.current]
@@ -97,6 +99,7 @@ const TopViewContainer: React.FC<Props> = ({ children }) => {
return ( return (
<> <>
{children} {children}
{messageContextHolder}
{modalContextHolder} {modalContextHolder}
<TopViewMinappContainer /> <TopViewMinappContainer />
{elements.map(({ element: Element, id }) => ( {elements.map(({ element: Element, id }) => (
@@ -1,72 +0,0 @@
import { addToast, closeAll, closeToast, getToastQueue, isToastClosing } from '@heroui/toast'
import type { RequireSome } from '@renderer/types'
type AddToastProps = Parameters<typeof addToast>[0]
type ToastPropsColored = Omit<AddToastProps, 'color'>
const createToast = (color: 'danger' | 'success' | 'warning' | 'default') => {
return (arg: ToastPropsColored | string): string | null => {
if (typeof arg === 'string') {
return addToast({ color, title: arg })
} else {
return addToast({ color, ...arg })
}
}
}
// syntatic sugar, oh yeah
/**
* Display an error toast notification with red color
* @param arg - Toast content (string) or toast options object
* @returns Toast ID or null
*/
export const error = createToast('danger')
/**
* Display a success toast notification with green color
* @param arg - Toast content (string) or toast options object
* @returns Toast ID or null
*/
export const success = createToast('success')
/**
* Display a warning toast notification with yellow color
* @param arg - Toast content (string) or toast options object
* @returns Toast ID or null
*/
export const warning = createToast('warning')
/**
* Display an info toast notification with default color
* @param arg - Toast content (string) or toast options object
* @returns Toast ID or null
*/
export const info = createToast('default')
/**
* Display a loading toast notification that resolves with a promise
* @param args - Toast options object containing a promise to resolve
* @returns Toast ID or null
*/
export const loading = (args: RequireSome<AddToastProps, 'promise'>) => {
// Disappear immediately by default
if (args.timeout === undefined) {
args.timeout = 1
}
return addToast(args)
}
export const getToastUtilities = () =>
({
getToastQueue,
addToast,
closeToast,
closeAll,
isToastClosing,
error,
success,
warning,
info,
loading
}) as const
@@ -0,0 +1,231 @@
import type { RequireSome } from '@renderer/types'
import { message as antdMessage } from 'antd'
import type { MessageInstance } from 'antd/es/message/interface'
import type React from 'react'
// Global message instance for static usage
let messageApi: MessageInstance | null = null
// Initialize message API - should be called once the App component is mounted
export const initMessageApi = (api: MessageInstance) => {
messageApi = api
}
// Get message API instance
const getMessageApi = (): MessageInstance => {
if (!messageApi) {
// Fallback to static method if hook API is not available
return antdMessage
}
return messageApi
}
type ToastColor = 'danger' | 'success' | 'warning' | 'default'
type MessageType = 'error' | 'success' | 'warning' | 'info'
interface ToastConfig {
title?: React.ReactNode
icon?: React.ReactNode
description?: React.ReactNode
timeout?: number
key?: string | number
className?: string
style?: React.CSSProperties
onClick?: () => void
onClose?: () => void
}
interface LoadingToastConfig extends ToastConfig {
promise: Promise<any>
}
const colorToType = (color: ToastColor): MessageType => {
switch (color) {
case 'danger':
return 'error'
case 'success':
return 'success'
case 'warning':
return 'warning'
case 'default':
return 'info'
}
}
// Toast content component
const ToastContent: React.FC<{ title?: React.ReactNode; description?: React.ReactNode; icon?: React.ReactNode }> = ({
title,
description,
icon
}) => {
return (
<div className="flex flex-col gap-1">
{(icon || title) && (
<div className="flex items-center gap-2 font-semibold">
{icon}
{title}
</div>
)}
{description && <div className="text-sm">{description}</div>}
</div>
)
}
const createToast = (color: ToastColor) => {
return (arg: ToastConfig | string): string | null => {
const api = getMessageApi()
const type = colorToType(color) as 'error' | 'success' | 'warning' | 'info'
if (typeof arg === 'string') {
// antd message methods return a function to close the message
api[type](arg)
return null
}
const { title, description, icon, timeout, ...restConfig } = arg
// Convert timeout from milliseconds to seconds (antd uses seconds)
const duration = timeout !== undefined ? timeout / 1000 : 3
return (
(api.open({
type: type as 'error' | 'success' | 'warning' | 'info',
content: <ToastContent title={title} description={description} icon={icon} />,
duration,
...restConfig
}) as any) || null
)
}
}
/**
* Display an error toast notification with red color
* @param arg - Toast content (string) or toast options object
* @returns Toast ID or null
*/
export const error = createToast('danger')
/**
* Display a success toast notification with green color
* @param arg - Toast content (string) or toast options object
* @returns Toast ID or null
*/
export const success = createToast('success')
/**
* Display a warning toast notification with yellow color
* @param arg - Toast content (string) or toast options object
* @returns Toast ID or null
*/
export const warning = createToast('warning')
/**
* Display an info toast notification with default color
* @param arg - Toast content (string) or toast options object
* @returns Toast ID or null
*/
export const info = createToast('default')
/**
* Display a loading toast notification that resolves with a promise
* @param args - Toast options object containing a promise to resolve
*/
export const loading = (args: RequireSome<LoadingToastConfig, 'promise'>): string | null => {
const api = getMessageApi()
const { title, description, icon, promise, timeout, ...restConfig } = args
// Generate unique key for this loading message
const key = args.key || `loading-${Date.now()}-${Math.random()}`
// Show loading message
api.loading({
content: <ToastContent title={title || 'Loading...'} description={description} icon={icon} />,
duration: 0, // Don't auto-close
key,
...restConfig
})
// Handle promise resolution
promise
.then((result) => {
api.success({
content: <ToastContent title={title || 'Success'} description={description} />,
duration: timeout !== undefined ? timeout / 1000 : 2,
key,
...restConfig
})
return result
})
.catch((err) => {
api.error({
content: (
<ToastContent title={title || 'Error'} description={err?.message || description || 'An error occurred'} />
),
duration: timeout !== undefined ? timeout / 1000 : 3,
key,
...restConfig
})
throw err
})
return key as string
}
/**
* Add a toast notification
* @param config - Toast configuration object
* @returns Toast ID or null
*/
export const addToast = (config: ToastConfig) => info(config)
/**
* Close a specific toast notification by its key
* @param key - Toast key (string)
*/
export const closeToast = (key: string) => {
getMessageApi().destroy(key)
}
/**
* Close all toast notifications
*/
export const closeAll = () => {
getMessageApi().destroy()
}
/**
* Stub functions for compatibility with previous toast API
* These are no-ops since antd message doesn't expose a queue
*/
/**
* @deprecated This function is a no-op stub for backward compatibility only.
* Antd message doesn't expose a queue. Do not rely on this function.
* @returns Empty toast queue stub
*/
export const getToastQueue = (): any => ({ toasts: [] })
/**
* @deprecated This function is a no-op stub for backward compatibility only.
* Antd message doesn't track closing state. Do not rely on this function.
* @param key - Toast key (unused)
* @returns Always returns false
*/
export const isToastClosing = (key?: string): boolean => {
key // unused
return false
}
export const getToastUtilities = () =>
({
getToastQueue,
addToast,
closeToast,
closeAll,
isToastClosing,
error,
success,
warning,
info,
loading
}) as const
@@ -1,101 +0,0 @@
import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader, ScrollShadow } from '@heroui/react'
import { loggerService } from '@logger'
import { handleSaveData } from '@renderer/store'
import type { ReleaseNoteInfo, UpdateInfo } from 'builder-util-runtime'
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Markdown from 'react-markdown'
const logger = loggerService.withContext('UpdateDialog')
interface UpdateDialogProps {
isOpen: boolean
onClose: () => void
releaseInfo: UpdateInfo | null
}
const UpdateDialog: React.FC<UpdateDialogProps> = ({ isOpen, onClose, releaseInfo }) => {
const { t } = useTranslation()
const [isInstalling, setIsInstalling] = useState(false)
useEffect(() => {
if (isOpen && releaseInfo) {
logger.info('Update dialog opened', { version: releaseInfo.version })
}
}, [isOpen, releaseInfo])
const handleInstall = async () => {
setIsInstalling(true)
try {
await handleSaveData()
await window.api.quitAndInstall()
} catch (error) {
logger.error('Failed to save data before update', error as Error)
setIsInstalling(false)
window.toast.error(t('update.saveDataError'))
}
}
const releaseNotes = releaseInfo?.releaseNotes
return (
<Modal
isOpen={isOpen}
onClose={onClose}
size="2xl"
scrollBehavior="inside"
classNames={{
base: 'max-h-[85vh]',
header: 'border-b border-divider',
footer: 'border-t border-divider'
}}>
<ModalContent>
{(onModalClose) => (
<>
<ModalHeader className="flex flex-col gap-1">
<h3 className="font-semibold text-lg">{t('update.title')}</h3>
<p className="text-default-500 text-small">
{t('update.message').replace('{{version}}', releaseInfo?.version || '')}
</p>
</ModalHeader>
<ModalBody>
<ScrollShadow className="max-h-[450px]" hideScrollBar>
<div className="markdown rounded-lg bg-default-50 p-4">
<Markdown>
{typeof releaseNotes === 'string'
? releaseNotes
: Array.isArray(releaseNotes)
? releaseNotes
.map((note: ReleaseNoteInfo) => note.note)
.filter(Boolean)
.join('\n\n')
: t('update.noReleaseNotes')}
</Markdown>
</div>
</ScrollShadow>
</ModalBody>
<ModalFooter>
<Button variant="light" onPress={onModalClose} isDisabled={isInstalling}>
{t('update.later')}
</Button>
<Button
color="primary"
onPress={async () => {
await handleInstall()
onModalClose()
}}
isLoading={isInstalling}>
{t('update.install')}
</Button>
</ModalFooter>
</>
)}
</ModalContent>
</Modal>
)
}
export default UpdateDialog
@@ -1,55 +0,0 @@
import type { SelectedItems, SelectProps } from '@heroui/react'
import { Chip, cn, Select, SelectItem } from '@heroui/react'
import type { Tool } from '@renderer/types'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
export interface AllowedToolsSelectProps extends Omit<SelectProps, 'children'> {
items: Tool[]
}
export const AllowedToolsSelect: React.FC<AllowedToolsSelectProps> = (props) => {
const { t } = useTranslation()
const { items: availableTools, className, ...rest } = props
const renderSelectedTools = useCallback((items: SelectedItems<Tool>) => {
if (!items.length) {
return null
}
return (
<div className="flex flex-wrap gap-2">
{items.map((item) => (
<Chip key={item.key} size="sm" variant="flat" className="max-w-[160px] truncate">
{item.data?.name ?? item.textValue ?? item.key}
</Chip>
))}
</div>
)
}, [])
return (
<Select
aria-label={t('agent.session.allowed_tools.label')}
selectionMode="multiple"
isMultiline
label={t('agent.session.allowed_tools.label')}
placeholder={t('agent.session.allowed_tools.placeholder')}
description={
availableTools.length ? t('agent.session.allowed_tools.helper') : t('agent.session.allowed_tools.empty')
}
isDisabled={!availableTools.length}
items={availableTools}
renderValue={renderSelectedTools}
className={cn('max-w-xl', className)}
{...rest}>
{(tool) => (
<SelectItem key={tool.id} textValue={tool.name}>
<div className="flex flex-col">
<span className="font-medium text-sm">{tool.name}</span>
{tool.description ? <span className="text-foreground-500 text-xs">{tool.description}</span> : null}
</div>
</SelectItem>
)}
</Select>
)
}
@@ -1 +0,0 @@
export { AllowedToolsSelect } from './AllowedToolsSelect'
@@ -27,6 +27,7 @@ export const SYSTEM_MODELS: Record<SystemProviderId | 'defaultModel', Model[]> =
], ],
cherryin: [], cherryin: [],
vertexai: [], vertexai: [],
sophnet: [],
'302ai': [ '302ai': [
{ {
id: 'deepseek-chat', id: 'deepseek-chat',
+33 -2
View File
@@ -46,6 +46,7 @@ import Ph8ProviderLogo from '@renderer/assets/images/providers/ph8.png'
import PPIOProviderLogo from '@renderer/assets/images/providers/ppio.png' import PPIOProviderLogo from '@renderer/assets/images/providers/ppio.png'
import QiniuProviderLogo from '@renderer/assets/images/providers/qiniu.webp' import QiniuProviderLogo from '@renderer/assets/images/providers/qiniu.webp'
import SiliconFlowProviderLogo from '@renderer/assets/images/providers/silicon.png' import SiliconFlowProviderLogo from '@renderer/assets/images/providers/silicon.png'
import SophnetProviderLogo from '@renderer/assets/images/providers/sophnet.svg'
import StepProviderLogo from '@renderer/assets/images/providers/step.png' import StepProviderLogo from '@renderer/assets/images/providers/step.png'
import TencentCloudProviderLogo from '@renderer/assets/images/providers/tencent-cloud-ti.png' import TencentCloudProviderLogo from '@renderer/assets/images/providers/tencent-cloud-ti.png'
import TogetherProviderLogo from '@renderer/assets/images/providers/together.png' import TogetherProviderLogo from '@renderer/assets/images/providers/together.png'
@@ -246,6 +247,16 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
isSystem: true, isSystem: true,
enabled: false enabled: false
}, },
sophnet: {
id: 'sophnet',
name: 'SophNet',
type: 'openai',
apiKey: '',
apiHost: 'https://www.sophnet.com/api/open-apis/v1',
models: [],
isSystem: true,
enabled: false
},
ppio: { ppio: {
id: 'ppio', id: 'ppio',
name: 'PPIO', name: 'PPIO',
@@ -729,7 +740,8 @@ export const PROVIDER_LOGO_MAP: AtLeast<SystemProviderId, string> = {
poe: 'poe', // use svg icon component poe: 'poe', // use svg icon component
aionly: AiOnlyProviderLogo, aionly: AiOnlyProviderLogo,
longcat: LongCatProviderLogo, longcat: LongCatProviderLogo,
huggingface: HuggingfaceProviderLogo huggingface: HuggingfaceProviderLogo,
sophnet: SophnetProviderLogo
} as const } as const
export function getProviderLogo(providerId: string) { export function getProviderLogo(providerId: string) {
@@ -808,6 +820,17 @@ export const PROVIDER_URLS: Record<SystemProviderId, ProviderUrls> = {
models: 'https://ai.burncloud.com/pricing' models: 'https://ai.burncloud.com/pricing'
} }
}, },
sophnet: {
api: {
url: 'https://www.sophnet.com/api/open-apis/v1'
},
websites: {
official: 'https://sophnet.com',
apiKey: 'https://sophnet.com/#/project/key',
docs: 'https://sophnet.com/docs/component/introduce.html',
models: 'https://sophnet.com/#/model/list'
}
},
ppio: { ppio: {
api: { api: {
url: 'https://api.ppinfra.com/v3/openai' url: 'https://api.ppinfra.com/v3/openai'
@@ -1463,6 +1486,14 @@ export const isNewApiProvider = (provider: Provider) => {
return ['new-api', 'cherryin'].includes(provider.id) || provider.type === 'new-api' return ['new-api', 'cherryin'].includes(provider.id) || provider.type === 'new-api'
} }
export function isCherryAIProvider(provider: Provider): boolean {
return provider.id === 'cherryai'
}
export function isPerplexityProvider(provider: Provider): boolean {
return provider.id === 'perplexity'
}
/** /**
* OpenAI * OpenAI
* @param {Provider} provider * @param {Provider} provider
@@ -1488,7 +1519,7 @@ export function isGeminiProvider(provider: Provider): boolean {
return provider.type === 'gemini' return provider.type === 'gemini'
} }
const NOT_SUPPORT_API_VERSION_PROVIDERS = ['github', 'copilot'] as const satisfies SystemProviderId[] const NOT_SUPPORT_API_VERSION_PROVIDERS = ['github', 'copilot', 'perplexity'] as const satisfies SystemProviderId[]
export const isSupportAPIVersionProvider = (provider: Provider) => { export const isSupportAPIVersionProvider = (provider: Provider) => {
if (isSystemProvider(provider)) { if (isSystemProvider(provider)) {
@@ -1,13 +0,0 @@
import { HeroUIProvider } from '@heroui/react'
import { useSettings } from '@renderer/hooks/useSettings'
const AppHeroUIProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const { language } = useSettings()
return (
<HeroUIProvider className="flex h-full w-full flex-1" locale={language}>
{children}
</HeroUIProvider>
)
}
export { AppHeroUIProvider as HeroUIProvider }
+12 -2
View File
@@ -1,12 +1,22 @@
/// <reference types="vite/client" /> /// <reference types="vite/client" />
import type { PermissionUpdate } from '@anthropic-ai/claude-agent-sdk' import type { PermissionUpdate } from '@anthropic-ai/claude-agent-sdk'
import type { addToast, closeAll, closeToast, getToastQueue, isToastClosing } from '@heroui/toast'
import type KeyvStorage from '@kangfenmao/keyv-storage' import type KeyvStorage from '@kangfenmao/keyv-storage'
import type { HookAPI } from 'antd/es/modal/useModal' import type { HookAPI } from 'antd/es/modal/useModal'
import type { NavigateFunction } from 'react-router-dom' import type { NavigateFunction } from 'react-router-dom'
import type { error, info, loading, success, warning } from './components/TopView/toast' import type {
addToast,
closeAll,
closeToast,
error,
getToastQueue,
info,
isToastClosing,
loading,
success,
warning
} from './components/TopView/toast'
interface ImportMetaEnv { interface ImportMetaEnv {
VITE_RENDERER_INTEGRATED_MODEL: string VITE_RENDERER_INTEGRATED_MODEL: string
-2
View File
@@ -1,2 +0,0 @@
import { heroui } from '@heroui/react'
export default heroui()
@@ -41,6 +41,7 @@ export const useAgents = () => {
// NOTE: We only use the array for now. useUpdateAgent depends on this behavior. // NOTE: We only use the array for now. useUpdateAgent depends on this behavior.
return result.data return result.data
}, [apiServerConfig.enabled, apiServerRunning, client, t]) }, [apiServerConfig.enabled, apiServerRunning, client, t])
const { data, error, isLoading, mutate } = useSWR(swrKey, fetcher) const { data, error, isLoading, mutate } = useSWR(swrKey, fetcher)
const { chat } = useRuntime() const { chat } = useRuntime()
const { activeAgentId } = chat const { activeAgentId } = chat
+10 -7
View File
@@ -31,21 +31,24 @@ export const useApiServer = () => {
try { try {
const status = await window.api.apiServer.getStatus() const status = await window.api.apiServer.getStatus()
setApiServerRunning(status.running) setApiServerRunning(status.running)
if (status.running && !apiServerConfig.enabled) {
setApiServerEnabled(true)
}
} catch (error: any) { } catch (error: any) {
logger.error('Failed to check API server status:', error) logger.error('Failed to check API server status:', error)
} finally { } finally {
setApiServerLoading(false) setApiServerLoading(false)
} }
}, []) }, [apiServerConfig.enabled, setApiServerEnabled])
const startApiServer = useCallback(async () => { const startApiServer = useCallback(async () => {
if (apiServerLoading) return if (apiServerLoading) return
setApiServerLoading(true) setApiServerLoading(true)
try { try {
const result = await window.api.apiServer.start() const result = await window.api.apiServer.start()
if (result.success) { if (result.success) {
setApiServerRunning(true) setApiServerRunning(true)
setApiServerEnabled(true)
window.toast.success(t('apiServer.messages.startSuccess')) window.toast.success(t('apiServer.messages.startSuccess'))
} else { } else {
window.toast.error(t('apiServer.messages.startError') + result.error) window.toast.error(t('apiServer.messages.startError') + result.error)
@@ -55,16 +58,16 @@ export const useApiServer = () => {
} finally { } finally {
setApiServerLoading(false) setApiServerLoading(false)
} }
}, [apiServerLoading, t]) }, [apiServerLoading, setApiServerEnabled, t])
const stopApiServer = useCallback(async () => { const stopApiServer = useCallback(async () => {
if (apiServerLoading) return if (apiServerLoading) return
setApiServerLoading(true) setApiServerLoading(true)
try { try {
const result = await window.api.apiServer.stop() const result = await window.api.apiServer.stop()
if (result.success) { if (result.success) {
setApiServerRunning(false) setApiServerRunning(false)
setApiServerEnabled(false)
window.toast.success(t('apiServer.messages.stopSuccess')) window.toast.success(t('apiServer.messages.stopSuccess'))
} else { } else {
window.toast.error(t('apiServer.messages.stopError') + result.error) window.toast.error(t('apiServer.messages.stopError') + result.error)
@@ -74,14 +77,14 @@ export const useApiServer = () => {
} finally { } finally {
setApiServerLoading(false) setApiServerLoading(false)
} }
}, [apiServerLoading, t]) }, [apiServerLoading, setApiServerEnabled, t])
const restartApiServer = useCallback(async () => { const restartApiServer = useCallback(async () => {
if (apiServerLoading) return if (apiServerLoading) return
setApiServerLoading(true) setApiServerLoading(true)
try { try {
const result = await window.api.apiServer.restart() const result = await window.api.apiServer.restart()
setApiServerEnabled(result.success)
if (result.success) { if (result.success) {
await checkApiServerStatus() await checkApiServerStatus()
window.toast.success(t('apiServer.messages.restartSuccess')) window.toast.success(t('apiServer.messages.restartSuccess'))
@@ -93,7 +96,7 @@ export const useApiServer = () => {
} finally { } finally {
setApiServerLoading(false) setApiServerLoading(false)
} }
}, [apiServerLoading, checkApiServerStatus, t]) }, [apiServerLoading, checkApiServerStatus, setApiServerEnabled, t])
useEffect(() => { useEffect(() => {
checkApiServerStatus() checkApiServerStatus()
+5 -6
View File
@@ -221,13 +221,12 @@ export function useAppInit() {
} }
} }
window.electron.ipcRenderer.on(IpcChannel.AgentToolPermission_Request, requestListener) const removeListeners = [
window.electron.ipcRenderer.on(IpcChannel.AgentToolPermission_Result, resultListener) window.electron.ipcRenderer.on(IpcChannel.AgentToolPermission_Request, requestListener),
window.electron.ipcRenderer.on(IpcChannel.AgentToolPermission_Result, resultListener)
]
return () => { return () => removeListeners.forEach((removeListener) => removeListener())
window.electron?.ipcRenderer.removeListener(IpcChannel.AgentToolPermission_Request, requestListener)
window.electron?.ipcRenderer.removeListener(IpcChannel.AgentToolPermission_Result, resultListener)
}
}, [dispatch, t]) }, [dispatch, t])
useEffect(() => { useEffect(() => {
+18 -1
View File
@@ -1,5 +1,12 @@
import store, { useAppSelector } from '@renderer/store' import store, { useAppSelector } from '@renderer/store'
import { setAwsBedrockAccessKeyId, setAwsBedrockRegion, setAwsBedrockSecretAccessKey } from '@renderer/store/llm' import {
setAwsBedrockAccessKeyId,
setAwsBedrockApiKey,
setAwsBedrockAuthType,
setAwsBedrockRegion,
setAwsBedrockSecretAccessKey
} from '@renderer/store/llm'
import type { AwsBedrockAuthType } from '@renderer/types'
import { useDispatch } from 'react-redux' import { useDispatch } from 'react-redux'
export function useAwsBedrockSettings() { export function useAwsBedrockSettings() {
@@ -8,8 +15,10 @@ export function useAwsBedrockSettings() {
return { return {
...settings, ...settings,
setAuthType: (authType: AwsBedrockAuthType) => dispatch(setAwsBedrockAuthType(authType)),
setAccessKeyId: (accessKeyId: string) => dispatch(setAwsBedrockAccessKeyId(accessKeyId)), setAccessKeyId: (accessKeyId: string) => dispatch(setAwsBedrockAccessKeyId(accessKeyId)),
setSecretAccessKey: (secretAccessKey: string) => dispatch(setAwsBedrockSecretAccessKey(secretAccessKey)), setSecretAccessKey: (secretAccessKey: string) => dispatch(setAwsBedrockSecretAccessKey(secretAccessKey)),
setApiKey: (apiKey: string) => dispatch(setAwsBedrockApiKey(apiKey)),
setRegion: (region: string) => dispatch(setAwsBedrockRegion(region)) setRegion: (region: string) => dispatch(setAwsBedrockRegion(region))
} }
} }
@@ -18,6 +27,10 @@ export function getAwsBedrockSettings() {
return store.getState().llm.settings.awsBedrock return store.getState().llm.settings.awsBedrock
} }
export function getAwsBedrockAuthType() {
return store.getState().llm.settings.awsBedrock.authType
}
export function getAwsBedrockAccessKeyId() { export function getAwsBedrockAccessKeyId() {
return store.getState().llm.settings.awsBedrock.accessKeyId return store.getState().llm.settings.awsBedrock.accessKeyId
} }
@@ -26,6 +39,10 @@ export function getAwsBedrockSecretAccessKey() {
return store.getState().llm.settings.awsBedrock.secretAccessKey return store.getState().llm.settings.awsBedrock.secretAccessKey
} }
export function getAwsBedrockApiKey() {
return store.getState().llm.settings.awsBedrock.apiKey
}
export function getAwsBedrockRegion() { export function getAwsBedrockRegion() {
return store.getState().llm.settings.awsBedrock.region return store.getState().llm.settings.awsBedrock.region
} }
-12
View File
@@ -12,19 +12,7 @@ export default function useUserTheme() {
const colorPrimary = Color(theme.colorPrimary) const colorPrimary = Color(theme.colorPrimary)
document.body.style.setProperty('--color-primary', colorPrimary.toString()) document.body.style.setProperty('--color-primary', colorPrimary.toString())
// overwrite hero UI primary color.
document.body.style.setProperty('--primary', colorPrimary.toString()) document.body.style.setProperty('--primary', colorPrimary.toString())
document.body.style.setProperty('--heroui-primary', colorPrimary.toString())
document.body.style.setProperty('--heroui-primary-900', colorPrimary.lighten(0.5).toString())
document.body.style.setProperty('--heroui-primary-800', colorPrimary.lighten(0.4).toString())
document.body.style.setProperty('--heroui-primary-700', colorPrimary.lighten(0.3).toString())
document.body.style.setProperty('--heroui-primary-600', colorPrimary.lighten(0.2).toString())
document.body.style.setProperty('--heroui-primary-500', colorPrimary.lighten(0.1).toString())
document.body.style.setProperty('--heroui-primary-400', colorPrimary.toString())
document.body.style.setProperty('--heroui-primary-300', colorPrimary.darken(0.1).toString())
document.body.style.setProperty('--heroui-primary-200', colorPrimary.darken(0.2).toString())
document.body.style.setProperty('--heroui-primary-100', colorPrimary.darken(0.3).toString())
document.body.style.setProperty('--heroui-primary-50', colorPrimary.darken(0.4).toString())
document.body.style.setProperty('--color-primary-soft', colorPrimary.alpha(0.6).toString()) document.body.style.setProperty('--color-primary-soft', colorPrimary.alpha(0.6).toString())
document.body.style.setProperty('--color-primary-mute', colorPrimary.alpha(0.3).toString()) document.body.style.setProperty('--color-primary-mute', colorPrimary.alpha(0.3).toString())
+3 -2
View File
@@ -85,7 +85,8 @@ const providerKeyMap = {
poe: 'provider.poe', poe: 'provider.poe',
aionly: 'provider.aionly', aionly: 'provider.aionly',
longcat: 'provider.longcat', longcat: 'provider.longcat',
huggingface: 'provider.huggingface' huggingface: 'provider.huggingface',
sophnet: 'provider.sophnet'
} as const } as const
/** /**
@@ -238,7 +239,7 @@ const paintingsImageSizeOptionsKeyMap = {
} as const } as const
export const getPaintingsImageSizeOptionsLabel = (key: string): string => { export const getPaintingsImageSizeOptionsLabel = (key: string): string => {
return getLabel(paintingsImageSizeOptionsKeyMap, key) return paintingsImageSizeOptionsKeyMap[key] ? getLabel(paintingsImageSizeOptionsKeyMap, key) : key
} }
const paintingsQualityOptionsKeyMap = { const paintingsQualityOptionsKeyMap = {
+50
View File
@@ -339,6 +339,41 @@
}, },
"title": "API Server" "title": "API Server"
}, },
"appMenu": {
"about": "About",
"close": "Close Window",
"copy": "Copy",
"cut": "Cut",
"delete": "Delete",
"documentation": "Documentation",
"edit": "Edit",
"feedback": "Feedback",
"file": "File",
"forceReload": "Force Reload",
"front": "Bring All to Front",
"help": "Help",
"hide": "Hide",
"hideOthers": "Hide Others",
"minimize": "Minimize",
"paste": "Paste",
"quit": "Quit",
"redo": "Redo",
"releases": "Releases",
"reload": "Reload",
"resetZoom": "Actual Size",
"selectAll": "Select All",
"services": "Services",
"toggleDevTools": "Toggle Developer Tools",
"toggleFullscreen": "Toggle Fullscreen",
"undo": "Undo",
"unhide": "Show All",
"view": "View",
"website": "Website",
"window": "Window",
"zoom": "Zoom",
"zoomIn": "Zoom In",
"zoomOut": "Zoom Out"
},
"assistants": { "assistants": {
"abbr": "Assistants", "abbr": "Assistants",
"clear": { "clear": {
@@ -2482,6 +2517,7 @@
"qiniu": "Qiniu AI", "qiniu": "Qiniu AI",
"qwenlm": "QwenLM", "qwenlm": "QwenLM",
"silicon": "SiliconFlow", "silicon": "SiliconFlow",
"sophnet": "SophNet",
"stepfun": "StepFun", "stepfun": "StepFun",
"tencent-cloud-ti": "Tencent Cloud TI", "tencent-cloud-ti": "Tencent Cloud TI",
"together": "Together", "together": "Together",
@@ -3765,6 +3801,7 @@
"description": "Do not enable MCP server functionality", "description": "Do not enable MCP server functionality",
"label": "Disable MCP Server" "label": "Disable MCP Server"
}, },
"discover": "Discover",
"duplicateName": "A server with this name already exists", "duplicateName": "A server with this name already exists",
"editJson": "Edit JSON", "editJson": "Edit JSON",
"editMcpJson": "Edit MCP Configuration", "editMcpJson": "Edit MCP Configuration",
@@ -3775,6 +3812,10 @@
"32000": "MCP server failed to start, please check the parameters according to the tutorial", "32000": "MCP server failed to start, please check the parameters according to the tutorial",
"toolNotFound": "Tool {{name}} not found" "toolNotFound": "Tool {{name}} not found"
}, },
"fetch": {
"button": "Fetch Servers",
"success": "Successfully fetched MCP servers"
},
"findMore": "Find More MCP", "findMore": "Find More MCP",
"headers": "Headers", "headers": "Headers",
"headersTooltip": "Custom headers for HTTP requests", "headersTooltip": "Custom headers for HTTP requests",
@@ -3790,6 +3831,7 @@
"logoUrl": "Logo URL", "logoUrl": "Logo URL",
"longRunning": "Long Running Mode", "longRunning": "Long Running Mode",
"longRunningTooltip": "When enabled, the server supports long-running tasks. When receiving progress notifications, the timeout will be reset and the maximum execution time will be extended to 10 minutes.", "longRunningTooltip": "When enabled, the server supports long-running tasks. When receiving progress notifications, the timeout will be reset and the maximum execution time will be extended to 10 minutes.",
"marketplaces": "Marketplaces",
"missingDependencies": "is Missing, please install it to continue.", "missingDependencies": "is Missing, please install it to continue.",
"more": { "more": {
"awesome": "Curated MCP Server List", "awesome": "Curated MCP Server List",
@@ -3838,6 +3880,7 @@
"provider": "Provider", "provider": "Provider",
"providerPlaceholder": "Provider name", "providerPlaceholder": "Provider name",
"providerUrl": "Provider URL", "providerUrl": "Provider URL",
"providers": "Providers",
"registry": "Package Registry", "registry": "Package Registry",
"registryDefault": "Default", "registryDefault": "Default",
"registryTooltip": "Choose the registry for package installation to resolve network issues with the default registry.", "registryTooltip": "Choose the registry for package installation to resolve network issues with the default registry.",
@@ -3860,6 +3903,7 @@
"searchNpx": "Search MCP", "searchNpx": "Search MCP",
"serverPlural": "servers", "serverPlural": "servers",
"serverSingular": "server", "serverSingular": "server",
"servers": "MCP Servers",
"sse": "Server-Sent Events (sse)", "sse": "Server-Sent Events (sse)",
"startError": "Start failed", "startError": "Start failed",
"stdio": "Standard Input/Output (stdio)", "stdio": "Standard Input/Output (stdio)",
@@ -4259,6 +4303,12 @@
"aws-bedrock": { "aws-bedrock": {
"access_key_id": "AWS Access Key ID", "access_key_id": "AWS Access Key ID",
"access_key_id_help": "Your AWS Access Key ID for accessing AWS Bedrock services", "access_key_id_help": "Your AWS Access Key ID for accessing AWS Bedrock services",
"api_key": "Bedrock API Key",
"api_key_help": "Your AWS Bedrock API Key for authentication",
"auth_type": "Authentication Type",
"auth_type_api_key": "Bedrock API Key",
"auth_type_help": "Choose between IAM credentials or Bedrock API Key authentication",
"auth_type_iam": "IAM Credentials",
"description": "AWS Bedrock is Amazon's fully managed foundation model service that supports various advanced large language models", "description": "AWS Bedrock is Amazon's fully managed foundation model service that supports various advanced large language models",
"region": "AWS Region", "region": "AWS Region",
"region_help": "Your AWS service region, e.g., us-east-1", "region_help": "Your AWS service region, e.g., us-east-1",
+51 -1
View File
@@ -339,6 +339,41 @@
}, },
"title": "API 服务器" "title": "API 服务器"
}, },
"appMenu": {
"about": "关于",
"close": "关闭窗口",
"copy": "复制",
"cut": "剪切",
"delete": "删除",
"documentation": "文档",
"edit": "编辑",
"feedback": "反馈",
"file": "文件",
"forceReload": "强制重新加载",
"front": "全部置于顶层",
"help": "帮助",
"hide": "隐藏",
"hideOthers": "隐藏其他",
"minimize": "最小化",
"paste": "粘贴",
"quit": "退出",
"redo": "重做",
"releases": "版本发布",
"reload": "重新加载",
"resetZoom": "实际大小",
"selectAll": "全选",
"services": "服务",
"toggleDevTools": "切换开发者工具",
"toggleFullscreen": "切换全屏",
"undo": "撤销",
"unhide": "全部显示",
"view": "视图",
"website": "网站",
"window": "窗口",
"zoom": "缩放",
"zoomIn": "放大",
"zoomOut": "缩小"
},
"assistants": { "assistants": {
"abbr": "助手", "abbr": "助手",
"clear": { "clear": {
@@ -1611,7 +1646,7 @@
}, },
"assistant": { "assistant": {
"added": { "added": {
"content": "智能体添加成功" "content": "助手添加成功"
} }
}, },
"attachments": { "attachments": {
@@ -2482,6 +2517,7 @@
"qiniu": "七牛云 AI 推理", "qiniu": "七牛云 AI 推理",
"qwenlm": "QwenLM", "qwenlm": "QwenLM",
"silicon": "硅基流动", "silicon": "硅基流动",
"sophnet": "SophNet",
"stepfun": "阶跃星辰", "stepfun": "阶跃星辰",
"tencent-cloud-ti": "腾讯云 TI", "tencent-cloud-ti": "腾讯云 TI",
"together": "Together", "together": "Together",
@@ -3765,6 +3801,7 @@
"description": "不启用 MCP 服务功能", "description": "不启用 MCP 服务功能",
"label": "不使用 MCP 服务器" "label": "不使用 MCP 服务器"
}, },
"discover": "发现",
"duplicateName": "已存在同名服务器", "duplicateName": "已存在同名服务器",
"editJson": "编辑 JSON", "editJson": "编辑 JSON",
"editMcpJson": "编辑 MCP 配置", "editMcpJson": "编辑 MCP 配置",
@@ -3775,6 +3812,10 @@
"32000": "MCP 服务器启动失败,请根据教程检查参数是否填写完整", "32000": "MCP 服务器启动失败,请根据教程检查参数是否填写完整",
"toolNotFound": "未找到工具 {{name}}" "toolNotFound": "未找到工具 {{name}}"
}, },
"fetch": {
"button": "获取服务器",
"success": "服务器获取成功"
},
"findMore": "更多 MCP", "findMore": "更多 MCP",
"headers": "请求头", "headers": "请求头",
"headersTooltip": "HTTP 请求的自定义请求头", "headersTooltip": "HTTP 请求的自定义请求头",
@@ -3790,6 +3831,7 @@
"logoUrl": "标志网址", "logoUrl": "标志网址",
"longRunning": "长时间运行模式", "longRunning": "长时间运行模式",
"longRunningTooltip": "启用后,服务器支持长时间任务,接收到进度通知时会重置超时计时器,并延长最大超时时间至10分钟", "longRunningTooltip": "启用后,服务器支持长时间任务,接收到进度通知时会重置超时计时器,并延长最大超时时间至10分钟",
"marketplaces": "市场",
"missingDependencies": "缺失,请安装它以继续", "missingDependencies": "缺失,请安装它以继续",
"more": { "more": {
"awesome": "精选的 MCP 服务器列表", "awesome": "精选的 MCP 服务器列表",
@@ -3838,6 +3880,7 @@
"provider": "提供者", "provider": "提供者",
"providerPlaceholder": "提供者名称", "providerPlaceholder": "提供者名称",
"providerUrl": "提供者网址", "providerUrl": "提供者网址",
"providers": "提供商",
"registry": "包管理源", "registry": "包管理源",
"registryDefault": "默认", "registryDefault": "默认",
"registryTooltip": "选择用于安装包的源,以解决默认源的网络问题", "registryTooltip": "选择用于安装包的源,以解决默认源的网络问题",
@@ -3860,6 +3903,7 @@
"searchNpx": "搜索 MCP", "searchNpx": "搜索 MCP",
"serverPlural": "服务器", "serverPlural": "服务器",
"serverSingular": "服务器", "serverSingular": "服务器",
"servers": "MCP 服务器",
"sse": "服务器发送事件 (sse)", "sse": "服务器发送事件 (sse)",
"startError": "启动失败", "startError": "启动失败",
"stdio": "标准输入 / 输出 (stdio)", "stdio": "标准输入 / 输出 (stdio)",
@@ -4259,6 +4303,12 @@
"aws-bedrock": { "aws-bedrock": {
"access_key_id": "AWS 访问密钥 ID", "access_key_id": "AWS 访问密钥 ID",
"access_key_id_help": "您的 AWS 访问密钥 ID,用于访问 AWS Bedrock 服务", "access_key_id_help": "您的 AWS 访问密钥 ID,用于访问 AWS Bedrock 服务",
"api_key": "Bedrock API 密钥",
"api_key_help": "您的 AWS Bedrock API 密钥,用于身份验证",
"auth_type": "认证方式",
"auth_type_api_key": "Bedrock API 密钥",
"auth_type_help": "选择使用 IAM 凭证或 Bedrock API 密钥进行身份验证",
"auth_type_iam": "IAM 凭证",
"description": "AWS Bedrock 是亚马逊提供的全托管基础模型服务,支持多种先进的大语言模型", "description": "AWS Bedrock 是亚马逊提供的全托管基础模型服务,支持多种先进的大语言模型",
"region": "AWS 区域", "region": "AWS 区域",
"region_help": "您的 AWS 服务区域,例如 us-east-1", "region_help": "您的 AWS 服务区域,例如 us-east-1",
+51 -1
View File
@@ -339,6 +339,41 @@
}, },
"title": "API 伺服器" "title": "API 伺服器"
}, },
"appMenu": {
"about": "關於",
"close": "關閉視窗",
"copy": "複製",
"cut": "剪下",
"delete": "刪除",
"documentation": "文件",
"edit": "編輯",
"feedback": "回饋",
"file": "檔案",
"forceReload": "強制重新載入",
"front": "全部置於頂層",
"help": "幫助",
"hide": "隱藏",
"hideOthers": "隱藏其他",
"minimize": "最小化",
"paste": "貼上",
"quit": "結束",
"redo": "重做",
"releases": "版本發布",
"reload": "重新載入",
"resetZoom": "實際大小",
"selectAll": "全選",
"services": "服務",
"toggleDevTools": "切換開發者工具",
"toggleFullscreen": "切換全螢幕",
"undo": "復原",
"unhide": "全部顯示",
"view": "檢視",
"website": "網站",
"window": "視窗",
"zoom": "縮放",
"zoomIn": "放大",
"zoomOut": "縮小"
},
"assistants": { "assistants": {
"abbr": "助手", "abbr": "助手",
"clear": { "clear": {
@@ -1611,7 +1646,7 @@
}, },
"assistant": { "assistant": {
"added": { "added": {
"content": "智慧代理人新增成功" "content": "助手新增成功"
} }
}, },
"attachments": { "attachments": {
@@ -2482,6 +2517,7 @@
"qiniu": "七牛雲 AI 推理", "qiniu": "七牛雲 AI 推理",
"qwenlm": "QwenLM", "qwenlm": "QwenLM",
"silicon": "SiliconFlow", "silicon": "SiliconFlow",
"sophnet": "SophNet",
"stepfun": "StepFun", "stepfun": "StepFun",
"tencent-cloud-ti": "騰訊雲 TI", "tencent-cloud-ti": "騰訊雲 TI",
"together": "Together", "together": "Together",
@@ -3765,6 +3801,7 @@
"description": "不啟用 MCP 服務功能", "description": "不啟用 MCP 服務功能",
"label": "不使用 MCP 伺服器" "label": "不使用 MCP 伺服器"
}, },
"discover": "發現",
"duplicateName": "已存在相同名稱的伺服器", "duplicateName": "已存在相同名稱的伺服器",
"editJson": "編輯 JSON", "editJson": "編輯 JSON",
"editMcpJson": "編輯 MCP 配置", "editMcpJson": "編輯 MCP 配置",
@@ -3775,6 +3812,10 @@
"32000": "MCP 伺服器啟動失敗,請根據教程檢查參數是否填寫完整", "32000": "MCP 伺服器啟動失敗,請根據教程檢查參數是否填寫完整",
"toolNotFound": "未找到工具 {{name}}" "toolNotFound": "未找到工具 {{name}}"
}, },
"fetch": {
"button": "獲取伺服器",
"success": "伺服器獲取成功"
},
"findMore": "更多 MCP", "findMore": "更多 MCP",
"headers": "請求標頭", "headers": "請求標頭",
"headersTooltip": "HTTP 請求的自定義標頭", "headersTooltip": "HTTP 請求的自定義標頭",
@@ -3790,6 +3831,7 @@
"logoUrl": "標誌網址", "logoUrl": "標誌網址",
"longRunning": "長時間運行模式", "longRunning": "長時間運行模式",
"longRunningTooltip": "啟用後,伺服器支援長時間任務,接收到進度通知時會重置超時計時器,並延長最大超時時間至10分鐘", "longRunningTooltip": "啟用後,伺服器支援長時間任務,接收到進度通知時會重置超時計時器,並延長最大超時時間至10分鐘",
"marketplaces": "市場",
"missingDependencies": "缺失,請安裝它以繼續", "missingDependencies": "缺失,請安裝它以繼續",
"more": { "more": {
"awesome": "精選的 MCP 伺服器清單", "awesome": "精選的 MCP 伺服器清單",
@@ -3838,6 +3880,7 @@
"provider": "提供者", "provider": "提供者",
"providerPlaceholder": "提供者名稱", "providerPlaceholder": "提供者名稱",
"providerUrl": "提供者網址", "providerUrl": "提供者網址",
"providers": "提供商",
"registry": "套件管理源", "registry": "套件管理源",
"registryDefault": "預設", "registryDefault": "預設",
"registryTooltip": "選擇用於安裝套件的源,以解決預設源的網路問題", "registryTooltip": "選擇用於安裝套件的源,以解決預設源的網路問題",
@@ -3860,6 +3903,7 @@
"searchNpx": "搜索 MCP", "searchNpx": "搜索 MCP",
"serverPlural": "伺服器", "serverPlural": "伺服器",
"serverSingular": "伺服器", "serverSingular": "伺服器",
"servers": "MCP 伺服器",
"sse": "伺服器傳送事件 (sse)", "sse": "伺服器傳送事件 (sse)",
"startError": "啟動失敗", "startError": "啟動失敗",
"stdio": "標準輸入 / 輸出 (stdio)", "stdio": "標準輸入 / 輸出 (stdio)",
@@ -4259,6 +4303,12 @@
"aws-bedrock": { "aws-bedrock": {
"access_key_id": "AWS 存取密鑰 ID", "access_key_id": "AWS 存取密鑰 ID",
"access_key_id_help": "您的 AWS 存取密鑰 ID,用於存取 AWS Bedrock 服務", "access_key_id_help": "您的 AWS 存取密鑰 ID,用於存取 AWS Bedrock 服務",
"api_key": "Bedrock API 金鑰",
"api_key_help": "您的 AWS Bedrock API 金鑰,用於身份驗證",
"auth_type": "認證方式",
"auth_type_api_key": "Bedrock API 金鑰",
"auth_type_help": "選擇使用 IAM 憑證或 Bedrock API 金鑰進行身份驗證",
"auth_type_iam": "IAM 憑證",
"description": "AWS Bedrock 是亞馬遜提供的全托管基础模型服務,支持多種先進的大語言模型", "description": "AWS Bedrock 是亞馬遜提供的全托管基础模型服務,支持多種先進的大語言模型",
"region": "AWS 區域", "region": "AWS 區域",
"region_help": "您的 AWS 服務區域,例如 us-east-1", "region_help": "您的 AWS 服務區域,例如 us-east-1",
+60 -6
View File
@@ -1,6 +1,7 @@
{ {
"agent": { "agent": {
"add": { "add": {
"description": "Bewältigen Sie komplexe Aufgaben mit verschiedenen Werkzeugen",
"error": { "error": {
"failed": "Agent hinzufügen fehlgeschlagen", "failed": "Agent hinzufügen fehlgeschlagen",
"invalid_agent": "Ungültiger Agent" "invalid_agent": "Ungültiger Agent"
@@ -338,6 +339,41 @@
}, },
"title": "API-Server" "title": "API-Server"
}, },
"appMenu": {
"about": "Über",
"close": "Fenster schließen",
"copy": "Kopieren",
"cut": "Schneiden",
"delete": "Löschen",
"documentation": "Dokumentation",
"edit": "Bearbeiten",
"feedback": "Rückmeldung",
"file": "Datei",
"forceReload": "Neu laden erzwingen",
"front": "Alle in den Vordergrund bringen",
"help": "Hilfe",
"hide": "Verstecken",
"hideOthers": "Andere ausblenden",
"minimize": "Minimieren",
"paste": "Einfügen",
"quit": "Aufhören",
"redo": "Wiederholen",
"releases": "Veröffentlichungen",
"reload": "Neu laden",
"resetZoom": "Tatsächliche Größe",
"selectAll": "Alle auswählen",
"services": "Dienstleistungen",
"toggleDevTools": "Entwicklertools ein-/ausblenden",
"toggleFullscreen": "Vollbild umschalten",
"undo": "Rückgängig machen",
"unhide": "Alle anzeigen",
"view": "Ansicht",
"website": "Website",
"window": "Fenster",
"zoom": "Zoom",
"zoomIn": "Heranzoomen",
"zoomOut": "Herauszoomen"
},
"assistants": { "assistants": {
"abbr": "Assistent", "abbr": "Assistent",
"clear": { "clear": {
@@ -547,8 +583,12 @@
"chat": { "chat": {
"add": { "add": {
"assistant": { "assistant": {
"description": "Tägliche Gespräche und schnelle Fragen & Antworten",
"title": "Assistent hinzufügen" "title": "Assistent hinzufügen"
}, },
"option": {
"title": "Typ auswählen"
},
"topic": { "topic": {
"title": "Neues Thema erstellen" "title": "Neues Thema erstellen"
} }
@@ -1606,7 +1646,7 @@
}, },
"assistant": { "assistant": {
"added": { "added": {
"content": "Agent erfolgreich hinzugefügt" "content": "Assistent erfolgreich hinzugefügt"
} }
}, },
"attachments": { "attachments": {
@@ -2471,12 +2511,13 @@
"openrouter": "OpenRouter", "openrouter": "OpenRouter",
"ovms": "Intel OVMS", "ovms": "Intel OVMS",
"perplexity": "Perplexity", "perplexity": "Perplexity",
"ph8": "PH8 Großmodell-Plattform", "ph8": "PH8",
"poe": "Poe", "poe": "Poe",
"ppio": "PPIO Cloud", "ppio": "PPIO Cloud",
"qiniu": "Qiniu Cloud KI-Inferenz", "qiniu": "Qiniu Cloud KI-Inferenz",
"qwenlm": "QwenLM", "qwenlm": "QwenLM",
"silicon": "SiliconFlow", "silicon": "SiliconFlow",
"sophnet": "SophNet",
"stepfun": "StepFun", "stepfun": "StepFun",
"tencent-cloud-ti": "Tencent Cloud TI", "tencent-cloud-ti": "Tencent Cloud TI",
"together": "Together", "together": "Together",
@@ -2923,15 +2964,14 @@
}, },
"description": "Ein KI-Assistent für Kreative", "description": "Ein KI-Assistent für Kreative",
"downloading": "Update wird heruntergeladen...", "downloading": "Update wird heruntergeladen...",
"enterprise": {
"title": "Unternehmen"
},
"feedback": { "feedback": {
"button": "Feedback", "button": "Feedback",
"title": "Feedback" "title": "Feedback"
}, },
"label": "Über uns", "label": "Über uns",
"license": {
"button": "Anzeigen",
"title": "Lizenz"
},
"releases": { "releases": {
"button": "Anzeigen", "button": "Anzeigen",
"title": "Changelog" "title": "Changelog"
@@ -3761,6 +3801,7 @@
"description": "MCP-Service-Funktion nicht aktivieren", "description": "MCP-Service-Funktion nicht aktivieren",
"label": "MCP-Server nicht verwenden" "label": "MCP-Server nicht verwenden"
}, },
"discover": "Entdecken",
"duplicateName": "Server mit gleichem Namen existiert bereits", "duplicateName": "Server mit gleichem Namen existiert bereits",
"editJson": "JSON bearbeiten", "editJson": "JSON bearbeiten",
"editMcpJson": "MCP-Konfiguration bearbeiten", "editMcpJson": "MCP-Konfiguration bearbeiten",
@@ -3771,6 +3812,10 @@
"32000": "MCP-Server starten fehlgeschlagen, bitte überprüfen Sie, ob alle Parameter vollständig ausgefüllt sind", "32000": "MCP-Server starten fehlgeschlagen, bitte überprüfen Sie, ob alle Parameter vollständig ausgefüllt sind",
"toolNotFound": "Tool {{name}} nicht gefunden" "toolNotFound": "Tool {{name}} nicht gefunden"
}, },
"fetch": {
"button": "Server abrufen",
"success": "MCP-Server erfolgreich abgerufen"
},
"findMore": "Mehr MCP", "findMore": "Mehr MCP",
"headers": "Request-Header", "headers": "Request-Header",
"headersTooltip": "Benutzerdefinierte Request-Header für HTTP-Anfragen", "headersTooltip": "Benutzerdefinierte Request-Header für HTTP-Anfragen",
@@ -3786,6 +3831,7 @@
"logoUrl": "Logo-URL", "logoUrl": "Logo-URL",
"longRunning": "Lang laufender Modus", "longRunning": "Lang laufender Modus",
"longRunningTooltip": "Nach Aktivierung unterstützt der Server lange Aufgaben. Wenn ein Fortschrittsbenachrichtigung empfangen wird, wird der Timeout-Timer zurückgesetzt und die maximale Timeout-Zeit auf 10 Minuten verlängert", "longRunningTooltip": "Nach Aktivierung unterstützt der Server lange Aufgaben. Wenn ein Fortschrittsbenachrichtigung empfangen wird, wird der Timeout-Timer zurückgesetzt und die maximale Timeout-Zeit auf 10 Minuten verlängert",
"marketplaces": "Marktplätze",
"missingDependencies": "Abhängigkeiten fehlen, bitte installieren Sie sie, um fortzufahren", "missingDependencies": "Abhängigkeiten fehlen, bitte installieren Sie sie, um fortzufahren",
"more": { "more": {
"awesome": "Kuratierte MCP-Serverliste", "awesome": "Kuratierte MCP-Serverliste",
@@ -3834,6 +3880,7 @@
"provider": "Anbieter", "provider": "Anbieter",
"providerPlaceholder": "Anbietername", "providerPlaceholder": "Anbietername",
"providerUrl": "Anbieter-Website", "providerUrl": "Anbieter-Website",
"providers": "Anbieter",
"registry": "Paketverwaltungsquelle", "registry": "Paketverwaltungsquelle",
"registryDefault": "Standard", "registryDefault": "Standard",
"registryTooltip": "Quelle für Paketinstallation auswählen um Netzwerkprobleme der Standardquelle zu lösen", "registryTooltip": "Quelle für Paketinstallation auswählen um Netzwerkprobleme der Standardquelle zu lösen",
@@ -3856,6 +3903,7 @@
"searchNpx": "MCP durchsuchen", "searchNpx": "MCP durchsuchen",
"serverPlural": "Server", "serverPlural": "Server",
"serverSingular": "Server", "serverSingular": "Server",
"servers": "MCP-Server",
"sse": "Server-Sende-Ereignisse (sse)", "sse": "Server-Sende-Ereignisse (sse)",
"startError": "Start fehlgeschlagen", "startError": "Start fehlgeschlagen",
"stdio": "Standard-Eingabe / -Ausgabe (stdio)", "stdio": "Standard-Eingabe / -Ausgabe (stdio)",
@@ -4255,6 +4303,12 @@
"aws-bedrock": { "aws-bedrock": {
"access_key_id": "AWS-Zugriffsschlüssel-ID", "access_key_id": "AWS-Zugriffsschlüssel-ID",
"access_key_id_help": "Ihre AWS-Zugriffsschlüssel-ID, um auf AWS Bedrock-Dienste zuzugreifen", "access_key_id_help": "Ihre AWS-Zugriffsschlüssel-ID, um auf AWS Bedrock-Dienste zuzugreifen",
"api_key": "Bedrock-API-Schlüssel",
"api_key_help": "Ihr AWS Bedrock-API-Schlüssel für die Authentifizierung",
"auth_type": "Authentifizierungstyp",
"auth_type_api_key": "Bedrock-API-Schlüssel",
"auth_type_help": "Wählen Sie zwischen IAM-Anmeldeinformationen oder Bedrock-API-Schlüssel-Authentifizierung",
"auth_type_iam": "IAM-Anmeldeinformationen",
"description": "AWS Bedrock ist ein vollständig verwalteter Basismodell-Dienst von Amazon, der eine Vielzahl moderner großer Sprachmodelle unterstützt", "description": "AWS Bedrock ist ein vollständig verwalteter Basismodell-Dienst von Amazon, der eine Vielzahl moderner großer Sprachmodelle unterstützt",
"region": "AWS-Region", "region": "AWS-Region",
"region_help": "Ihre AWS-Serviceregion, z.B. us-east-1", "region_help": "Ihre AWS-Serviceregion, z.B. us-east-1",
+60 -6
View File
@@ -1,6 +1,7 @@
{ {
"agent": { "agent": {
"add": { "add": {
"description": "Χειριστείτε πολύπλοκες εργασίες με διάφορα εργαλεία",
"error": { "error": {
"failed": "Αποτυχία προσθήκης πράκτορα", "failed": "Αποτυχία προσθήκης πράκτορα",
"invalid_agent": "Μη έγκυρος Agent" "invalid_agent": "Μη έγκυρος Agent"
@@ -338,6 +339,41 @@
}, },
"title": "Διακομιστής API" "title": "Διακομιστής API"
}, },
"appMenu": {
"about": "Σχετικά",
"close": "Κλείσιμο Παραθύρου",
"copy": "Αντιγραφή",
"cut": "Κόψε",
"delete": "Διαγραφή",
"documentation": "Τεκμηρίωση",
"edit": "Επεξεργασία",
"feedback": "Σχόλια",
"file": "Αρχείο",
"forceReload": "Εξαναγκασμένη επαναφόρτωση",
"front": "Μεταφορά Όλων Μπροστά",
"help": "Βοήθεια",
"hide": "Κρύψε",
"hideOthers": "Απόκρυψη Άλλων",
"minimize": "Ελαχιστοποίηση",
"paste": "Επικόλληση",
"quit": "Παραιτήσου",
"redo": "Ξανακάνε",
"releases": "Κυκλοφορίες",
"reload": "Επαναφόρτωση",
"resetZoom": "Πραγματικό Μέγεθος",
"selectAll": "Επιλογή Όλων",
"services": "Υπηρεσίες",
"toggleDevTools": "Εναλλαγή Εργαλείων Προγραμματιστή",
"toggleFullscreen": "Εναλλαγή πλήρους οθόνης",
"undo": "Αναίρεση",
"unhide": "Εμφάνιση Όλων",
"view": "Προβολή",
"website": "Ιστοσελίδα",
"window": "Παράθυρο",
"zoom": "Ζουμ",
"zoomIn": "Μεγέθυνση",
"zoomOut": "Σμίκρυνση"
},
"assistants": { "assistants": {
"abbr": "Βοηθός", "abbr": "Βοηθός",
"clear": { "clear": {
@@ -547,8 +583,12 @@
"chat": { "chat": {
"add": { "add": {
"assistant": { "assistant": {
"description": "Καθημερινές συνομιλίες και γρήγορες ερωταπαντήσεις",
"title": "Προσθήκη βοηθού" "title": "Προσθήκη βοηθού"
}, },
"option": {
"title": "Επιλέξτε Τύπο"
},
"topic": { "topic": {
"title": "Δημιουργία νέου θέματος" "title": "Δημιουργία νέου θέματος"
} }
@@ -1606,7 +1646,7 @@
}, },
"assistant": { "assistant": {
"added": { "added": {
"content": "Ο ενεργοποιημένος αστρόναυτης προστέθηκε επιτυχώς" "content": "Ο βοηθός προστέθηκε επιτυχώς"
} }
}, },
"attachments": { "attachments": {
@@ -2471,12 +2511,13 @@
"openrouter": "OpenRouter", "openrouter": "OpenRouter",
"ovms": "Intel OVMS", "ovms": "Intel OVMS",
"perplexity": "Perplexity", "perplexity": "Perplexity",
"ph8": "Πλατφόρμα Ανοιχτής Μεγάλης Μοντέλου PH8", "ph8": "PH8",
"poe": "Poe", "poe": "Poe",
"ppio": "PPIO Piao Yun", "ppio": "PPIO Piao Yun",
"qiniu": "Qiniu AI", "qiniu": "Qiniu AI",
"qwenlm": "QwenLM", "qwenlm": "QwenLM",
"silicon": "Σιδηρική Παρουσία", "silicon": "Σιδηρική Παρουσία",
"sophnet": "SophNet",
"stepfun": "Βήμα Ουράς", "stepfun": "Βήμα Ουράς",
"tencent-cloud-ti": "Tencent Cloud TI", "tencent-cloud-ti": "Tencent Cloud TI",
"together": "Together", "together": "Together",
@@ -2923,15 +2964,14 @@
}, },
"description": "Ένα AI ασιστάντα που έχει σχεδιαστεί για δημιουργούς", "description": "Ένα AI ασιστάντα που έχει σχεδιαστεί για δημιουργούς",
"downloading": "Λήψη ενημερώσεων...", "downloading": "Λήψη ενημερώσεων...",
"enterprise": {
"title": "Επιχείρηση"
},
"feedback": { "feedback": {
"button": "Σχόλια και Παρατηρήσεις", "button": "Σχόλια και Παρατηρήσεις",
"title": "Αποστολή σχολίων" "title": "Αποστολή σχολίων"
}, },
"label": "Περί μας", "label": "Περί μας",
"license": {
"button": "Προβολή",
"title": "Licenses"
},
"releases": { "releases": {
"button": "Προβολή", "button": "Προβολή",
"title": "Ημερολόγιο Ενημερώσεων" "title": "Ημερολόγιο Ενημερώσεων"
@@ -3761,6 +3801,7 @@
"description": "Να μην ενεργοποιείται η λειτουργία υπηρεσίας MCP", "description": "Να μην ενεργοποιείται η λειτουργία υπηρεσίας MCP",
"label": "Να μην χρησιμοποιείται διακομιστής MCP" "label": "Να μην χρησιμοποιείται διακομιστής MCP"
}, },
"discover": "Ανακαλύψτε",
"duplicateName": "Υπάρχει ήδη ένας διακομιστής με αυτό το όνομα", "duplicateName": "Υπάρχει ήδη ένας διακομιστής με αυτό το όνομα",
"editJson": "Επεξεργασία JSON", "editJson": "Επεξεργασία JSON",
"editMcpJson": "Επεξεργασία ρύθμισης MCP", "editMcpJson": "Επεξεργασία ρύθμισης MCP",
@@ -3771,6 +3812,10 @@
"32000": "Η εκκίνηση του MCP απέτυχε. Παρακαλώ ελέγξτε αν όλες οι παράμετροι έχουν συμπληρωθεί σύμφωνα με τον οδηγό.", "32000": "Η εκκίνηση του MCP απέτυχε. Παρακαλώ ελέγξτε αν όλες οι παράμετροι έχουν συμπληρωθεί σύμφωνα με τον οδηγό.",
"toolNotFound": "Δεν βρέθηκε το εργαλείο {{name}}" "toolNotFound": "Δεν βρέθηκε το εργαλείο {{name}}"
}, },
"fetch": {
"button": "Λήψη Διακομιστών",
"success": "Επιτυχής ανάκτηση διακομιστών MCP"
},
"findMore": "Περισσότεροι διακομιστές MCP", "findMore": "Περισσότεροι διακομιστές MCP",
"headers": "Κεφαλίδες", "headers": "Κεφαλίδες",
"headersTooltip": "Προσαρμοσμένες κεφαλίδες HTTP αιτήσεων", "headersTooltip": "Προσαρμοσμένες κεφαλίδες HTTP αιτήσεων",
@@ -3786,6 +3831,7 @@
"logoUrl": "URL Λογότυπου", "logoUrl": "URL Λογότυπου",
"longRunning": "Μακροχρόνια λειτουργία", "longRunning": "Μακροχρόνια λειτουργία",
"longRunningTooltip": "Όταν ενεργοποιηθεί, ο διακομιστής υποστηρίζει μακροχρόνιες εργασίες, επαναφέρει το χρονικό όριο μετά από λήψη ειδοποίησης προόδου και επεκτείνει το μέγιστο χρονικό όριο σε 10 λεπτά.", "longRunningTooltip": "Όταν ενεργοποιηθεί, ο διακομιστής υποστηρίζει μακροχρόνιες εργασίες, επαναφέρει το χρονικό όριο μετά από λήψη ειδοποίησης προόδου και επεκτείνει το μέγιστο χρονικό όριο σε 10 λεπτά.",
"marketplaces": "Αγορές",
"missingDependencies": "Λείπει, παρακαλώ εγκαταστήστε το για να συνεχίσετε", "missingDependencies": "Λείπει, παρακαλώ εγκαταστήστε το για να συνεχίσετε",
"more": { "more": {
"awesome": "Επιλεγμένος κατάλογος διακομιστών MCP", "awesome": "Επιλεγμένος κατάλογος διακομιστών MCP",
@@ -3834,6 +3880,7 @@
"provider": "Πάροχος", "provider": "Πάροχος",
"providerPlaceholder": "Όνομα παρόχου", "providerPlaceholder": "Όνομα παρόχου",
"providerUrl": "URL Παρόχου", "providerUrl": "URL Παρόχου",
"providers": "Πάροχοι",
"registry": "Πηγή Διαχείρισης πακέτων", "registry": "Πηγή Διαχείρισης πακέτων",
"registryDefault": "Προεπιλεγμένη", "registryDefault": "Προεπιλεγμένη",
"registryTooltip": "Επιλέξτε την πηγή για την εγκατάσταση πακέτων, για να αντιμετωπιστούν προβλήματα δικτύου από την προεπιλεγμένη πηγή.", "registryTooltip": "Επιλέξτε την πηγή για την εγκατάσταση πακέτων, για να αντιμετωπιστούν προβλήματα δικτύου από την προεπιλεγμένη πηγή.",
@@ -3856,6 +3903,7 @@
"searchNpx": "Αναζήτηση MCP", "searchNpx": "Αναζήτηση MCP",
"serverPlural": "Διακομιστές", "serverPlural": "Διακομιστές",
"serverSingular": "Διακομιστής", "serverSingular": "Διακομιστής",
"servers": "Διακομιστές MCP",
"sse": "Συμβάντα Αποστολής από τον Διακομιστή (sse)", "sse": "Συμβάντα Αποστολής από τον Διακομιστή (sse)",
"startError": "Εκκίνηση Απέτυχε", "startError": "Εκκίνηση Απέτυχε",
"stdio": "Πρότυπη Είσοδος/Έξοδος (stdio)", "stdio": "Πρότυπη Είσοδος/Έξοδος (stdio)",
@@ -4255,6 +4303,12 @@
"aws-bedrock": { "aws-bedrock": {
"access_key_id": "Αναγνωριστικό κλειδιού πρόσβασης AWS", "access_key_id": "Αναγνωριστικό κλειδιού πρόσβασης AWS",
"access_key_id_help": "Το ID του κλειδιού πρόσβασης AWS που χρησιμοποιείται για την πρόσβαση στην υπηρεσία AWS Bedrock", "access_key_id_help": "Το ID του κλειδιού πρόσβασης AWS που χρησιμοποιείται για την πρόσβαση στην υπηρεσία AWS Bedrock",
"api_key": "Κλειδί API Bedrock",
"api_key_help": "Το κλειδί API του AWS Bedrock για έλεγχο ταυτότητας",
"auth_type": "Τύπος Πιστοποίησης",
"auth_type_api_key": "Κλειδί API Bedrock",
"auth_type_help": "Επιλέξτε μεταξύ πιστοποιητικών IAM ή πιστοποίησης με κλειδί API Bedrock",
"auth_type_iam": "Διαπιστευτήρια IAM",
"description": "Η AWS Bedrock είναι μια πλήρως διαχειριζόμενη υπηρεσία βασικών μοντέλων που παρέχεται από την Amazon και υποστηρίζει διάφορα προηγμένα μεγάλα γλωσσικά μοντέλα.", "description": "Η AWS Bedrock είναι μια πλήρως διαχειριζόμενη υπηρεσία βασικών μοντέλων που παρέχεται από την Amazon και υποστηρίζει διάφορα προηγμένα μεγάλα γλωσσικά μοντέλα.",
"region": "Περιοχές AWS", "region": "Περιοχές AWS",
"region_help": "Η περιοχή υπηρεσίας AWS σας, για παράδειγμα us-east-1", "region_help": "Η περιοχή υπηρεσίας AWS σας, για παράδειγμα us-east-1",
+59 -5
View File
@@ -1,6 +1,7 @@
{ {
"agent": { "agent": {
"add": { "add": {
"description": "Maneja tareas complejas con varias herramientas",
"error": { "error": {
"failed": "Error al añadir agente", "failed": "Error al añadir agente",
"invalid_agent": "Agent inválido" "invalid_agent": "Agent inválido"
@@ -338,6 +339,41 @@
}, },
"title": "Servidor API" "title": "Servidor API"
}, },
"appMenu": {
"about": "Acerca de",
"close": "Cerrar ventana",
"copy": "Copiar",
"cut": "Cortar",
"delete": "Eliminar",
"documentation": "Documentación",
"edit": "Editar",
"feedback": "Retroalimentación",
"file": "Archivo",
"forceReload": "Forzar recarga",
"front": "Traer todo al frente",
"help": "Ayuda",
"hide": "Ocultar",
"hideOthers": "Ocultar Otros",
"minimize": "Minimizar",
"paste": "Pegar",
"quit": "Abandonar",
"redo": "Rehacer",
"releases": "Lanzamientos",
"reload": "Recargar",
"resetZoom": "Tamaño Real",
"selectAll": "Seleccionar todo",
"services": "Servicios",
"toggleDevTools": "Alternar herramientas de desarrollo",
"toggleFullscreen": "Activar pantalla completa",
"undo": "Deshacer",
"unhide": "Mostrar todo",
"view": "Vista",
"website": "Sitio web",
"window": "Ventana",
"zoom": "Zoom",
"zoomIn": "Acercar",
"zoomOut": "Alejar"
},
"assistants": { "assistants": {
"abbr": "Asistente", "abbr": "Asistente",
"clear": { "clear": {
@@ -547,8 +583,12 @@
"chat": { "chat": {
"add": { "add": {
"assistant": { "assistant": {
"description": "Conversaciones diarias y preguntas y respuestas rápidas",
"title": "Agregar asistente" "title": "Agregar asistente"
}, },
"option": {
"title": "Seleccionar Tipo"
},
"topic": { "topic": {
"title": "Crear nuevo tema" "title": "Crear nuevo tema"
} }
@@ -2471,12 +2511,13 @@
"openrouter": "OpenRouter", "openrouter": "OpenRouter",
"ovms": "Intel OVMS", "ovms": "Intel OVMS",
"perplexity": "Perplejidad", "perplexity": "Perplejidad",
"ph8": "Plataforma Abierta de Grandes Modelos PH8", "ph8": "PH8",
"poe": "Poe", "poe": "Poe",
"ppio": "PPIO Cloud Piao", "ppio": "PPIO Cloud Piao",
"qiniu": "Qiniu AI", "qiniu": "Qiniu AI",
"qwenlm": "QwenLM", "qwenlm": "QwenLM",
"silicon": "Silicio Fluido", "silicon": "Silicio Fluido",
"sophnet": "SophNet",
"stepfun": "Función Salto", "stepfun": "Función Salto",
"tencent-cloud-ti": "Tencent Nube TI", "tencent-cloud-ti": "Tencent Nube TI",
"together": "Juntos", "together": "Juntos",
@@ -2923,15 +2964,14 @@
}, },
"description": "Una asistente de IA creada para los creadores", "description": "Una asistente de IA creada para los creadores",
"downloading": "Descargando actualización...", "downloading": "Descargando actualización...",
"enterprise": {
"title": "Empresa"
},
"feedback": { "feedback": {
"button": "Enviar feedback", "button": "Enviar feedback",
"title": "Enviar comentarios" "title": "Enviar comentarios"
}, },
"label": "Acerca de nosotros", "label": "Acerca de nosotros",
"license": {
"button": "Ver",
"title": "Licencia"
},
"releases": { "releases": {
"button": "Ver", "button": "Ver",
"title": "Registro de cambios" "title": "Registro de cambios"
@@ -3761,6 +3801,7 @@
"description": "No habilitar funciones del servicio MCP", "description": "No habilitar funciones del servicio MCP",
"label": "No utilizar servidor MCP" "label": "No utilizar servidor MCP"
}, },
"discover": "Descubrir",
"duplicateName": "Ya existe un servidor con el mismo nombre", "duplicateName": "Ya existe un servidor con el mismo nombre",
"editJson": "Editar JSON", "editJson": "Editar JSON",
"editMcpJson": "Editar configuración MCP", "editMcpJson": "Editar configuración MCP",
@@ -3771,6 +3812,10 @@
"32000": "El servidor MCP no se pudo iniciar, verifique si los parámetros están completos según la guía", "32000": "El servidor MCP no se pudo iniciar, verifique si los parámetros están completos según la guía",
"toolNotFound": "Herramienta no encontrada {{name}}" "toolNotFound": "Herramienta no encontrada {{name}}"
}, },
"fetch": {
"button": "Obtener Servidores",
"success": "Servidores MCP obtenidos con éxito"
},
"findMore": "Más servidores MCP", "findMore": "Más servidores MCP",
"headers": "Encabezados", "headers": "Encabezados",
"headersTooltip": "Encabezados personalizados para solicitudes HTTP", "headersTooltip": "Encabezados personalizados para solicitudes HTTP",
@@ -3786,6 +3831,7 @@
"logoUrl": "URL del logotipo", "logoUrl": "URL del logotipo",
"longRunning": "Modo de ejecución prolongada", "longRunning": "Modo de ejecución prolongada",
"longRunningTooltip": "Una vez habilitado, el servidor admite tareas de larga duración, reinicia el temporizador de tiempo de espera al recibir notificaciones de progreso y amplía el tiempo máximo de espera hasta 10 minutos.", "longRunningTooltip": "Una vez habilitado, el servidor admite tareas de larga duración, reinicia el temporizador de tiempo de espera al recibir notificaciones de progreso y amplía el tiempo máximo de espera hasta 10 minutos.",
"marketplaces": "Mercados",
"missingDependencies": "Faltan, instalelas para continuar", "missingDependencies": "Faltan, instalelas para continuar",
"more": { "more": {
"awesome": "Lista seleccionada de servidores MCP", "awesome": "Lista seleccionada de servidores MCP",
@@ -3834,6 +3880,7 @@
"provider": "Proveedor", "provider": "Proveedor",
"providerPlaceholder": "Nombre del proveedor", "providerPlaceholder": "Nombre del proveedor",
"providerUrl": "URL del proveedor", "providerUrl": "URL del proveedor",
"providers": "Proveedores",
"registry": "Repositorio de paquetes", "registry": "Repositorio de paquetes",
"registryDefault": "Predeterminado", "registryDefault": "Predeterminado",
"registryTooltip": "Seleccione un repositorio para instalar paquetes, útil para resolver problemas de red con el repositorio predeterminado.", "registryTooltip": "Seleccione un repositorio para instalar paquetes, útil para resolver problemas de red con el repositorio predeterminado.",
@@ -3856,6 +3903,7 @@
"searchNpx": "Buscar MCP", "searchNpx": "Buscar MCP",
"serverPlural": "Servidores", "serverPlural": "Servidores",
"serverSingular": "Servidor", "serverSingular": "Servidor",
"servers": "Servidores MCP",
"sse": "Eventos enviados por el servidor (sse)", "sse": "Eventos enviados por el servidor (sse)",
"startError": "Inicio fallido", "startError": "Inicio fallido",
"stdio": "Entrada/Salida estándar (stdio)", "stdio": "Entrada/Salida estándar (stdio)",
@@ -4255,6 +4303,12 @@
"aws-bedrock": { "aws-bedrock": {
"access_key_id": "ID de clave de acceso de AWS", "access_key_id": "ID de clave de acceso de AWS",
"access_key_id_help": "Su ID de clave de acceso de AWS, utilizado para acceder al servicio AWS Bedrock", "access_key_id_help": "Su ID de clave de acceso de AWS, utilizado para acceder al servicio AWS Bedrock",
"api_key": "Clave de API de Bedrock",
"api_key_help": "Tu clave de API de AWS Bedrock para autenticación",
"auth_type": "Tipo de autenticación",
"auth_type_api_key": "Clave de API de Bedrock",
"auth_type_help": "Elige entre credenciales IAM o autenticación con clave API de Bedrock",
"auth_type_iam": "Credenciales de IAM",
"description": "AWS Bedrock es un servicio de modelos fundamentales completamente gestionado proporcionado por Amazon, que admite diversos modelos avanzados de lenguaje de gran tamaño.", "description": "AWS Bedrock es un servicio de modelos fundamentales completamente gestionado proporcionado por Amazon, que admite diversos modelos avanzados de lenguaje de gran tamaño.",
"region": "Región de AWS", "region": "Región de AWS",
"region_help": "Su región de servicio AWS, por ejemplo us-east-1", "region_help": "Su región de servicio AWS, por ejemplo us-east-1",
+59 -5
View File
@@ -1,6 +1,7 @@
{ {
"agent": { "agent": {
"add": { "add": {
"description": "Gérez des tâches complexes avec divers outils",
"error": { "error": {
"failed": "Échec de l'ajout de l'agent", "failed": "Échec de l'ajout de l'agent",
"invalid_agent": "Agent invalide" "invalid_agent": "Agent invalide"
@@ -338,6 +339,41 @@
}, },
"title": "Serveur API" "title": "Serveur API"
}, },
"appMenu": {
"about": "À propos",
"close": "Fermer la fenêtre",
"copy": "Copier",
"cut": "Couper",
"delete": "Supprimer",
"documentation": "Documentation",
"edit": "Modifier",
"feedback": "Retour d'information",
"file": "Fichier",
"forceReload": "Rechargement forcé",
"front": "Tout ramener au premier plan",
"help": "Aide",
"hide": "Cacher",
"hideOthers": "Masquer les autres",
"minimize": "Minimiser",
"paste": "Coller",
"quit": "Quitter",
"redo": "Refaire",
"releases": "Sorties",
"reload": "Recharger",
"resetZoom": "Taille réelle",
"selectAll": "Tout sélectionner",
"services": "Services",
"toggleDevTools": "Basculer les outils de développement",
"toggleFullscreen": "Basculer en plein écran",
"undo": "Annuler",
"unhide": "Tout afficher",
"view": "Vue",
"website": "Site web",
"window": "Fenêtre",
"zoom": "Zoom",
"zoomIn": "Zoom Avant",
"zoomOut": "Dézoomer"
},
"assistants": { "assistants": {
"abbr": "Aide", "abbr": "Aide",
"clear": { "clear": {
@@ -547,8 +583,12 @@
"chat": { "chat": {
"add": { "add": {
"assistant": { "assistant": {
"description": "Conversations quotidiennes et Q&R rapides",
"title": "Ajouter un assistant" "title": "Ajouter un assistant"
}, },
"option": {
"title": "Sélectionner le type"
},
"topic": { "topic": {
"title": "Nouveau sujet" "title": "Nouveau sujet"
} }
@@ -2471,12 +2511,13 @@
"openrouter": "OpenRouter", "openrouter": "OpenRouter",
"ovms": "Intel OVMS", "ovms": "Intel OVMS",
"perplexity": "Perplexité", "perplexity": "Perplexité",
"ph8": "Plateforme ouverte de grands modèles PH8", "ph8": "PH8",
"poe": "Poe", "poe": "Poe",
"ppio": "PPIO Cloud Piou", "ppio": "PPIO Cloud Piou",
"qiniu": "Qiniu AI", "qiniu": "Qiniu AI",
"qwenlm": "QwenLM", "qwenlm": "QwenLM",
"silicon": "Silicium Fluide", "silicon": "Silicium Fluide",
"sophnet": "SophNet",
"stepfun": "Échelon Étoile", "stepfun": "Échelon Étoile",
"tencent-cloud-ti": "Tencent Cloud TI", "tencent-cloud-ti": "Tencent Cloud TI",
"together": "Ensemble", "together": "Ensemble",
@@ -2923,15 +2964,14 @@
}, },
"description": "Un assistant IA conçu pour les créateurs", "description": "Un assistant IA conçu pour les créateurs",
"downloading": "Téléchargement de la mise à jour en cours...", "downloading": "Téléchargement de la mise à jour en cours...",
"enterprise": {
"title": "Entreprise"
},
"feedback": { "feedback": {
"button": "Faire un retour", "button": "Faire un retour",
"title": "Retour d'information" "title": "Retour d'information"
}, },
"label": "À propos de nous", "label": "À propos de nous",
"license": {
"button": "Afficher",
"title": "Licence"
},
"releases": { "releases": {
"button": "Afficher", "button": "Afficher",
"title": "Journal des mises à jour" "title": "Journal des mises à jour"
@@ -3761,6 +3801,7 @@
"description": "Désactiver les fonctionnalités du service MCP", "description": "Désactiver les fonctionnalités du service MCP",
"label": "Ne pas utiliser le serveur MCP" "label": "Ne pas utiliser le serveur MCP"
}, },
"discover": "Découvrir",
"duplicateName": "Un serveur portant le même nom existe déjà", "duplicateName": "Un serveur portant le même nom existe déjà",
"editJson": "Modifier le JSON", "editJson": "Modifier le JSON",
"editMcpJson": "Редактировать конфигурацию MCP", "editMcpJson": "Редактировать конфигурацию MCP",
@@ -3771,6 +3812,10 @@
"32000": "Échec du démarrage du serveur MCP, veuillez vérifier si tous les paramètres sont correctement remplis conformément au tutoriel", "32000": "Échec du démarrage du serveur MCP, veuillez vérifier si tous les paramètres sont correctement remplis conformément au tutoriel",
"toolNotFound": "Outil non trouvé {{name}}" "toolNotFound": "Outil non trouvé {{name}}"
}, },
"fetch": {
"button": "Récupérer les serveurs",
"success": "Serveurs MCP récupérés avec succès"
},
"findMore": "Plus de serveurs MCP", "findMore": "Plus de serveurs MCP",
"headers": "Заголовки запроса", "headers": "Заголовки запроса",
"headersTooltip": "Пользовательские заголовки HTTP-запроса", "headersTooltip": "Пользовательские заголовки HTTP-запроса",
@@ -3786,6 +3831,7 @@
"logoUrl": "Адрес логотипа", "logoUrl": "Адрес логотипа",
"longRunning": "Mode d'exécution prolongée", "longRunning": "Mode d'exécution prolongée",
"longRunningTooltip": "Une fois activé, le serveur prend en charge les tâches de longue durée, réinitialise le minuteur de temporisation à la réception des notifications de progression, et prolonge le délai d'expiration maximal à 10 minutes.", "longRunningTooltip": "Une fois activé, le serveur prend en charge les tâches de longue durée, réinitialise le minuteur de temporisation à la réception des notifications de progression, et prolonge le délai d'expiration maximal à 10 minutes.",
"marketplaces": "Places de marché",
"missingDependencies": "Manquantes, veuillez les installer pour continuer", "missingDependencies": "Manquantes, veuillez les installer pour continuer",
"more": { "more": {
"awesome": "Liste sélectionnée de serveurs MCP", "awesome": "Liste sélectionnée de serveurs MCP",
@@ -3834,6 +3880,7 @@
"provider": "Поставщик", "provider": "Поставщик",
"providerPlaceholder": "Название поставщика", "providerPlaceholder": "Название поставщика",
"providerUrl": "Адрес поставщика", "providerUrl": "Адрес поставщика",
"providers": "Fournisseurs",
"registry": "Источник управления пакетами", "registry": "Источник управления пакетами",
"registryDefault": "По умолчанию", "registryDefault": "По умолчанию",
"registryTooltip": "Выберите источник для установки пакетов, чтобы решить проблемы с сетью по умолчанию.", "registryTooltip": "Выберите источник для установки пакетов, чтобы решить проблемы с сетью по умолчанию.",
@@ -3856,6 +3903,7 @@
"searchNpx": "Поиск MCP", "searchNpx": "Поиск MCP",
"serverPlural": "Serveurs", "serverPlural": "Serveurs",
"serverSingular": "Serveur", "serverSingular": "Serveur",
"servers": "Serveurs MCP",
"sse": "Серверные отправляемые события (sse)", "sse": "Серверные отправляемые события (sse)",
"startError": "Ошибка запуска", "startError": "Ошибка запуска",
"stdio": "Стандартный ввод/вывод (stdio)", "stdio": "Стандартный ввод/вывод (stdio)",
@@ -4255,6 +4303,12 @@
"aws-bedrock": { "aws-bedrock": {
"access_key_id": "Identifiant de clé d'accès AWS", "access_key_id": "Identifiant de clé d'accès AWS",
"access_key_id_help": "Votre identifiant de clé d'accès AWS, utilisé pour accéder au service AWS Bedrock", "access_key_id_help": "Votre identifiant de clé d'accès AWS, utilisé pour accéder au service AWS Bedrock",
"api_key": "Clé API Bedrock",
"api_key_help": "Votre clé API AWS Bedrock pour l'authentification",
"auth_type": "Type d'authentification",
"auth_type_api_key": "Clé API Bedrock",
"auth_type_help": "Choisissez entre l'authentification par identifiants IAM ou par clé API Bedrock",
"auth_type_iam": "Identifiants IAM",
"description": "AWS Bedrock est un service de modèles de base entièrement géré proposé par Amazon, prenant en charge divers grands modèles linguistiques avancés.", "description": "AWS Bedrock est un service de modèles de base entièrement géré proposé par Amazon, prenant en charge divers grands modèles linguistiques avancés.",
"region": "Région AWS", "region": "Région AWS",
"region_help": "Votre région de service AWS, par exemple us-east-1", "region_help": "Votre région de service AWS, par exemple us-east-1",
+59 -5
View File
@@ -1,6 +1,7 @@
{ {
"agent": { "agent": {
"add": { "add": {
"description": "さまざまなツールを使って複雑なタスクを処理する",
"error": { "error": {
"failed": "エージェントの追加に失敗しました", "failed": "エージェントの追加に失敗しました",
"invalid_agent": "無効なエージェント" "invalid_agent": "無効なエージェント"
@@ -338,6 +339,41 @@
}, },
"title": "API サーバー" "title": "API サーバー"
}, },
"appMenu": {
"about": "について",
"close": "ウィンドウを閉じる",
"copy": "コピー",
"cut": "切る",
"delete": "削除",
"documentation": "ドキュメント",
"edit": "編集",
"feedback": "フィードバック",
"file": "ファイル",
"forceReload": "強制再読み込み",
"front": "すべてを前面に移動",
"help": "助け",
"hide": "隠す",
"hideOthers": "他を隠す",
"minimize": "最小化",
"paste": "ペースト",
"quit": "やめる",
"redo": "やり直し",
"releases": "リリース",
"reload": "リロード",
"resetZoom": "実寸",
"selectAll": "すべて選択",
"services": "サービス",
"toggleDevTools": "開発者ツールを切り替え",
"toggleFullscreen": "全画面表示を切り替え",
"undo": "元に戻す",
"unhide": "すべて表示",
"view": "表示",
"website": "ウェブサイト",
"window": "窓",
"zoom": "ズーム",
"zoomIn": "ズームイン",
"zoomOut": "ズームアウト"
},
"assistants": { "assistants": {
"abbr": "アシスタント", "abbr": "アシスタント",
"clear": { "clear": {
@@ -547,8 +583,12 @@
"chat": { "chat": {
"add": { "add": {
"assistant": { "assistant": {
"description": "日常会話と簡単なQ&A",
"title": "アシスタントを追加" "title": "アシスタントを追加"
}, },
"option": {
"title": "種類を選択"
},
"topic": { "topic": {
"title": "新しいトピック" "title": "新しいトピック"
} }
@@ -1606,7 +1646,7 @@
}, },
"assistant": { "assistant": {
"added": { "added": {
"content": "アシスタントが追加されました" "content": "助手が追加されました"
} }
}, },
"attachments": { "attachments": {
@@ -2477,6 +2517,7 @@
"qiniu": "七牛云 AI 推理", "qiniu": "七牛云 AI 推理",
"qwenlm": "QwenLM", "qwenlm": "QwenLM",
"silicon": "SiliconFlow", "silicon": "SiliconFlow",
"sophnet": "SophNet",
"stepfun": "StepFun", "stepfun": "StepFun",
"tencent-cloud-ti": "Tencent Cloud TI", "tencent-cloud-ti": "Tencent Cloud TI",
"together": "Together", "together": "Together",
@@ -2923,15 +2964,14 @@
}, },
"description": "クリエイターのための強力なAIアシスタント", "description": "クリエイターのための強力なAIアシスタント",
"downloading": "ダウンロード中...", "downloading": "ダウンロード中...",
"enterprise": {
"title": "エンタープライズ"
},
"feedback": { "feedback": {
"button": "フィードバック", "button": "フィードバック",
"title": "フィードバック" "title": "フィードバック"
}, },
"label": "について", "label": "について",
"license": {
"button": "ライセンス",
"title": "ライセンス"
},
"releases": { "releases": {
"button": "リリース", "button": "リリース",
"title": "リリースノート" "title": "リリースノート"
@@ -3761,6 +3801,7 @@
"description": "MCP機能を有効にしない", "description": "MCP機能を有効にしない",
"label": "MCPサーバーを無効にする" "label": "MCPサーバーを無効にする"
}, },
"discover": "発見",
"duplicateName": "同じ名前のサーバーが既に存在します", "duplicateName": "同じ名前のサーバーが既に存在します",
"editJson": "JSONを編集", "editJson": "JSONを編集",
"editMcpJson": "MCP 設定を編集", "editMcpJson": "MCP 設定を編集",
@@ -3771,6 +3812,10 @@
"32000": "MCP サーバーが起動しませんでした。パラメーターを確認してください", "32000": "MCP サーバーが起動しませんでした。パラメーターを確認してください",
"toolNotFound": "ツール {{name}} が見つかりません" "toolNotFound": "ツール {{name}} が見つかりません"
}, },
"fetch": {
"button": "サーバーを取得",
"success": "MCPサーバーの取得に成功しました"
},
"findMore": "MCP を見つける", "findMore": "MCP を見つける",
"headers": "ヘッダー", "headers": "ヘッダー",
"headersTooltip": "HTTP リクエストのカスタムヘッダー", "headersTooltip": "HTTP リクエストのカスタムヘッダー",
@@ -3786,6 +3831,7 @@
"logoUrl": "ロゴURL", "logoUrl": "ロゴURL",
"longRunning": "長時間運行モード", "longRunning": "長時間運行モード",
"longRunningTooltip": "このオプションを有効にすると、サーバーは長時間のタスクをサポートします。進行状況通知を受信すると、タイムアウトがリセットされ、最大実行時間が10分に延長されます。", "longRunningTooltip": "このオプションを有効にすると、サーバーは長時間のタスクをサポートします。進行状況通知を受信すると、タイムアウトがリセットされ、最大実行時間が10分に延長されます。",
"marketplaces": "マーケットプレイス",
"missingDependencies": "が不足しています。続行するにはインストールしてください。", "missingDependencies": "が不足しています。続行するにはインストールしてください。",
"more": { "more": {
"awesome": "厳選された MCP サーバーリスト", "awesome": "厳選された MCP サーバーリスト",
@@ -3834,6 +3880,7 @@
"provider": "プロバイダー", "provider": "プロバイダー",
"providerPlaceholder": "プロバイダー名", "providerPlaceholder": "プロバイダー名",
"providerUrl": "プロバイダーURL", "providerUrl": "プロバイダーURL",
"providers": "プロバイダー",
"registry": "パッケージ管理レジストリ", "registry": "パッケージ管理レジストリ",
"registryDefault": "デフォルト", "registryDefault": "デフォルト",
"registryTooltip": "デフォルトのレジストリでネットワークの問題が発生した場合、パッケージインストールに使用するレジストリを選択してください。", "registryTooltip": "デフォルトのレジストリでネットワークの問題が発生した場合、パッケージインストールに使用するレジストリを選択してください。",
@@ -3856,6 +3903,7 @@
"searchNpx": "MCP を検索", "searchNpx": "MCP を検索",
"serverPlural": "サーバー", "serverPlural": "サーバー",
"serverSingular": "サーバー", "serverSingular": "サーバー",
"servers": "MCPサーバー",
"sse": "サーバー送信イベント (sse)", "sse": "サーバー送信イベント (sse)",
"startError": "起動に失敗しました", "startError": "起動に失敗しました",
"stdio": "標準入力/出力 (stdio)", "stdio": "標準入力/出力 (stdio)",
@@ -4255,6 +4303,12 @@
"aws-bedrock": { "aws-bedrock": {
"access_key_id": "AWS アクセスキー ID", "access_key_id": "AWS アクセスキー ID",
"access_key_id_help": "あなたの AWS アクセスキー ID は、AWS Bedrock サービスへのアクセスに使用されます", "access_key_id_help": "あなたの AWS アクセスキー ID は、AWS Bedrock サービスへのアクセスに使用されます",
"api_key": "Bedrock APIキー",
"api_key_help": "認証用のAWS Bedrock APIキー",
"auth_type": "認証タイプ",
"auth_type_api_key": "Bedrock APIキー",
"auth_type_help": "IAM認証情報とBedrock APIキー認証のどちらかを選択してください",
"auth_type_iam": "IAM認証情報",
"description": "AWS Bedrock は、Amazon が提供する完全に管理されたベースモデルサービスで、さまざまな最先端の大言語モデルをサポートしています", "description": "AWS Bedrock は、Amazon が提供する完全に管理されたベースモデルサービスで、さまざまな最先端の大言語モデルをサポートしています",
"region": "AWS リージョン", "region": "AWS リージョン",
"region_help": "あなたの AWS サービスリージョン、例:us-east-1", "region_help": "あなたの AWS サービスリージョン、例:us-east-1",
+59 -5
View File
@@ -1,6 +1,7 @@
{ {
"agent": { "agent": {
"add": { "add": {
"description": "Lide com tarefas complexas usando várias ferramentas",
"error": { "error": {
"failed": "Falha ao adicionar agente", "failed": "Falha ao adicionar agente",
"invalid_agent": "Agent inválido" "invalid_agent": "Agent inválido"
@@ -338,6 +339,41 @@
}, },
"title": "Servidor API" "title": "Servidor API"
}, },
"appMenu": {
"about": "Sobre",
"close": "Fechar Janela",
"copy": "Copiar",
"cut": "Corte",
"delete": "Excluir",
"documentation": "Documentação",
"edit": "Editar",
"feedback": "Feedback",
"file": "Arquivo",
"forceReload": "Forçar Recarregamento",
"front": "Trazer Tudo para a Frente",
"help": "Ajuda",
"hide": "Esconder",
"hideOthers": "Ocultar Outros",
"minimize": "Minimizar",
"paste": "Colar",
"quit": "Sair",
"redo": "Refazer",
"releases": "Lançamentos",
"reload": "Recarregar",
"resetZoom": "Tamanho Real",
"selectAll": "Selecionar Todos",
"services": "Serviços",
"toggleDevTools": "Alternar Ferramentas de Desenvolvedor",
"toggleFullscreen": "Alternar Tela Cheia",
"undo": "Desfazer",
"unhide": "Mostrar Todos",
"view": "Visualizar",
"website": "Site",
"window": "Janela",
"zoom": "Zoom",
"zoomIn": "Ampliar",
"zoomOut": "Reduzir Zoom"
},
"assistants": { "assistants": {
"abbr": "Assistente", "abbr": "Assistente",
"clear": { "clear": {
@@ -547,8 +583,12 @@
"chat": { "chat": {
"add": { "add": {
"assistant": { "assistant": {
"description": "Conversas diárias e perguntas e respostas rápidas",
"title": "Adicionar assistente" "title": "Adicionar assistente"
}, },
"option": {
"title": "Selecionar Tipo"
},
"topic": { "topic": {
"title": "Novo Tópico" "title": "Novo Tópico"
} }
@@ -2471,12 +2511,13 @@
"openrouter": "OpenRouter", "openrouter": "OpenRouter",
"ovms": "Intel OVMS", "ovms": "Intel OVMS",
"perplexity": "Perplexidade", "perplexity": "Perplexidade",
"ph8": "Plataforma Aberta de Grandes Modelos PH8", "ph8": "PH8",
"poe": "Poe", "poe": "Poe",
"ppio": "PPIO Nuvem Piao", "ppio": "PPIO Nuvem Piao",
"qiniu": "Qiniu AI", "qiniu": "Qiniu AI",
"qwenlm": "QwenLM", "qwenlm": "QwenLM",
"silicon": "Silício em Fluxo", "silicon": "Silício em Fluxo",
"sophnet": "SophNet",
"stepfun": "Função de Passo Estelar", "stepfun": "Função de Passo Estelar",
"tencent-cloud-ti": "Nuvem TI da Tencent", "tencent-cloud-ti": "Nuvem TI da Tencent",
"together": "Juntos", "together": "Juntos",
@@ -2923,15 +2964,14 @@
}, },
"description": "Um assistente de IA criado para criadores", "description": "Um assistente de IA criado para criadores",
"downloading": "Baixando atualizações...", "downloading": "Baixando atualizações...",
"enterprise": {
"title": "Empresa"
},
"feedback": { "feedback": {
"button": "Feedback", "button": "Feedback",
"title": "Enviar feedback" "title": "Enviar feedback"
}, },
"label": "Sobre Nós", "label": "Sobre Nós",
"license": {
"button": "Ver",
"title": "Licença"
},
"releases": { "releases": {
"button": "Ver", "button": "Ver",
"title": "Registro de alterações" "title": "Registro de alterações"
@@ -3761,6 +3801,7 @@
"description": "Não ativar a funcionalidade do serviço MCP", "description": "Não ativar a funcionalidade do serviço MCP",
"label": "Não usar servidor MCP" "label": "Não usar servidor MCP"
}, },
"discover": "Descobrir",
"duplicateName": "Já existe um servidor com o mesmo nome", "duplicateName": "Já existe um servidor com o mesmo nome",
"editJson": "Editar JSON", "editJson": "Editar JSON",
"editMcpJson": "Editar Configuração MCP", "editMcpJson": "Editar Configuração MCP",
@@ -3771,6 +3812,10 @@
"32000": "Falha ao iniciar o servidor MCP, verifique se todos os parâmetros foram preenchidos corretamente conforme o tutorial", "32000": "Falha ao iniciar o servidor MCP, verifique se todos os parâmetros foram preenchidos corretamente conforme o tutorial",
"toolNotFound": "Ferramenta não encontrada {{name}}" "toolNotFound": "Ferramenta não encontrada {{name}}"
}, },
"fetch": {
"button": "Buscar Servidores",
"success": "Servidores MCP obtidos com sucesso"
},
"findMore": "Mais servidores MCP", "findMore": "Mais servidores MCP",
"headers": "Cabeçalhos da Requisição", "headers": "Cabeçalhos da Requisição",
"headersTooltip": "Cabeçalhos HTTP personalizados para as requisições", "headersTooltip": "Cabeçalhos HTTP personalizados para as requisições",
@@ -3786,6 +3831,7 @@
"logoUrl": "URL do Logotipo", "logoUrl": "URL do Logotipo",
"longRunning": "Modo de execução prolongada", "longRunning": "Modo de execução prolongada",
"longRunningTooltip": "Quando ativado, o servidor suporta tarefas de longa duração, redefinindo o temporizador de tempo limite ao receber notificações de progresso e estendendo o tempo máximo de tempo limite para 10 minutos.", "longRunningTooltip": "Quando ativado, o servidor suporta tarefas de longa duração, redefinindo o temporizador de tempo limite ao receber notificações de progresso e estendendo o tempo máximo de tempo limite para 10 minutos.",
"marketplaces": "Mercados",
"missingDependencies": "Ausente, instale para continuar", "missingDependencies": "Ausente, instale para continuar",
"more": { "more": {
"awesome": "Lista selecionada de servidores MCP", "awesome": "Lista selecionada de servidores MCP",
@@ -3834,6 +3880,7 @@
"provider": "Fornecedor", "provider": "Fornecedor",
"providerPlaceholder": "Nome do Fornecedor", "providerPlaceholder": "Nome do Fornecedor",
"providerUrl": "URL do Fornecedor", "providerUrl": "URL do Fornecedor",
"providers": "Fornecedores",
"registry": "Fonte de Gerenciamento de Pacotes", "registry": "Fonte de Gerenciamento de Pacotes",
"registryDefault": "Padrão", "registryDefault": "Padrão",
"registryTooltip": "Selecione uma fonte alternativa para instalar pacotes, caso tenha problemas de rede com a fonte padrão.", "registryTooltip": "Selecione uma fonte alternativa para instalar pacotes, caso tenha problemas de rede com a fonte padrão.",
@@ -3856,6 +3903,7 @@
"searchNpx": "Buscar MCP", "searchNpx": "Buscar MCP",
"serverPlural": "Servidores", "serverPlural": "Servidores",
"serverSingular": "Servidor", "serverSingular": "Servidor",
"servers": "Servidores MCP",
"sse": "Eventos do Servidor (sse)", "sse": "Eventos do Servidor (sse)",
"startError": "Falha ao Iniciar", "startError": "Falha ao Iniciar",
"stdio": "Entrada/Saída Padrão (stdio)", "stdio": "Entrada/Saída Padrão (stdio)",
@@ -4255,6 +4303,12 @@
"aws-bedrock": { "aws-bedrock": {
"access_key_id": "ID da chave de acesso da AWS", "access_key_id": "ID da chave de acesso da AWS",
"access_key_id_help": "O seu ID da chave de acesso AWS, utilizado para aceder ao serviço AWS Bedrock", "access_key_id_help": "O seu ID da chave de acesso AWS, utilizado para aceder ao serviço AWS Bedrock",
"api_key": "Chave de API do Bedrock",
"api_key_help": "Sua Chave de API AWS Bedrock para autenticação",
"auth_type": "Tipo de Autenticação",
"auth_type_api_key": "Chave de API do Bedrock",
"auth_type_help": "Escolha entre credenciais IAM ou autenticação por chave de API do Bedrock",
"auth_type_iam": "Credenciais IAM",
"description": "A AWS Bedrock é um serviço de modelos fundamentais totalmente gerido fornecido pela Amazon, que suporta diversos modelos avançados de linguagem.", "description": "A AWS Bedrock é um serviço de modelos fundamentais totalmente gerido fornecido pela Amazon, que suporta diversos modelos avançados de linguagem.",
"region": "Região da AWS", "region": "Região da AWS",
"region_help": "A sua região de serviço da AWS, por exemplo, us-east-1", "region_help": "A sua região de serviço da AWS, por exemplo, us-east-1",
+59 -5
View File
@@ -1,6 +1,7 @@
{ {
"agent": { "agent": {
"add": { "add": {
"description": "Справляйтесь со сложными задачами с помощью различных инструментов",
"error": { "error": {
"failed": "Не удалось добавить агента", "failed": "Не удалось добавить агента",
"invalid_agent": "Недействительный агент" "invalid_agent": "Недействительный агент"
@@ -338,6 +339,41 @@
}, },
"title": "API Сервер" "title": "API Сервер"
}, },
"appMenu": {
"about": "О",
"close": "Закрыть окно",
"copy": "Копировать",
"cut": "Резать",
"delete": "Удалить",
"documentation": "Документация",
"edit": "Редактировать",
"feedback": "Обратная связь",
"file": "Файл",
"forceReload": "Принудительная перезагрузка",
"front": "Показать все поверх других",
"help": "Помощь",
"hide": "Скрыть",
"hideOthers": "Скрыть остальные",
"minimize": "Минимизировать",
"paste": "Вставить",
"quit": "Выйти",
"redo": "Переделать",
"releases": "Релизы",
"reload": "Перезагрузка",
"resetZoom": "Настоящий размер",
"selectAll": "Выбрать все",
"services": "Услуги",
"toggleDevTools": "Переключить инструменты разработчика",
"toggleFullscreen": "Переключить полноэкранный режим",
"undo": "Отменить",
"unhide": "Показать все",
"view": "Вид",
"website": "Веб-сайт",
"window": "Окно",
"zoom": "Zoom",
"zoomIn": "Увеличить",
"zoomOut": "Отдалить"
},
"assistants": { "assistants": {
"abbr": "Ассистент", "abbr": "Ассистент",
"clear": { "clear": {
@@ -547,8 +583,12 @@
"chat": { "chat": {
"add": { "add": {
"assistant": { "assistant": {
"description": "Ежедневные разговоры и быстрые вопросы и ответы",
"title": "Добавить ассистента" "title": "Добавить ассистента"
}, },
"option": {
"title": "Выберите тип"
},
"topic": { "topic": {
"title": "Новый топик" "title": "Новый топик"
} }
@@ -2477,6 +2517,7 @@
"qiniu": "Qiniu AI", "qiniu": "Qiniu AI",
"qwenlm": "QwenLM", "qwenlm": "QwenLM",
"silicon": "SiliconFlow", "silicon": "SiliconFlow",
"sophnet": "SophNet",
"stepfun": "StepFun", "stepfun": "StepFun",
"tencent-cloud-ti": "Tencent Cloud TI", "tencent-cloud-ti": "Tencent Cloud TI",
"together": "Together", "together": "Together",
@@ -2923,15 +2964,14 @@
}, },
"description": "Мощный AI-ассистент для созидания", "description": "Мощный AI-ассистент для созидания",
"downloading": "Загрузка...", "downloading": "Загрузка...",
"enterprise": {
"title": "Предприятие"
},
"feedback": { "feedback": {
"button": "Обратная связь", "button": "Обратная связь",
"title": "Обратная связь" "title": "Обратная связь"
}, },
"label": "О программе и обратная связь", "label": "О программе и обратная связь",
"license": {
"button": "Лицензия",
"title": "Лицензия"
},
"releases": { "releases": {
"button": "Релизы", "button": "Релизы",
"title": "Заметки о релизах" "title": "Заметки о релизах"
@@ -3043,7 +3083,7 @@
"confirm": { "confirm": {
"button": "Выберите файл резервной копии" "button": "Выберите файл резервной копии"
}, },
"content": "Экспорт части данных, включая чат и настройки. Пожалуйста, обратите внимание, что процесс резервного копирования может занять некоторое время. Благодарим за ваше терпение.", "content": "Экспорт части данных, включая историю чатов и настройки. Обратите внимание, процесс резервного копирования может занять некоторое время, благодарим за ваше терпение.",
"lan": { "lan": {
"auto_close_tip": "Автоматическое закрытие через {{seconds}} секунд...", "auto_close_tip": "Автоматическое закрытие через {{seconds}} секунд...",
"confirm_close_message": "Передача файла в процессе. Закрытие прервет передачу. Вы уверены, что хотите принудительно закрыть?", "confirm_close_message": "Передача файла в процессе. Закрытие прервет передачу. Вы уверены, что хотите принудительно закрыть?",
@@ -3761,6 +3801,7 @@
"description": "Не включать функциональность сервера MCP", "description": "Не включать функциональность сервера MCP",
"label": "Отключить сервер MCP" "label": "Отключить сервер MCP"
}, },
"discover": "Откройте",
"duplicateName": "Сервер с таким именем уже существует", "duplicateName": "Сервер с таким именем уже существует",
"editJson": "Редактировать JSON", "editJson": "Редактировать JSON",
"editMcpJson": "Редактировать MCP", "editMcpJson": "Редактировать MCP",
@@ -3771,6 +3812,10 @@
"32000": "MCP сервер не запущен, пожалуйста, проверьте параметры", "32000": "MCP сервер не запущен, пожалуйста, проверьте параметры",
"toolNotFound": "Инструмент {{name}} не найден" "toolNotFound": "Инструмент {{name}} не найден"
}, },
"fetch": {
"button": "Получить серверы",
"success": "Успешно получены MCP-серверы"
},
"findMore": "Найти больше MCP", "findMore": "Найти больше MCP",
"headers": "Заголовки", "headers": "Заголовки",
"headersTooltip": "Пользовательские заголовки для HTTP-запросов", "headersTooltip": "Пользовательские заголовки для HTTP-запросов",
@@ -3786,6 +3831,7 @@
"logoUrl": "URL логотипа", "logoUrl": "URL логотипа",
"longRunning": "Длительный режим работы", "longRunning": "Длительный режим работы",
"longRunningTooltip": "Включив эту опцию, сервер будет поддерживать длительные задачи. При получении уведомлений о ходе выполнения будет сброшен тайм-аут и максимальное время выполнения будет увеличено до 10 минут.", "longRunningTooltip": "Включив эту опцию, сервер будет поддерживать длительные задачи. При получении уведомлений о ходе выполнения будет сброшен тайм-аут и максимальное время выполнения будет увеличено до 10 минут.",
"marketplaces": "Торговые площадки",
"missingDependencies": "отсутствует, пожалуйста, установите для продолжения.", "missingDependencies": "отсутствует, пожалуйста, установите для продолжения.",
"more": { "more": {
"awesome": "Кураторский список серверов MCP", "awesome": "Кураторский список серверов MCP",
@@ -3834,6 +3880,7 @@
"provider": "Провайдер", "provider": "Провайдер",
"providerPlaceholder": "Имя провайдера", "providerPlaceholder": "Имя провайдера",
"providerUrl": "URL провайдера", "providerUrl": "URL провайдера",
"providers": "Поставщики",
"registry": "Реестр пакетов", "registry": "Реестр пакетов",
"registryDefault": "По умолчанию", "registryDefault": "По умолчанию",
"registryTooltip": "Выберите реестр для установки пакетов, если возникают проблемы с сетью при использовании реестра по умолчанию.", "registryTooltip": "Выберите реестр для установки пакетов, если возникают проблемы с сетью при использовании реестра по умолчанию.",
@@ -3856,6 +3903,7 @@
"searchNpx": "Найти MCP", "searchNpx": "Найти MCP",
"serverPlural": "серверы", "serverPlural": "серверы",
"serverSingular": "сервер", "serverSingular": "сервер",
"servers": "Серверы MCP",
"sse": "События, отправляемые сервером (sse)", "sse": "События, отправляемые сервером (sse)",
"startError": "Запуск не удалось", "startError": "Запуск не удалось",
"stdio": "Стандартный ввод/вывод (stdio)", "stdio": "Стандартный ввод/вывод (stdio)",
@@ -4255,6 +4303,12 @@
"aws-bedrock": { "aws-bedrock": {
"access_key_id": "AWS Ключ доступа ID", "access_key_id": "AWS Ключ доступа ID",
"access_key_id_help": "Ваш AWS Ключ доступа ID для доступа к AWS Bedrock", "access_key_id_help": "Ваш AWS Ключ доступа ID для доступа к AWS Bedrock",
"api_key": "Ключ API Bedrock",
"api_key_help": "Ваш ключ API AWS Bedrock для аутентификации",
"auth_type": "Тип аутентификации",
"auth_type_api_key": "Ключ API Bedrock",
"auth_type_help": "Выберите между аутентификацией с помощью учетных данных IAM или ключа API Bedrock",
"auth_type_iam": "Учетные данные IAM",
"description": "AWS Bedrock — это полное управляемое сервисное предложение для моделей, поддерживающее различные современные модели языка", "description": "AWS Bedrock — это полное управляемое сервисное предложение для моделей, поддерживающее различные современные модели языка",
"region": "AWS регион", "region": "AWS регион",
"region_help": "Ваш регион AWS, например us-east-1", "region_help": "Ваш регион AWS, например us-east-1",
+4 -17
View File
@@ -1,4 +1,3 @@
import { Alert } from '@heroui/react'
import { loggerService } from '@logger' import { loggerService } from '@logger'
import type { ContentSearchRef } from '@renderer/components/ContentSearch' import type { ContentSearchRef } from '@renderer/components/ContentSearch'
import { ContentSearch } from '@renderer/components/ContentSearch' import { ContentSearch } from '@renderer/components/ContentSearch'
@@ -17,7 +16,7 @@ import { useTimer } from '@renderer/hooks/useTimer'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import type { Assistant, Topic } from '@renderer/types' import type { Assistant, Topic } from '@renderer/types'
import { classNames } from '@renderer/utils' import { classNames } from '@renderer/utils'
import { Flex } from 'antd' import { Alert, Flex } from 'antd'
import { debounce } from 'lodash' import { debounce } from 'lodash'
import { AnimatePresence, motion } from 'motion/react' import { AnimatePresence, motion } from 'motion/react'
import type { FC } from 'react' import type { FC } from 'react'
@@ -170,11 +169,7 @@ const Chat: FC<Props> = (props) => {
return () => <div> Active Session ID is invalid.</div> return () => <div> Active Session ID is invalid.</div>
} }
if (!apiServer.enabled) { if (!apiServer.enabled) {
return () => ( return () => <Alert type="warning" message={t('agent.warning.enable_server')} style={{ margin: '5px 16px' }} />
<div>
<Alert color="warning" title={t('agent.warning.enable_server')} />
</div>
)
} }
return () => <AgentSessionMessages agentId={activeAgentId} sessionId={activeSessionId} /> return () => <AgentSessionMessages agentId={activeAgentId} sessionId={activeSessionId} />
}, [activeAgentId, activeSessionId, apiServer.enabled, t]) }, [activeAgentId, activeSessionId, apiServer.enabled, t])
@@ -191,22 +186,14 @@ const Chat: FC<Props> = (props) => {
// TODO: more info // TODO: more info
const AgentInvalid = useCallback(() => { const AgentInvalid = useCallback(() => {
return ( return <Alert type="warning" message="Select an agent" style={{ margin: '5px 16px' }} />
<div className="flex h-full w-full items-center justify-center">
<div>
<Alert color="warning" title="Select an agent" />
</div>
</div>
)
}, []) }, [])
// TODO: more info // TODO: more info
const SessionInvalid = useCallback(() => { const SessionInvalid = useCallback(() => {
return ( return (
<div className="flex h-full w-full items-center justify-center"> <div className="flex h-full w-full items-center justify-center">
<div> <Alert type="warning" message="Create a session" style={{ margin: '5px 16px' }} />
<Alert color="warning" title="Create a session" />
</div>
</div> </div>
) )
}, []) }, [])
@@ -1,4 +1,3 @@
import { Tooltip } from '@heroui/react'
import { loggerService } from '@logger' import { loggerService } from '@logger'
import { ActionIconButton } from '@renderer/components/Buttons' import { ActionIconButton } from '@renderer/components/Buttons'
import { QuickPanelView } from '@renderer/components/QuickPanel' import { QuickPanelView } from '@renderer/components/QuickPanel'
@@ -11,6 +10,7 @@ import { useShortcutDisplay } from '@renderer/hooks/useShortcuts'
import { useTimer } from '@renderer/hooks/useTimer' import { useTimer } from '@renderer/hooks/useTimer'
import PasteService from '@renderer/services/PasteService' import PasteService from '@renderer/services/PasteService'
import { pauseTrace } from '@renderer/services/SpanManagerService' import { pauseTrace } from '@renderer/services/SpanManagerService'
import { estimateUserPromptUsage } from '@renderer/services/TokenService'
import { useAppDispatch, useAppSelector } from '@renderer/store' import { useAppDispatch, useAppSelector } from '@renderer/store'
import { newMessagesActions, selectMessagesForTopic } from '@renderer/store/newMessage' import { newMessagesActions, selectMessagesForTopic } from '@renderer/store/newMessage'
import { sendMessage as dispatchSendMessage } from '@renderer/store/thunk/messageThunk' import { sendMessage as dispatchSendMessage } from '@renderer/store/thunk/messageThunk'
@@ -22,6 +22,7 @@ import { abortCompletion } from '@renderer/utils/abortController'
import { buildAgentSessionTopicId } from '@renderer/utils/agentSession' import { buildAgentSessionTopicId } from '@renderer/utils/agentSession'
import { getSendMessageShortcutLabel, isSendMessageKeyPressed } from '@renderer/utils/input' import { getSendMessageShortcutLabel, isSendMessageKeyPressed } from '@renderer/utils/input'
import { createMainTextBlock, createMessage } from '@renderer/utils/messageUtils/create' import { createMainTextBlock, createMessage } from '@renderer/utils/messageUtils/create'
import { Tooltip } from 'antd'
import type { TextAreaRef } from 'antd/es/input/TextArea' import type { TextAreaRef } from 'antd/es/input/TextArea'
import TextArea from 'antd/es/input/TextArea' import TextArea from 'antd/es/input/TextArea'
import { isEmpty } from 'lodash' import { isEmpty } from 'lodash'
@@ -199,11 +200,15 @@ const AgentSessionInputbar: FC<Props> = ({ agentId, sessionId }) => {
} }
: undefined : undefined
// Calculate token usage for the user message
const usage = await estimateUserPromptUsage({ content: text })
const userMessage: Message = createMessage('user', sessionTopicId, agentId, { const userMessage: Message = createMessage('user', sessionTopicId, agentId, {
id: userMessageId, id: userMessageId,
blocks: userMessageBlocks.map((block) => block?.id), blocks: userMessageBlocks.map((block) => block?.id),
model, model,
modelId: model?.id modelId: model?.id,
usage
}) })
const assistantStub: Assistant = { const assistantStub: Assistant = {
@@ -309,7 +314,7 @@ const AgentSessionInputbar: FC<Props> = ({ agentId, sessionId }) => {
/> />
<Toolbar> <Toolbar>
<ToolbarGroup> <ToolbarGroup>
<Tooltip placement="top" content={t('chat.input.new_topic', { Command: newTopicShortcut })} delay={0}> <Tooltip placement="top" title={t('chat.input.new_topic', { Command: newTopicShortcut })}>
<ActionIconButton <ActionIconButton
onClick={handleCreateSession} onClick={handleCreateSession}
disabled={createSessionDisabled} disabled={createSessionDisabled}
@@ -321,7 +326,7 @@ const AgentSessionInputbar: FC<Props> = ({ agentId, sessionId }) => {
<ToolbarGroup> <ToolbarGroup>
<SendMessageButton sendMessage={sendMessage} disabled={sendDisabled} /> <SendMessageButton sendMessage={sendMessage} disabled={sendDisabled} />
{canAbort && ( {canAbort && (
<Tooltip placement="top" content={t('chat.input.pause')}> <Tooltip placement="top" title={t('chat.input.pause')}>
<ActionIconButton onClick={abortAgentSession} style={{ marginRight: -2 }}> <ActionIconButton onClick={abortAgentSession} style={{ marginRight: -2 }}>
<CirclePause size={20} color="var(--color-error)" /> <CirclePause size={20} color="var(--color-error)" />
</ActionIconButton> </ActionIconButton>
@@ -2,6 +2,7 @@ import type { DropResult } from '@hello-pangea/dnd'
import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd' import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd'
import { loggerService } from '@logger' import { loggerService } from '@logger'
import { ActionIconButton } from '@renderer/components/Buttons' import { ActionIconButton } from '@renderer/components/Buttons'
import { MdiLightbulbOn } from '@renderer/components/Icons'
import type { QuickPanelListItem } from '@renderer/components/QuickPanel' import type { QuickPanelListItem } from '@renderer/components/QuickPanel'
import { import {
isAnthropicModel, isAnthropicModel,
@@ -230,6 +231,15 @@ const InputbarTools = ({
quickPhrasesButtonRef.current?.openQuickPanel() quickPhrasesButtonRef.current?.openQuickPanel()
} }
}, },
{
label: t('assistants.settings.reasoning_effort.label'),
description: '',
icon: <MdiLightbulbOn />,
isMenu: true,
action: () => {
thinkingButtonRef.current?.openQuickPanel()
}
},
{ {
label: t('assistants.presets.edit.model.select.title'), label: t('assistants.presets.edit.model.select.title'),
description: '', description: '',
@@ -245,6 +255,7 @@ const InputbarTools = ({
icon: <FileSearch />, icon: <FileSearch />,
isMenu: true, isMenu: true,
disabled: files.length > 0, disabled: files.length > 0,
hidden: !showKnowledgeBaseButton,
action: () => { action: () => {
knowledgeBaseButtonRef.current?.openQuickPanel() knowledgeBaseButtonRef.current?.openQuickPanel()
} }
@@ -312,7 +323,7 @@ const InputbarTools = ({
translate() translate()
} }
} }
] ] satisfies QuickPanelListItem[]
} }
const handleDragEnd = (result: DropResult) => { const handleDragEnd = (result: DropResult) => {
@@ -1,4 +1,3 @@
import { Button } from '@heroui/button'
import CodeViewer from '@renderer/components/CodeViewer' import CodeViewer from '@renderer/components/CodeViewer'
import { useCodeStyle } from '@renderer/context/CodeStyleProvider' import { useCodeStyle } from '@renderer/context/CodeStyleProvider'
import { useTimer } from '@renderer/hooks/useTimer' import { useTimer } from '@renderer/hooks/useTimer'
@@ -33,6 +32,7 @@ import {
} from '@renderer/types/error' } from '@renderer/types/error'
import type { ErrorMessageBlock, Message } from '@renderer/types/newMessage' import type { ErrorMessageBlock, Message } from '@renderer/types/newMessage'
import { formatAiSdkError, formatError, safeToString } from '@renderer/utils/error' import { formatAiSdkError, formatError, safeToString } from '@renderer/utils/error'
import { Button } from 'antd'
import { Alert as AntdAlert, Modal } from 'antd' import { Alert as AntdAlert, Modal } from 'antd'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { Trans, useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
@@ -144,9 +144,11 @@ const MessageErrorInfo: React.FC<{ block: ErrorMessageBlock; message: Message }>
onClick={showErrorDetail} onClick={showErrorDetail}
style={{ cursor: 'pointer' }} style={{ cursor: 'pointer' }}
action={ action={
<Button size="sm" className="p-0" variant="light" onPress={showErrorDetail}> <>
{t('common.detail')} <Button size="middle" color="default" variant="text" onClick={showErrorDetail}>
</Button> {t('common.detail')}
</Button>
</>
} }
/> />
<ErrorDetailModal open={showDetailModal} onClose={() => setShowDetailModal(false)} error={block.error} /> <ErrorDetailModal open={showDetailModal} onClose={() => setShowDetailModal(false)} error={block.error} />
@@ -198,10 +200,10 @@ const ErrorDetailModal: React.FC<ErrorDetailModalProps> = ({ open, onClose, erro
open={open} open={open}
onCancel={onClose} onCancel={onClose}
footer={[ footer={[
<Button key="copy" size="sm" variant="light" onPress={copyErrorDetails}> <Button key="copy" variant="text" color="default" onClick={copyErrorDetails}>
{t('common.copy')} {t('common.copy')}
</Button>, </Button>,
<Button key="close" size="sm" variant="light" onPress={onClose}> <Button key="close" variant="text" color={'default'} onClick={onClose}>
{t('common.close')} {t('common.close')}
</Button> </Button>
]} ]}
@@ -1,6 +1,6 @@
import { Spinner } from '@heroui/react'
import { MessageBlockStatus, MessageBlockType, type PlaceholderMessageBlock } from '@renderer/types/newMessage' import { MessageBlockStatus, MessageBlockType, type PlaceholderMessageBlock } from '@renderer/types/newMessage'
import React from 'react' import React from 'react'
import { BeatLoader } from 'react-spinners'
import styled from 'styled-components' import styled from 'styled-components'
interface PlaceholderBlockProps { interface PlaceholderBlockProps {
@@ -10,7 +10,7 @@ const PlaceholderBlock: React.FC<PlaceholderBlockProps> = ({ block }) => {
if (block.status === MessageBlockStatus.PROCESSING && block.type === MessageBlockType.UNKNOWN) { if (block.status === MessageBlockStatus.PROCESSING && block.type === MessageBlockType.UNKNOWN) {
return ( return (
<MessageContentLoading> <MessageContentLoading>
<Spinner color="current" variant="dots" /> <BeatLoader color="var(--color-text-1)" size={8} speedMultiplier={0.8} />
</MessageContentLoading> </MessageContentLoading>
) )
} }
@@ -1,4 +1,3 @@
import { cn } from '@heroui/react'
import { loggerService } from '@logger' import { loggerService } from '@logger'
import HorizontalScrollContainer from '@renderer/components/HorizontalScrollContainer' import HorizontalScrollContainer from '@renderer/components/HorizontalScrollContainer'
import Scrollbar from '@renderer/components/Scrollbar' import Scrollbar from '@renderer/components/Scrollbar'
@@ -15,7 +14,7 @@ import { getModelUniqId } from '@renderer/services/ModelService'
import { estimateMessageUsage } from '@renderer/services/TokenService' import { estimateMessageUsage } from '@renderer/services/TokenService'
import type { Assistant, Topic } from '@renderer/types' import type { Assistant, Topic } from '@renderer/types'
import type { Message, MessageBlock } from '@renderer/types/newMessage' import type { Message, MessageBlock } from '@renderer/types/newMessage'
import { classNames } from '@renderer/utils' import { classNames, cn } from '@renderer/utils'
import { isMessageProcessing } from '@renderer/utils/messageUtils/is' import { isMessageProcessing } from '@renderer/utils/messageUtils/is'
import { Divider } from 'antd' import { Divider } from 'antd'
import type { Dispatch, FC, SetStateAction } from 'react' import type { Dispatch, FC, SetStateAction } from 'react'
@@ -1,4 +1,5 @@
import { AccordionItem, Chip, Code } from '@heroui/react' import type { CollapseProps } from 'antd'
import { Tag } from 'antd'
import { CheckCircle, Terminal, XCircle } from 'lucide-react' import { CheckCircle, Terminal, XCircle } from 'lucide-react'
import { useMemo } from 'react' import { useMemo } from 'react'
@@ -15,7 +16,13 @@ interface ParsedBashOutput {
tool_use_error?: string tool_use_error?: string
} }
export function BashOutputTool({ input, output }: { input: BashOutputToolInput; output?: BashOutputToolOutput }) { export function BashOutputTool({
input,
output
}: {
input: BashOutputToolInput
output?: BashOutputToolOutput
}): NonNullable<CollapseProps['items']>[number] {
// 解析 XML 输出 // 解析 XML 输出
const parsedOutput = useMemo(() => { const parsedOutput = useMemo(() => {
if (!output) return null if (!output) return null
@@ -84,93 +91,88 @@ export function BashOutputTool({ input, output }: { input: BashOutputToolInput;
} as const } as const
}, [parsedOutput]) }, [parsedOutput])
return ( const children = parsedOutput ? (
<AccordionItem <div className="flex flex-col gap-4">
key={AgentToolsType.BashOutput} {/* Status Info */}
aria-label="BashOutput Tool" <div className="flex flex-wrap items-center gap-2">
title={ {parsedOutput.exit_code !== undefined && (
<Tag color={parsedOutput.exit_code === 0 ? 'success' : 'danger'}>Exit Code: {parsedOutput.exit_code}</Tag>
)}
{parsedOutput.timestamp && (
<Tag className="py-0 font-mono text-xs">{new Date(parsedOutput.timestamp).toLocaleString()}</Tag>
)}
</div>
{/* Standard Output */}
{parsedOutput.stdout && (
<div>
<div className="mb-2 font-medium text-default-600 text-xs">stdout:</div>
<pre className="whitespace-pre-wrap font-mono text-default-700 text-xs dark:text-default-300">
{parsedOutput.stdout}
</pre>
</div>
)}
{/* Standard Error */}
{parsedOutput.stderr && (
<div className="border border-danger-200">
<div className="mb-2 font-medium text-danger-600 text-xs">stderr:</div>
<pre className="whitespace-pre-wrap font-mono text-danger-600 text-xs dark:text-danger-400">
{parsedOutput.stderr}
</pre>
</div>
)}
{/* Tool Use Error */}
{parsedOutput.tool_use_error && (
<div className="border border-danger-200">
<div className="mb-2 flex items-center gap-2">
<XCircle className="h-4 w-4 text-danger" />
<span className="font-medium text-danger-600 text-xs">Error:</span>
</div>
<pre className="whitespace-pre-wrap font-mono text-danger-600 text-xs dark:text-danger-400">
{parsedOutput.tool_use_error}
</pre>
</div>
)}
</div>
) : (
// 原始输出(如果解析失败或非 XML 格式)
output && (
<div>
<pre className="whitespace-pre-wrap font-mono text-default-700 text-xs dark:text-default-300">{output}</pre>
</div>
)
)
return {
key: AgentToolsType.BashOutput,
label: (
<>
<ToolTitle <ToolTitle
icon={<Terminal className="h-4 w-4" />} icon={<Terminal className="h-4 w-4" />}
label="Bash Output" label="Bash Output"
params={ params={
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Code size="sm" className="py-0 text-xs"> <Tag className="py-0 font-mono text-xs">{input.bash_id}</Tag>
{input.bash_id}
</Code>
{statusConfig && ( {statusConfig && (
<Chip <Tag
size="sm"
color={statusConfig.color} color={statusConfig.color}
variant="flat" icon={statusConfig.icon}
startContent={statusConfig.icon} style={{
className="h-5"> display: 'flex',
flexDirection: 'row',
alignItems: 'center',
gap: '2px'
}}>
{statusConfig.text} {statusConfig.text}
</Chip> </Tag>
)} )}
</div> </div>
} }
/> />
} </>
classNames={{ ),
content: 'space-y-3 px-1'
}}>
{parsedOutput ? (
<>
{/* Status Info */}
<div className="flex flex-wrap items-center gap-2">
{parsedOutput.exit_code !== undefined && (
<Chip size="sm" color={parsedOutput.exit_code === 0 ? 'success' : 'danger'} variant="flat">
Exit Code: {parsedOutput.exit_code}
</Chip>
)}
{parsedOutput.timestamp && (
<Code size="sm" className="py-0 text-xs">
{new Date(parsedOutput.timestamp).toLocaleString()}
</Code>
)}
</div>
{/* Standard Output */} children: children
{parsedOutput.stdout && ( }
<div>
<div className="mb-2 font-medium text-default-600 text-xs">stdout:</div>
<pre className="whitespace-pre-wrap font-mono text-default-700 text-xs dark:text-default-300">
{parsedOutput.stdout}
</pre>
</div>
)}
{/* Standard Error */}
{parsedOutput.stderr && (
<div className="border border-danger-200">
<div className="mb-2 font-medium text-danger-600 text-xs">stderr:</div>
<pre className="whitespace-pre-wrap font-mono text-danger-600 text-xs dark:text-danger-400">
{parsedOutput.stderr}
</pre>
</div>
)}
{/* Tool Use Error */}
{parsedOutput.tool_use_error && (
<div className="border border-danger-200">
<div className="mb-2 flex items-center gap-2">
<XCircle className="h-4 w-4 text-danger" />
<span className="font-medium text-danger-600 text-xs">Error:</span>
</div>
<pre className="whitespace-pre-wrap font-mono text-danger-600 text-xs dark:text-danger-400">
{parsedOutput.tool_use_error}
</pre>
</div>
)}
</>
) : (
// 原始输出(如果解析失败或非 XML 格式)
output && (
<div>
<pre className="whitespace-pre-wrap font-mono text-default-700 text-xs dark:text-default-300">{output}</pre>
</div>
)
)}
</AccordionItem>
)
} }
@@ -1,31 +1,35 @@
import { AccordionItem, Code } from '@heroui/react' import type { CollapseProps } from 'antd'
import { Tag } from 'antd'
import { Terminal } from 'lucide-react' import { Terminal } from 'lucide-react'
import { ToolTitle } from './GenericTools' import { ToolTitle } from './GenericTools'
import type { BashToolInput as BashToolInputType, BashToolOutput as BashToolOutputType } from './types' import type { BashToolInput as BashToolInputType, BashToolOutput as BashToolOutputType } from './types'
export function BashTool({ input, output }: { input: BashToolInputType; output?: BashToolOutputType }) { export function BashTool({
input,
output
}: {
input: BashToolInputType
output?: BashToolOutputType
}): NonNullable<CollapseProps['items']>[number] {
// 如果有输出,计算输出行数 // 如果有输出,计算输出行数
const outputLines = output ? output.split('\n').length : 0 const outputLines = output ? output.split('\n').length : 0
return ( return {
<AccordionItem key: 'tool',
key="tool" label: (
aria-label="Bash Tool" <>
title={
<ToolTitle <ToolTitle
icon={<Terminal className="h-4 w-4" />} icon={<Terminal className="h-4 w-4" />}
label="Bash" label="Bash"
params={input.description} params={input.description}
stats={output ? `${outputLines} ${outputLines === 1 ? 'line' : 'lines'}` : undefined} stats={output ? `${outputLines} ${outputLines === 1 ? 'line' : 'lines'}` : undefined}
/> />
} <div className="mt-1">
subtitle={ <Tag className="whitespace-pre-wrap break-all font-mono">{input.command}</Tag>
<Code size="sm" className="line-clamp-1 w-max max-w-full text-ellipsis py-0 text-xs"> </div>
{input.command} </>
</Code> ),
}> children: <div className="whitespace-pre-line">{output}</div>
<div className="whitespace-pre-line">{output}</div> }
</AccordionItem>
)
} }
@@ -1,4 +1,4 @@
import { AccordionItem } from '@heroui/react' import type { CollapseProps } from 'antd'
import { FileEdit } from 'lucide-react' import { FileEdit } from 'lucide-react'
import { ToolTitle } from './GenericTools' import { ToolTitle } from './GenericTools'
@@ -28,19 +28,26 @@ export const renderCodeBlock = (content: string, variant: 'old' | 'new') => {
) )
} }
export function EditTool({ input, output }: { input: EditToolInput; output?: EditToolOutput }) { export function EditTool({
return ( input,
<AccordionItem output
key={AgentToolsType.Edit} }: {
aria-label="Edit Tool" input: EditToolInput
title={<ToolTitle icon={<FileEdit className="h-4 w-4" />} label="Edit" params={input.file_path} />}> output?: EditToolOutput
{/* Diff View */} }): NonNullable<CollapseProps['items']>[number] {
{/* Old Content */} return {
{renderCodeBlock(input.old_string, 'old')} key: AgentToolsType.Edit,
{/* New Content */} label: <ToolTitle icon={<FileEdit className="h-4 w-4" />} label="Edit" params={input.file_path} />,
{renderCodeBlock(input.new_string, 'new')} children: (
{/* Output */} <>
{output} {/* Diff View */}
</AccordionItem> {/* Old Content */}
) {renderCodeBlock(input.old_string, 'old')}
{/* New Content */}
{renderCodeBlock(input.new_string, 'new')}
{/* Output */}
{output}
</>
)
}
} }
@@ -1,4 +1,4 @@
import { AccordionItem } from '@heroui/react' import type { CollapseProps } from 'antd'
import { DoorOpen } from 'lucide-react' import { DoorOpen } from 'lucide-react'
import ReactMarkdown from 'react-markdown' import ReactMarkdown from 'react-markdown'
@@ -6,19 +6,22 @@ import { ToolTitle } from './GenericTools'
import type { ExitPlanModeToolInput, ExitPlanModeToolOutput } from './types' import type { ExitPlanModeToolInput, ExitPlanModeToolOutput } from './types'
import { AgentToolsType } from './types' import { AgentToolsType } from './types'
export function ExitPlanModeTool({ input, output }: { input: ExitPlanModeToolInput; output?: ExitPlanModeToolOutput }) { export function ExitPlanModeTool({
return ( input,
<AccordionItem output
key={AgentToolsType.ExitPlanMode} }: {
aria-label="ExitPlanMode Tool" input: ExitPlanModeToolInput
title={ output?: ExitPlanModeToolOutput
<ToolTitle }): NonNullable<CollapseProps['items']>[number] {
icon={<DoorOpen className="h-4 w-4" />} return {
label="ExitPlanMode" key: AgentToolsType.ExitPlanMode,
stats={`${input.plan.split('\n\n').length} plans`} label: (
/> <ToolTitle
}> icon={<DoorOpen className="h-4 w-4" />}
{<ReactMarkdown>{input.plan + '\n\n' + (output ?? '')}</ReactMarkdown>} label="ExitPlanMode"
</AccordionItem> stats={`${input.plan.split('\n\n').length} plans`}
) />
),
children: <ReactMarkdown>{input.plan + '\n\n' + (output ?? '')}</ReactMarkdown>
}
} }
@@ -1,26 +1,29 @@
import { AccordionItem } from '@heroui/react' import type { CollapseProps } from 'antd'
import { FolderSearch } from 'lucide-react' import { FolderSearch } from 'lucide-react'
import { ToolTitle } from './GenericTools' import { ToolTitle } from './GenericTools'
import type { GlobToolInput as GlobToolInputType, GlobToolOutput as GlobToolOutputType } from './types' import type { GlobToolInput as GlobToolInputType, GlobToolOutput as GlobToolOutputType } from './types'
export function GlobTool({ input, output }: { input: GlobToolInputType; output?: GlobToolOutputType }) { export function GlobTool({
input,
output
}: {
input: GlobToolInputType
output?: GlobToolOutputType
}): NonNullable<CollapseProps['items']>[number] {
// 如果有输出,计算文件数量 // 如果有输出,计算文件数量
const lineCount = output ? output.split('\n').filter((line) => line.trim()).length : 0 const lineCount = output ? output.split('\n').filter((line) => line.trim()).length : 0
return ( return {
<AccordionItem key: 'tool',
key="tool" label: (
aria-label="Glob Tool" <ToolTitle
title={ icon={<FolderSearch className="h-4 w-4" />}
<ToolTitle label="Glob"
icon={<FolderSearch className="h-4 w-4" />} params={input.pattern}
label="Glob" stats={output ? `${lineCount} ${lineCount === 1 ? 'file' : 'files'}` : undefined}
params={input.pattern} />
stats={output ? `${lineCount} of output` : undefined} ),
/> children: <div>{output}</div>
}> }
<div>{output}</div>
</AccordionItem>
)
} }
@@ -1,31 +1,34 @@
import { AccordionItem } from '@heroui/react' import type { CollapseProps } from 'antd'
import { FileSearch } from 'lucide-react' import { FileSearch } from 'lucide-react'
import { ToolTitle } from './GenericTools' import { ToolTitle } from './GenericTools'
import type { GrepToolInput, GrepToolOutput } from './types' import type { GrepToolInput, GrepToolOutput } from './types'
export function GrepTool({ input, output }: { input: GrepToolInput; output?: GrepToolOutput }) { export function GrepTool({
input,
output
}: {
input: GrepToolInput
output?: GrepToolOutput
}): NonNullable<CollapseProps['items']>[number] {
// 如果有输出,计算结果行数 // 如果有输出,计算结果行数
const resultLines = output ? output.split('\n').filter((line) => line.trim()).length : 0 const resultLines = output ? output.split('\n').filter((line) => line.trim()).length : 0
return ( return {
<AccordionItem key: 'tool',
key="tool" label: (
aria-label="Grep Tool" <ToolTitle
title={ icon={<FileSearch className="h-4 w-4" />}
<ToolTitle label="Grep"
icon={<FileSearch className="h-4 w-4" />} params={
label="Grep" <>
params={ {input.pattern}
<> {input.output_mode && <span className="ml-1">({input.output_mode})</span>}
{input.pattern} </>
{input.output_mode && <span className="ml-1">({input.output_mode})</span>} }
</> stats={output ? `${resultLines} ${resultLines === 1 ? 'line' : 'lines'}` : undefined}
} />
stats={output ? `${resultLines} ${resultLines === 1 ? 'line' : 'lines'}` : undefined} ),
/> children: <div>{output}</div>
}> }
<div>{output}</div>
</AccordionItem>
)
} }

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